I was trying to call a controller function from a directive in order to update a counter inside an hash-map.
After reading a few solutions, I ended up doing this:
'use strict';
var dragDropApp = angular.module('dragDropApp', []);
dragDropApp.controller('DragDropCtrl', function($scope) {
$scope.itemCount = {
'item1' : {
'count' : 0
},
'item2' : {
'count' : 0
},
'item3' : {
'count' : 0
}
};
//this.updateItemCounter = function(itemId) {
// $scope.itemCount[itemId].count++;
//}
$scope.updateItemCounter = function(itemId) {
$scope.itemCount[itemId].count++;
}
}
dragDropApp.directive('droppable', function() {
return {
restrict : 'A',
scope : {
drop : '&', // parent
bin : '=' // bi-directional scope
},
controller : 'DragDropCtrl',
link : function(scope, element, attrs, DragDropCtrl) {
var el = element[0];
el.addEventListener('drop', function(e) {
var item = document.getElementById(
e.dataTransfer.getData('Text')).cloneNode(true);
//DragDropCtrl.updateItemCounter(item.id);
>>>> scope.$parent.updateItemCounter(item.id); <<<<
return false;
}, false);
}
}
});
It works and does what I want, but I don't know if this approach is correct. Is it?
I've also tried to use the controller to access the function updateItemCounter, but the hash-map does not get updated, having the same values every time I call the function.
In html you should set attributes like this:
<div droppable bin="something" drop="updateItemCounter(itemId)"></div>
In directive if you want to call your controller's function you should call it like this. Note that property name of passed object must be the same as used in html
link : function(scope, element, attrs, DragDropCtrl) {
...
scope.updateItemCounter({itemId: item.id});
...
}
if you wold like to pass more arguments you should use in html:
<div droppable drop="updateItemCounter(itemId, param2, param3)"></div>
and call:
scope.updateItemCounter({itemId: item.id, param2: 'something', param3: 'something'});
Read these answers:
https://stackoverflow.com/a/27997722/1937797
https://stackoverflow.com/a/23843218/1937797
Related
I have 2 set of arrays. the first set is being as the default. when user click the next button, i need to update the new set. I can do this.
But the previous set as well exist. I don't know how to properly remove that, and define the new values. I am looking for the way that, which remove the event listner while remove the DOM. and memory leak should not be as well.
here is my js :
var myApp = angular.module('myApp', []);
myApp.controller('main', function ($scope) {
$scope.values = [{"name":"one", "num" : 1}, {"name":"two", "num" : 2}, {"name":"three", "num" : 3}];
$scope.next = function () {
$scope.index = 4;
$scope.values = [{"name":"four", "num" : 4}, {"name":"five", "num" : 5}, {"name":"six", "num" : 6}];
}
$scope.index = 0;
$scope.update = function (value) {
console.log("clicked " + value.num);
$scope.index = value.num;
$scope.$apply();
}
});
myApp.directive("newArray", function ($compile) {
return {
scope : {
value : "=",
index : "=",
update:"&"
},
link : function (scope, element, attrs) {
var getTemplate = function (value, index) {
switch(index) {
case 0 :
return '<div ng-click="update()">I am here {{index}} {{value.name}}</div>'
break;
case 1 :
return $('<div />', {
class:'blue',
html : "<h1>testing{{index}} {{value.name}}</h1>",
click : function () {
scope.update({num: scope.value.num});
}
});
break;
case 2 :
return $('<div />', {
class:'green',
html : "<h1>testing{{index}} {{value.name}}</h1>",
click : function () {
scope.update({num: scope.value.num});
}
});
break;
}
}
element.html(getTemplate(scope.value, scope.index));
$compile(element.contents())(scope);
element.replaceWith(element.contents());
}
}
});
Live Demo
updating my plnkr is appreciated. may help my future reference.
For such a different requirement I'll not go for the isolated scope directive that will mess up, As you want to use replace directive DOM with the the directive template. Another reason you are using ng-repeat on your directive which is not maintaining DOM structure in proper manner as you are replacing directive DOM with the newly constructed DOM. Instead of which I created a simple directive that do loop inside directive and create a element with new isolated scope & appending it to Pseudo element inside watcher.
Markup
<body ng-controller="main">
<a ng-click="next()" href="#">Next</a>
<h1>{{index}}</h1>
<new-array values='values'></new-array>
</body>
Directive
myApp.directive("newArray", function ($compile) {
return {
link : function (scope, element, attrs) {
var getTemplate = function (value, index) {
var newScope = scope.$new(true);
newScope.value = value;
switch(index) {
case 0 :
newScope.index = index;
return $compile('<div ng-click="$parent.update(value)">I am here {{value.num}} {{value.name}}</div>')(newScope)
break;
case 1 :
return $compile($('<div />', {
class:'blue',
html : "<h1>testing{{index}} {{value.name}}</h1>",
click : function () {
scope.update({num: scope.values[index].num});
}
}))(newScope);
break;
case 2 :
return $compile($('<div />', {
class:'green',
html : "<h1>testing{{index}} {{value.name}}</h1>",
click : function () {
scope.update({num: scope.values[index].num});
}
}))(newScope);
break;
}
}
scope.$watch('values', function(newVal){
var html = '', dom = $('<div/>');
element.empty();
for(var i=0;i < newVal.length; i++){
element.append(getTemplate(newVal[i], i))
}
//element.replaceWith(dom)
}, true);
}
}
})
Working Plunkr
Note
You can not really think of $destroy in your case. It just like
destructor which used to clear the events.
Im' working on the Malhar Angular Dashboard, based on this github project https://github.com/DataTorrent/malhar-angular-dashboard.
As per the documentation in the link post just above, under the 'dataModelType' heading 1/2 way down:
`The best way to provide data to a widget is to specify a dataModelType in the Widget Definition Object (above). This function is used as a constructor whenever a new widget is instantiated on the page.`
And when setting up the Widget Definition Objects, there are various options to choose from :
templateUrl - URL of template to use for widget content
template - String template (ignored if templateUrl is present)
directive - HTML-injectable directive name (eg. "ng-show")
So when I add my own widget definition column chart, I attempt to use the 'template' option; however it does NOT inject the {{value}} scope variable I'm setting.
Using the original datamodel sample widget def, it works fine using the 'directive' option. If I mimic this method on my column chart definition then it works ! But it doesn't work using the template option.
Here's the 'widgetDefinitions' factory code :
(function () {
'use strict';
angular.module('rage')
.factory('widgetDefinitions', ['RandomDataModel','GadgetDataModel', widgetDefinitions])
function widgetDefinitions(RandomDataModel, GadgetDataModel) {
return [
{
name: 'datamodel',
directive: 'wt-scope-watch',
dataAttrName: 'value',
dataModelType: RandomDataModel // GOTTA FIGURE THIS OUT !! -BM:
},
{
name: 'column chart',
title: 'Column Chart',
template: '<div>Chart Gadget Here {{value}}</div>',
dataAttrName: 'value',
size: {width: '40%',height: '200px'},
dataModelType: ColumnChartDataModel
},
];
}
})();
and here are the factories:
'use strict';
angular.module('rage')
.factory('TreeGridDataModel', function (WidgetDataModel, gadgetInitService) {
function TreeGridDataModel() {
}
TreeGridDataModel.prototype = Object.create(WidgetDataModel.prototype);
TreeGridDataModel.prototype.constructor = WidgetDataModel;
angular.extend(TreeGridDataModel.prototype, {
init: function () {
var dataModelOptions = this.dataModelOptions;
this.limit = (dataModelOptions && dataModelOptions.limit) ? dataModelOptions.limit : 100;
this.treeGridActive = true;
//this.treeGridOptions = {};
this.updateScope('THIS IS A TreeGridDataModel...'); // see WidgetDataModel factory
},
updateLimit: function (limit) {
this.dataModelOptions = this.dataModelOptions ? this.dataModelOptions : {};
this.dataModelOptions.limit = limit;
this.limit = limit;
},
destroy: function () {
WidgetDataModel.prototype.destroy.call(this);
}
});
return TreeGridDataModel;
});
'use strict';
angular.module('rage')
.factory('ColumnChartDataModel', function (WidgetDataModel) {
function ColumnChartDataModel() {
}
ColumnChartDataModel.prototype = Object.create(WidgetDataModel.prototype);
ColumnChartDataModel.prototype.constructor = WidgetDataModel;
angular.extend(ColumnChartDataModel.prototype, {
init: function () {
var dataModelOptions = this.dataModelOptions;
this.limit = (dataModelOptions && dataModelOptions.limit) ? dataModelOptions.limit : 100;
this.treeGridActive = true;
var value = 'THIS IS A ColChartDataModel...';
//$scope.value = value;
this.updateScope(value); // see WidgetDataModel factory
},
updateLimit: function (limit) {
this.dataModelOptions = this.dataModelOptions ? this.dataModelOptions : {};
this.dataModelOptions.limit = limit;
this.limit = limit;
},
destroy: function () {
WidgetDataModel.prototype.destroy.call(this);
}
});
return ColumnChartDataModel;
});
and finally the directives:
'use strict';
angular.module('rage')
.directive('wtTime', function ($interval) {
return {
restrict: 'A',
scope: true,
replace: true,
template: '<div>Time<div class="alert alert-success">{{time}}</div></div>',
link: function (scope) {
function update() {
scope.time = new Date().toLocaleTimeString();
}
update();
var promise = $interval(update, 500);
scope.$on('$destroy', function () {
$interval.cancel(promise);
});
}
};
})
.directive('wtScopeWatch', function () {
return {
restrict: 'A',
replace: true,
template: '<div>Value<div class="alert alert-info">{{value}}</div></div>',
scope: {
value: '=value'
}
};
})
.directive('wtFluid', function () {
return {
restrict: 'A',
replace: true,
templateUrl: 'app/views/template2/fluid.html',
scope: true,
controller: function ($scope) {
$scope.$on('widgetResized', function (event, size) {
$scope.width = size.width || $scope.width;
$scope.height = size.height || $scope.height;
});
}
};
});
I'd like to know why ONLY the directive option will update the wigdet's data and not the template option.
thank you,
Bob
I believe I see the problem. The dataAttrName setting and updateScope method are actually doing something other than what you're expecting.
Look at the makeTemplateString function here. This is what ultimately builds your widget's template. You should notice that if you supply a template, the dataAttrName does not even get used.
Next, take a look at what updateScope does, and keep in mind that you can override this function in your own data model to do what you really want, a la:
angular.extend(TreeGridDataModel.prototype, {
init: function() {...},
destroy: function() {...},
updateScope: function(data) {
// I don't see this "main" object defined anywhere, I'm just going
// off your treegrid.html template, which has jqx-settings="main.treeGridOptions"
this.widgetScope.main = { treeGridOptions: data };
// Doing it without main, you could just do:
// this.widgetScope.treeGridOptions = data;
// And then update your treegrid.html file to be:
// <div id="treeGrid" jqx-tree-grid jqx-settings="treeGridOptions"></div>
}
});
I want to be able to load the directive's template from a promise. e.g.
template: templateRepo.get('myTemplate')
templateRepo.get returns a promise, that when resolved has the content of the template in a string.
Any ideas?
You could load your html inside your directive apply it to your element and compile.
.directive('myDirective', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
//Some arbitrary promise.
fetchHtml()
.then(function(result){
element.html(result);
$compile(element.contents())(scope);
}, function(error){
});
}
}
});
This is really interesting question with several answers of different complexity. As others have already suggested, you can put loading image inside directive and when template is loaded it'll be replaced.
Seeing as you want more generic loading indicator solution that should be suitable for other things, I propose to:
Create generic service to control indicator with.
Manually load template inside link function, show indicator on request send and hide on response.
Here's very simplified example you can start with:
<button ng-click="more()">more</button>
<div test="item" ng-repeat="item in items"></div>
.throbber {
position: absolute;
top: calc(50% - 16px);
left: calc(50% - 16px);
}
angular
.module("app", [])
.run(function ($rootScope) {
$rootScope.items = ["One", "Two"];
$rootScope.more = function () {
$rootScope.items.push(Math.random());
};
})
.factory("throbber", function () {
var visible = false;
var throbber = document.createElement("img");
throbber.src = "http://upload.wikimedia.org/wikipedia/en/2/29/Throbber-Loadinfo-292929-ffffff.gif";
throbber.classList.add("throbber");
function show () {
document.body.appendChild(throbber);
}
function hide () {
document.body.removeChild(throbber);
}
return {
show: show,
hide: hide
};
})
.directive("test", function ($templateCache, $timeout, $compile, $q, throbber) {
var template = "<div>{{text}}</div>";
var templateUrl = "templateUrl";
return {
link: function (scope, el, attr) {
var tmpl = $templateCache.get(templateUrl);
if (!tmpl) {
throbber.show();
tmpl = $timeout(function () {
return template;
}, 1000);
}
$q.when(tmpl).then(function (value) {
$templateCache.put(templateUrl, value);
el.html(value);
$compile(el.contents())(scope);
throbber.hide();
});
},
scope: {
text: "=test"
}
};
});
JSBin example.
In live code you'll have to replace $timeout with $http.get(templateUrl), I've used the former to illustrate async loading.
How template loading works in my example:
Check if there's our template in $templateCache.
If no, fetch it from URL and show indicator.
Manually put template inside element and [$compile][2] it.
Hide indicator.
If you wonder what $templateCache is, read the docs. AngularJS uses it with templateUrl by default, so I did the same.
Template loading can probably be moved to decorator, but I lack relevant experience here. This would separate concerns even further, since directives don't need to know about indicator, and get rid of boilerplate code.
I've also added ng-repeat and run stuff to demonstrate that template doesn't trigger indicator if it was already loaded.
What I would do is to add an ng-include in my directive to selectively load what I need
Check this demo from angular page. It may help:
http://docs.angularjs.org/api/ng.directive:ngInclude
````
/**
* async load template
* eg :
* <div class="ui-header">
* {{data.name}}
* <ng-transclude></ng-transclude>
* </div>
*/
Spa.Service.factory("RequireTpl", [
'$q',
'$templateCache',
'DataRequest',
'TplConfig',
function(
$q,
$templateCache,
DataRequest,
TplConfig
) {
function getTemplate(tplName) {
var name = TplConfig[tplName];
var tpl = "";
if(!name) {
return $q.reject(tpl);
} else {
tpl = $templateCache.get(name) || "";
}
if(!!tpl) {
return $q.resolve(tpl);
}
//加载还未获得的模板
return new $q(function(resolve, reject) {
DataRequest.get({
url : "/template/",
action : "components",
responseType : "text",
components : name
}).success(function(tpl) {
$templateCache.put(name, tpl);
resolve(tpl);
}).error(function() {
reject(null);
});
});
}
return getTemplate;
}]);
/**
* usage:
* <component template="table" data="info">
* <span>{{info.name}}{{name}}</span>
* </component>
*/
Spa.Directive.directive("component", [
"$compile",
"RequireTpl",
function(
$compile,
RequireTpl
) {
var directive = {
restrict : 'E',
scope : {
data : '='
},
transclude : true,
link: function ($scope, element, attrs, $controller, $transclude) {
var linkFn = $compile(element.contents());
element.empty();
var tpl = attrs.template || "";
RequireTpl(tpl)
.then(function(rs) {
var tplElem = angular.element(rs);
element.replaceWith(tplElem);
$transclude(function(clone, transcludedScope) {
if(clone.length) {
tplElem.find("ng-transclude").replaceWith(clone);
linkFn($scope);
} else {
transcludedScope.$destroy()
}
$compile(tplElem.contents())($scope);
}, null, "");
})
.catch(function() {
element.remove();
console.log("%c component tpl isn't exist : " + tpl, "color:red")
});
}
};
return directive;
}]);
````
I am attempting to write a directive with it's own controller.
myApp.directive('imageUploadifive', function (createGal)
{
return {
restrict: 'A',
controller: function($scope)
{
//create gallery
$scope.created = createGal.createGal();
$scope.created.then(function(createGal){
$scope.gallery.id = createGal.created_id;
console.log($scope.gallery.id);//returning after the link function
});
$scope.gallery.galleryName = "New Image Gallery";
},
link: function($scope, element, attrs)
{
var id = $scope.gallery.id;
console.log(id);
$(element).uploadifive({
'uploadScript' : '/beta/images/upload',
'buttonClass' : 'uploadifive-button btn btn-primary',
'queueID' : 'imageGallery_queue',
'buttonText' : 'Select Files',
'fileSizeLimit' : 500,
'formData' : {
'galleryID' : id
},
'onError': function(errorType)
{
alert('There was a problem');
},
'onUpload': function()
{
}
});
}
};
});
This directive is called with a modal and the //create gallery is generating an id for the uploader js. What I don't understand is the link: function is running and returning undefined before the controller. Any guidance on this would be appreciated.
Thanks
It's not the linking function is called before the controller, it's because the $scope.created.then() is asynchronous, the callback function to set the id is called after the linking function.
To fix it, you need to call $scope.created.then() in the linking function instead:
link: function($scope, element, attrs) {
$scope.created.then(function(createGal) {
$scope.gallery.id = createGal.created_id;
$(element).uploadifive({
'uploadScript' : '/beta/images/upload',
'buttonClass' : 'uploadifive-button btn btn-primary',
'queueID' : 'imageGallery_queue',
'buttonText' : 'Select Files',
'fileSizeLimit' : 500,
'formData' : {
'galleryID' : $scope.gallery.id
},
'onError': function(errorType)
{
alert('There was a problem');
},
'onUpload': function()
{
}
});
});
}
I can't figure this one on my own. Maybe I'm missing something. I have a controller and a directive which creates its own scope.
Plunker link : http://run.plnkr.co/plunks/wFV7d2blZKEXUgHIOxYo/
Here is the controller code which just creates 3 variables and exposes a "change" function for each one :
var myApp = angular.module("myApp",[]);
myApp.controller('MainController', function ( $scope, $rootScope ) {
$rootScope.showStuff = true;
$scope.showStuff2 = true;
$scope.showStuffObj = { stuff : true };
$scope.changeStuff = function () {
$rootScope.showStuff = false;
}
$scope.changeStuff2 = function () {
$scope.showStuff2 = false;
}
$scope.changeStuff3 = function () {
$scope.showStuffObj.stuff = false;
}
});
Coming up next, the directive:
myApp.directive("mydirective", function () {
return {
scope : {
showStuff : "=",
showStuff2 : "=",
showStuffObj : '='
},
restrict : "EA",
template : "<h2>Running</h2>",
link : function ( $scope ) {
console.log("running directive", $scope);
$scope.$watch("showStuff", function () {
console.log($scope.showStuff);
});
$scope.$watch("showStuff2", function () {
console.log($scope.showStuff2);
});
$scope.$watch("showStuffObj", function () {
console.log($scope.showStuffObj);
});
}
};
});
Why do I get this ?
If the two way binding works, I should see the real values of the variables, not undefined. Why doesn't the watch update when I update the variables? This is very confusing.
There are two problems:
As beniwal said, the attributes in the directives must be separated with dashes, while the local scope variables are in camelCase:
<mydirective show-stuff="showStuff" show-stuff2="showStuff2" show-stuff-obj="showStuffObj">
.
scope : {
showStuff : "=",
showStuff2 : "=",
showStuffObj : "="
},
For watching to work In case of showStuffObj, you must a) deep-watch the object, or directly watch a property. Deep-watching is expensive, so only do it if it is really necessary:
Watching single property:
$scope.$watch("showStuffObj.stuff", function () {
console.log($scope.showStuffObj.stuff);
});
Deep watch:
$scope.$watch("localShowStuffObj", function () {
console.log($scope.localShowStuffObj);
}, true);
The third parameter of watch set to true turns on deep watching.
plnkr: http://plnkr.co/edit/sxfIK16kTffxr4Z1R80s?p=preview