changing values of my directive in angular - angularjs

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

Related

How to watch directive attributes changes?

I have a directive:
set-bid.js
app.directive("setBid", ($rootScope, Http, Toast) => {
return {
template: require('./set-bid.html'),
scope: {
newOrder: "<"
},
link: function($scope, element, attrs) {
console.log(`$scope.newOrder:`, $scope.newOrder); // undefined
}
}
});
set-bid.html
{{newOrder}} <!-- nothing -->
parent.html
<set-bid new-order="newOrder"></set-bid>
As you can see, I send newOrder variable to the set-bid directive.
However newOrder will be filled async.
I want the set-bid directive to watch for changes in this attribute.
But I do not want to watch on each param like this:
$scope.$watch("newOrder", newOrder => console.log(newOrder));
This is tedious. I want that the whole directive will listen for changes in every
param that it receives. Automatically. How this can be done?
I know I can do something like this: <set-bid ng-if="newOrder" ...></set-bid> but I need the variable to continously be watched, not just for the first time.
Set the third argument to true:
$scope.$watch("newOrder", newOrder => console.log(newOrder), true);
That creates a "deep" watch.
For more information, see
AngularJS Developer Guide - Scope $Watch Depths
You could just pass one object instead of many params and watch this one (deeply). Then, you also don't have the problem that the watcher fires multiple times, even though you e.g. just wanted to switch all params together.
Another approach would be to use a $watchCollection() and watch multiple params together, but you would need them to be listed one by one.

HTML added in link function won't bind to scope

I need a template that changes depending on an object on the scope. Because it's an object and not a string, I can't use a template function, so I'm using the link function to add the html to the element. Of course my html needs to be explicitly $interpolated now, so here's what I end up with:
scope: {
obj: "=ngModel",
type: "<"
},
controllerAs: "display",
controller: function() {
let display = this;
// sets bunch of other functions, including booleanLabel and displayData
},
link: function(scope, element, attrs) {
let display = scope;
function template(type) {
switch (type) {
case ObjTypes.X:
return `<button ng-click="display.toggleBoolean(display.obj)">
{{display.booleanLabel(display.obj.data)}}
</button>`;
case ObjTypes.Y:
return `<button>
{{display.displayData(display.obj.data)}}
</button>`;
}
}
element.html($interpolate(template(display.type))(display));
}
This doesn't work. But here's the weird thing: when I replace display.obj with just obj, the value gets through. But the functions need to be called with display.function().
I suspect it has to do with the fact that obj is put on the scope from the outside, while the functions are put on the scope by the controller, but it's incredibly weird and confusing. And what's worse, the ng-click doesn't trigger. Whether I use display.toggle() or just toggle() doesn't matter. But display.booleanLabel() and display.displayData() work fine, as long as I pass the obj, rather than display.obj.
What's going on here? And how do I make this easier to understand and maintain?
I'm using Angular 1.5, and I use controllerAs: 'display' and let display = scope to make sure my scope uses the same name everywhere. The point is to keep it easier to understand, but that's clearly not working here.
First, you can use ng-if in template.
Second, template can be a function.
Third, if you add new html elements - usually you need to $compile it. Not sure what are you tying to do with interpolate...

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.

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

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

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