AngularJS - Calling function in directive when a 2-way bound value changes - angularjs

I've got a directive all setup with 2-way data binding on the attributes using = and I can see everything is working well with that. Now I'm stuck at the need to call a function within the directive whenever one of my bound attributes changes in the parent scope, and I can't figure out how to pull that off.
I'm basically creating a version of the ui checkbox button that works with arrays of objects. You pass the directive an allowed array (which contains all the different options) and an applied array (which contains the same objects from allowed). For checking if an object is in the allowed array I have another array that is the just the id properties. Within the directive this is working great, but if the applied array changes outside of the directive the id array never gets updated.
The Directive:
angular.module('MyApp',[])
.directive('btnCheckboxGroup', function(){
return {
restrict: 'E',
// controller: DirCtrl,
templateUrl: 'btnCheckboxGroup.html',
scope: {
allowed: '=',
applied: '=',
id: '=',
title: '='
},
link: function(scope, elem, attrs){
scope.abp = [];
// this works right away, but how do I run it when the parent scope updates it?
angular.forEach(scope.applied, function(obj){
scope.abp.push( obj[scope.id] );
});
scope.addRemove = function(a){
var index = scope.abp.indexOf(a[scope.id]);
// doesn't exist, add it
if(index === -1){
scope.abp.push(a[scope.id]);
scope.applied.push(a);
// does exist, remove it
} else {
scope.abp.splice(index, 1);
for(var i in scope.applied){
if(scope.applied[i][scope.id]==a[scope.id]){
scope.applied.splice(i,1);
break;
}
}
}
}// end addRemove()
}
};
});
JSFiddle
I've tried lots of variations of things like scope.$watch, attrs.$observe, and attempted at one point to try one-way data-binding with # and that made lots of things crash.
So whats the magic I'm missing here?

You can pass a third parameter to $watch to change the way it compares the old and the new value. See the Angular docs
$watch(watchExpression, [listener], [objectEquality]);
If you set this to true, it will pick up changes in the content of the array and not only when the array reference changes. This does have a performance impact (depending on the length of the array). Checking only the length of the array does not cover the case where the number of elements stay the same but the elements themselves do change.
In your case you would need to do something like this:
scope.$watch(
"applied",
function() {
scope.abp = [];
angular.forEach(scope.applied, function(obj){
scope.abp.push( obj[scope.id] );
});
},
true);

Is this what you're looking for?
scope.$watch(function() {
return scope.applied.length;
}, function(val) {
console.log(val);
console.log(scope.applied);
});
The array on scope doesn't change but its length does, so if you were using the string-variant of $watch it won't fire, but using a function and looking at the length of the array will. More on this in the docs

Related

ngModel and How it is Used

I am just getting started with angular and ran into the directive below. I read a few tutorials already and am reading some now, but I really don't understand what "require: ngModel" does, mainly because I have no idea what ngModel does overall. Now, if I am not insane, it's the same directive that provides two way binding (the whole $scope.blah = "blah blah" inside ctrl, and then {{blah}} to show 'blah blah' inside an html element controlled by directive.
That doesn't help me here. Furthermore, I don't understand what "model: '#ngModel' does. #ngModel implies a variable on the parents scope, but ngModel isn't a variable there.
tl;dr:
What does "require: ngModel" do?
What does "model : '#ngModel'" do?
*auth is a service that passes profile's dateFormat property (irrelevant to q)
Thanks in advance for any help.
angular.module('app').directive('directiveDate', function($filter, auth) {
return {
require: 'ngModel',
scope: {
model : '#ngModel',
search: '=?search'
},
restrict: 'E',
replace: true,
template: '<span>{{ search }}</span>',
link: function($scope) {
$scope.set = function () {
$scope.text = $filter('date')($scope.model, auth.profile.dateFormat );
$scope.search = $scope.text;
};
$scope.$watch( function(){ return $scope.model; }, function () {
$scope.set();
}, true );
//update if locale changes
$scope.$on('$localeChangeSuccess', function () {
$scope.set();
});
}
};
});
ngModel is an Angular directive responsible for data-binding. Through its controller, ngModelController, it's possible to create directives that render and/or update the model.
Take a look at the following code. It's a very simple numeric up and down control. Its job is to render the model and update it when the user clicks on the + and - buttons.
app.directive('numberInput', function() {
return {
require: 'ngModel',
restrict: 'E',
template: '<span></span><button>+</button><button>-</button>',
link: function(scope, element, attrs, ngModelCtrl) {
var span = element.find('span'),
plusButton = element.find('button').eq(0),
minusButton = element.find('button').eq(1);
ngModelCtrl.$render = function(value) {
updateValue();
};
plusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue + 1);
updateValue();
});
minusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue - 1);
updateValue();
});
function updateValue(value) {
span.html(ngModelCtrl.$modelValue);
}
}
};
});
Working Plunker
Since it interacts with the model, we can use ngModelController. To do that, we use the require option to tell Angular we want it to inject that controller into the link function as its fourth argument. Now, ngModelController has a vast API and I won't get into much detail here. All we need for this example are two methods, $render and $setViewValue, and one property, $modelValue.
$render and $setViewValue are two ways of the same road. $render is called by Angular every time the model changes elsewhere so the directive can (re)render it, and $setViewValue should be called by the directive every time the user does something that should change the model's value. And $modelValue is the current value of the model. The rest of the code is pretty much self-explanatory.
Finally, ngModelController has an arguably shortcoming: it doesn't work well with "reference" types (arrays, objects, etc). So if you have a directive that binds to, say, an array, and that array later changes (for instance, an item is added), Angular won't call $render and the directive won't know it should update the model representation. The same is true if your directive adds/removes an item to/from the array and call $setViewValue: Angular won't update the model because it'll think nothing has changed (although the array's content has changed, its reference remains the same).
This should get you started. I suggest that you read the ngModelController documentation and the official guide on directives so you can understand better how this all works.
P.S: The directive you have posted above isn't using ngModelController at all, so the require: 'ngModel' line is useless. It's simply accessing the ng-model attribute to get its value.

How do I retrieve the unevaluated value of an attribute from a directive in Angular?

I have a directive which is using $observe to watch when the value of one of the attributes changes. When this fires, I need to be able to retrieve the unevaluated value of the attribute not the evaluated value.
So my HTML would look like this:
<div my-attrib="{{scopeVar}}"></div>
Then the link function in my directive:
attrib.$observe('myAttrib', function(val) {
// Both val and attrib.myAttrib contain "ABC"
// I would like the uncompiled value instead
var evaluatedValue = attrib.myAttrib;
});
If the controller had done this:
$scope.myAttrib = "ABC";
When $observe first, evalutedValue returns "ABC". I actually need it to return "{{scopeVar}}".
EDIT: Per the comment below from François Wahl I ended up moving this into a ng-repeat element which is bound to an array of one item. Then I just remove/add the new item in the controller which updates $scope. This eliminates the need to retrieve the uncompiled attribute value and actually cleans things up quite a bit. It's definitely odd when looking at the view since it's not immediately clear as to why it's in a repeater, but it's worth it since it cleans up the code quite a bit.
You have to grab it in the compile: function of the directive link this:
.directive('myAttrib', function() {
return {
compile: function(tElement, tAttrs) {
var unevaluatedValue = tAttrs.myAttrib;
return function postLink(scope, element, attrs) {
attrs.$observe('myAttrib', function(val) {
// Both val and attrib.myAttrib contain "ABC"
// I would like the uncompiled value instead
var evaluatedValue = attrs.myAttrib;
console.log(unevaluatedValue);
console.log(evaluatedValue);
});
}
}
}
})
Example Plunker: http://plnkr.co/edit/bHDmxJvnENqz8Mpg1qpd?p=preview
Hope this helps.

changing values of my directive in angular

I have this directive
MyApp.directive('dynatree', function ($rootScope) {
return {
restrict: 'E',
link: function postlink(scope, element, attrs) {
$(element).dynatree({
onActivate: function (node) {
scope.$emit('nodeActivated', node);
},
persist: false,
children: scope.treeData //this variable is in the layout for either csb or sku
});
}
};
});
My controller contains the values for children:
scope.treeData = [ ... ]
Every time I update scope.treeData, my directive does not update. Does anyone have any idea on how to update my directive from my controller?Thank you
I've never worked with dynatree but it seems that the tree is not being redrawn when the src array changes.
Try watching for changes on scope.tree.data and reload the dynatree when it occurs:
MyApp.directive('dynatree', function ($rootScope) {
return {
restrict: 'E',
link: function postlink(scope, element, attrs) {
$(element).dynatree({
onActivate: function (node) {
scope.$emit('nodeActivated', node);
},
persist: true,
children: scope.tree.data //this variable is in the layout for either csb or sku
});
scope.$watch('tree.data', function() {
// try to update the tree data here (I'm not aware how it's done)
// and after that, reload...
$(element).dynatree("getTree").reload();
});
}
};
});
If there's no way to update the tree data, try destroying the dynatree and recreating it (inside the watcher function):
// destroy tree
$(element).dynatree("destroy");
// recreate tree with new data
$(element).dynatree({ ... children: scope.tree.data ... });
Note that I've used tree.data and not treeData, that's because you should not store this kind of information directly on the scope, instead you should store it inside a model (i.e. scope.tree = { data: [ ... ] }).
Using scope.$watch is the way to go - as the guys mentioned before. When watching objects I found necessary to use 'true' as additional parameter
$watch("tree.data", function () {..}, true)
so comparizon won't be lazy and will check deeper then just references.
http://docs.angularjs.org/api/ng.$rootScope.Scope
How would Dynatree know that you changed the treeData?
You probably need to tell it by installing a watcher on the treedata in your directive:
scope.$watch('treeData', function(treeData, old, scope){
element.dynatree().howEverDynatreeIsUpdated();
})
I don't know dynatree, but if it's supposed to update itself, note that this will only work for modifications within the tree.
Replacing the entire tree with another one on the scope (= setting the scopes treeData property to something else) will not be detected by Dynatree, because the tree you passed into Dynatree during creation is the only one Dynatree can ever know about.
It cannot, without manual interference, know that the treeData property on the scope has changed and it's own reference is pointing to an outdated one. You need to tell it somehow.
OK so the answer is
scope.$watch('treeData', function (treeData, old, scope) {
if (treeData.children) {
$(element).dynatree({children: treeData.children});
$(element).dynatree("getTree").reload();
$(element).show();
}
}, true)
It's not enough to just change the data of the tree, one has to reload it... I added the show because I'm hiding it before so that you don't see it bounce with the data changes
There are 2 ways to bind your variable inside AngularJs directive.First way is used,when you don't want to have isolated scope.You set watcher on attribute field.The second way is used,when you have isolated scope.this way is more professional.You set watcher only on your scope's variable.
if you want more,here is a good article.
http://www.w3docs.com/snippets/angularjs/bind-variable-inside-angularjs-directive-isolated-scope.html

directive for array without using ng-repeat

Once again I stumbled on slow performance of ng-repeat and can't figure how to build a directive that will render an array of elements without utilizing ng-repeat anywhere (even in the template)
So, how do you guys do that?
Even if I iterate through the array, using template for every single element:
.directive('foo', function($parse, $compile) {
return {
restrict: 'E',
scope: { items: '=myarray' },
link: function (scope, element, attrs){
var tmplt = '<div> {{name}} </div>';
scope.items.forEach(function(){
element.append(tmplt(scope));
// now I'd like to have a scope of that element,
// and pass it into the template, and use properties related
// only to that element
// but I don't know how to get a scope for a child element
});
scope.$watch(attrs.myarray, function(value) { console.log('something change'); })
}
}});
If I choose to have a single template for all the elements, then again I have no choice but to use ng-repeat in it and it will create ngRepeatWatchers and everything gets slow down again.
Though I agree with #Mark, but here is dirty solution for your question:
http://plnkr.co/edit/wxM8mSoNsRGXBJUao5Xb?p=preview
The idea is to create isolated child scopes:
scope.items.forEach(function(item){
var childScope = scope.$new(true);
angular.extend(childScope, item);
element.append($compile(tmplt)(childScope));
});
And do not forget to delete those child scopes unpon each refresh.
and sill this solution needed to be benchmarked to see is it faster and how much comparing to ngRepeat.

Angularjs + kendoui dropdownlist

I have this directive
angular.module('xxx', [
])
.directive('qnDropdown', [
'$parse',
function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
scope.$watch(attr.qnDropdown, function(source) {
var model = $parse(attr.ngModel);
elem.kendoDropDownList({
dataTextField: "Name",
dataValueField: "ID",
value: attr.value,
select: function(e) {
var item = this.dataItem(e.item.index());
scope.$apply(function() {
model.assign(scope, item.value);
});
},
//template: '<strong>${ data.Name }</strong><p>${ data.ID }</p>',
dataSource: source
});
});
}
};
}]);
Input field is
<input qn:dropdown="locations" ng:model="installation.LocationID" value="{{installation.LocationID}}" />
EVerything works fine but initial value for kendoDropDownList is not filled (value: attr.value).
I suppose I am doing something at wrong place or time but not sure what?
You probably need to use $observe:
Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined. -- docs, see section Attributes.
Here's an example where I used $observe recently. See also #asgoth's answer there, where he uses $watch, but he also created an isolate scope.
I'm still not clear on when we need to use $observe vs when we can use $watch.
Are you sure {{installation.LocationID}} has a value you expect? I was able to copy-paste your code with some tweaks for my situation and the dropdownlist is working wonderfully (thank you for doing the hard work for me!). I'm populating value on the input field and when the directive executes, attr.value has it and Kendo shows it as expected. Perhaps this was an Angular issue a couple versions ago?
I had the same problem, the attr.value was empty. The problem was related to an $http async call being made to get the data. The scope data was not yet populated when the dropdownlist was being defined in the directive.
I fixed this by watching attr.ngModel instead of attr.qnDropdown in the link function of the directive. This way the dropdownlist gets defined when the scope data is populated.

Resources