Watching for model change - javascript

I have the following code in my directive's link function:
link: function (scope, elem, attrs, ngModel) {
$(elem).datagrid({
columns: [[
{ field: 'ck', checkbox: 'true' },
{ field: 'ProjectID', title: 'Project ID', width: '30%' },
{ field: 'Name', title: 'Name' }
]]
});
ngModel.$render = function (value) {
$(elem).datagrid('loadData', ngModel.$viewValue);
};
scope.$watch('projectList', function (newValue, oldValue) {
$(elem).datagrid('loadData', ngModel.$viewValue);
});
}
When Array $scope.projectList is initially assigned with data both listeners are fired. Somewhere in my controller (just for testing) I am adding another element to $scope.projectList:
$scope.test = function () {
var project = $scope.projectList[0];
$scope.projectList.push(project);
}
At this point none of listeners are fired.Can someone please explain why that is happening?
Thanks

$watch is only checking if the reference to the projectList array has changed, it does not perform a deep watch on the collection. When you assign the array to the scope variable, you change this reference, but subsequently modifying this array leaves the reference intact. In your case, using the $watchCollection() method seems more suitable.
it's worth noting, though, that $watchCollection only checks if the collection element references have changed, e.g. by adding/removing/replacing an item. It does not check if those elements themselves have been modified.
If you want to have a deep watch on your collection, pass a true as the third parameter to $watch().
scope.$watch('projectList', function (newValue, oldValue) {
$(elem).datagrid('loadData', ngModel.$viewValue);
}, true); // <--- note the objectEquality flag set to true
Note, however, that this might have performance implications if the items in the collection are complex and require more time to compare them.
You can also check Angular docs for $scope for more information (scroll down a bit for $watch() and $watchCollection() method descriptions).

This is because the normal $watch function just looks at reference equality, so if you did something like this:
var project = $scope.projectList[0];
$scope.newProjectList = [];
$scope.newProjectList.push(project);
$scope.projectList = $scope.newProjectList;
Then it would trigger your watch because the object reference of $scope.projectList changed.
If you wanted your example:
var project = $scope.projectList[0];
$scope.projectList.push(project);
to trigger the watch, then you would either have to do
scope.$watch('projectList', function (newValue, oldValue) {
$(elem).datagrid('loadData', ngModel.$viewValue);
}, true);
(Passing true as the last argument to $watch causes $watch to do a deep equality comparison, which can be slow with big objects or large lists)
OR
scope.$watchCollection('projectList', function (newValue, oldValue) {
$(elem).datagrid('loadData', ngModel.$viewValue);
});
(This is similar to the regular $watch in terms of reference equality, but it was made especially for lists. So, on top of the main reference check, it also does a reference check of each of the items in the collection or array, so it would trigger from things like .push and .pop)
They all have their advantages, depending on what kind of checks you're looking for. Also, remember that the $watch returns a deregister function that you can use to clear it out, which you would usually do inside scope.$on('$destroy'. If you don't, they just stay around for a while and can be a drain if you have a lot.
Here's a good
article on all the differences between the 3 flavors of watch

Related

Deep watch in not working on object Vue

I have a watcher setup on an array and I have deep watch enabled on it, however the handler function does not trigger when the array changes, applications is defined in the object returned in data. Here's the code:
watch: {
applications: {
handler: function(val, oldVal) {
console.log('app changed');
},
deep: true,
},
page(newPage) {
console.log('Newpage', newPage);
},
},
Vue cannot detect some changes to an array such as when you directly set an item within the index:
e.g. arr[indexOfItem] = newValue
Here are some alternative ways to detect changes in an array:
Vue.set(arr, indexOfItem, newValue)
or
arr.splice(indexOfItem, 1, newValue)
You can find better understanding of Array Change Detection here
If you reset your array with arr[ index ] = 'some value', Vue doesn't track to this variable. It would better to use Vue array’s mutation method. These methods used to track array change detection by Vue.
It is worked for me.

Is it possible to detect changes to ng-model in a directive without a deep $watch

I'm writing a directive with custom validation logic to validate an object.
HTML:
<input type="hidden" name="obj" ng-model="vm.obj" validate-object />
JS:
angular
.module('myApp')
.directive('validateObject', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$validators.validateObject = myValidator;
function myValidator (modelValue, viewValue) {
return validateObject(modelValue);
}
function validateObject (obj) {
// Look inside the object
}
}
}
});
The problem is that the validator doesn't run when a property inside the object is changed.
I could add a $watch with objectEquality === true, and then manually $setCustomValidity with my validation logic. Something like this:
link: function (scope, element, attrs, ngModelCtrl) {
scope.$watch(attrs.ngModel, onModelChange, true);
function onModelChange (newValue) {
ngModelCtrl.$setCustomValidity('validateObject', validateObject(newValue))
}
function validateObject (obj) {
// Look inside the object
}
}
But I don't like using the old school way of manually using $setValidity, plus adding a manual $watch while NgModelController already has ways of registering inside the update process (like $formatters), and in addition the $watch being a deep one which can has performance issues.
Am I getting this wrong? Is there a better way?
From https://github.com/angular/angular.js/blob/master/src/ng/directive/ngModel.js#L699 :
if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
return;
}
ngModel performs a flat equality check against the older version of the model, so any changes inside an object would not be reflected on ngModel or ngChange.
The perferred approach would be to use immutable data, that means that every time you change the model (the object), create a new copy instead:
function changeModel(){
this.vm.name = "roy";
// Create a new object for ngModel;
this.vm = angular.copy(this.vm);
}
EDIT
I remember that I solved a previous issue before. You want to have a set of ng-models binded to properties on an object, and have 1 change listener for the entire object.
Here's my solution: http://plnkr.co/edit/6tPMrB8n1agINMo252F2?p=preview
What I did was to create a new directive "formModel" that must be placed on a form element. Angular has a form directive which has a controller.
NgModelController requires a parent form controller, which then it adds itself to the form (this is how you get validity on an entire form).
So in my directive, I decorated the form's $addControl method, and added a listener for every ngModelController that adds itself via $viewChangeListeners, and now on every change of ngModel inside the form, the formModel directive will duplicate the entire object and fire $setViewValue.

AngularJs Directive: How to dynamically set attributes on ng-repeat element

I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?
First, I thought of this solution:
template
...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...
*directive
link: function(scope, elm, attrs) {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
...
Result
It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.
temporary fix
in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;
And then in the template,
a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')
While it works fine, it is obviously not DRY at all. HELP!
Instead of removing attributes, change your click handler.
Add $event to the list of arguments and conditionally use preventDefault().
<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>
In your controller:
$scope.addToPlaylist = function(event,playlist) {
if (!$scope.adding) return;
//otherwise
event.preventDefault();
//do add operation
};
When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.
From the Docs:
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:
link: function(scope, elm, attrs) {
elm.ready(function() {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
});
}
However, this is unlikely to work in your situation, as #charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.
I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.
Update:
I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.
The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:
app.component('playlistComponent', {
// We can define out template as a function that returns a string:
template: function($element, $attrs) {
var action = 'add' in $attrs
? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
: 'ng-href="playlist/{{playlist._id}}"';
return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
action + '></a>';
},
// Components always use controllers rather than scopes
controller: ['playlistService', function(playlists) {
this.playlists = playlists;
this.addToPlaylist = function(playlist, track) {
// Some logic
};
}]
});

Three state checkbox with angular directive

I'm new to angularjs and I'm having a very difficult time attempting to create a directive to implement a three-state checkbox. I'd like to create a two-way binding between the isolated scope variable in my directive and my controller scope variable. Obviously somehow I've cocked it up.
My goal is to be able to update the state of the checkbox in the dom whenever the state variable gets updated in the controller, and to update the state variable in the controller whenever the user clicks on the checkbox.
After snooping around on the google machine i decided I needed to set up a $watch on my directive attribute (my knowledge of which is tenuous as best). I haven't been able to get that to work. It never gets called when my variable is changed from the controller. Below is a snippet of my markup and the meat of my directive. See my fiddle below for the details.
<input checkbox-three-state='item.state' type='checkbox' />
directive('checkboxThreeState', ['$compile', function($compile) {
return {
restrict: 'A',
scope: {
state: '=checkboxThreeState'
},
link: function (scope, element, attributes) {
element.on('click', function () {
// update the controller scope
});
scope.$watch(attributes.checkboxThreeState, function (newVal, oldVal) {
// update the dom
});
}
}
}]);
I think I have my other objective (update controller scope on click) down.
Here's my fiddle.
I realize now that my question was poorly framed, it really should have been asking why the watch I set up in my directive wasn't working. Now i know that I was telling it to watch the wrong scope property, one that didn't actually exist. Mea culpa. Previously I was telling it to watch my directive attribute -- I had just copied somebody else's example, because I'm a monkey.
scope: {
state: '=checkboxThreeState'
},
link: function (scope, element, attributes) {
// other stuff
scope.$watch(attributes.checkboxThreeState, function (newVal, oldVal) {
// update the dom
});
}
After doing a lot more snooping -- I don't know why this isn't spelled out more ubiquitously for idiots like myself -- I recognized that I had to indicate which property on my scope to watch, namely 'state'.
scope.$watch('state', function (newVal, oldVal) {
// do my jam
});
Now that i've figured that out it seems entirely obvious, but I can be pretty dense.
Here's my now working fiddle.
Updated Question
Why doesn't the watch function in my directive get fired when the controller changes the value of the variable that gets passed into my directive?
Answer Summary:
The watch function should watch a variable that actually exists on your directive scope variable. This is never spelled out in the documentation and although it may be obvious to most, I didn't put that together immediately. I was under the impression -- and to be fair to myself there are a lot of really bad examples out there that severely misled me -- that I should be watching my directive attribute, whose value comes from the controller scope.

ui-select2 inside directive isn't updating controller model

I have a directive that takes in a collection and builds out a dropdown.
.directive("lookupdropdown", function () {
return {
restrict: 'E',
scope: {
collectionset: '=',
collectionchoice: '='
},
replace: true,
template: '<select class="input-large" ui-select2 ng-model="collectionchoice" data-placeholder="">' +
' <option ng-repeat="collection in repeatedCollection" value="{{collection.id}}">{{collection.description}}</option>' +
'</select>',
controller: ["$scope", function ($scope) {
$scope.repeatedCollection = new Array(); //declare our ng-repeat for the template
$scope.$watch('collectionset', function () {
if ($scope.collectionset.length > 0) {
angular.forEach($scope.collectionset, function (value, key) { //need to 'copy' these objects to our repeated collection array so we can template it out
$scope.repeatedCollection.push({ id: value[Object.keys(value)[0]], description: value[Object.keys(value)[1]] });
});
}
});
$scope.$watch('collectionchoice', function (newValue, oldValue) {
debugger;
$scope.collectionchoice;
});
} ]
}
});
This works fine. It builds out the drop down no problem. When I change the dropdown value, the second watch function gets called and I can see that it sets the value of collection choice to what I want. However, the collectionchoice that I have put into the directive doesn't bind to the new choice.
<lookupDropdown collectionset="SecurityLevels" collectionchoice="AddedSecurityLevel"></lookupDropdown>
That is the HTML markup.
This is the javascript:
$scope.SecurityLevels = new Array();
$scope.GetSecurityLevelData = function () {
genericResource.setupResource('/SecurityLevel/:action/:id', { action: "#action", id: "#id" });
genericResource.getResourecsList({ action: "GetAllSecurityLevels" }).then(function (data) {
$scope.AddedSecurityLevel = data[0].SCRTY_LVL_CD;
$scope.SecurityLevels = data;
//have to get security levels first, then we can manipulate the rest of the page
genericResource.setupResource('/UserRole/:action/:id', { action: "#action", id: "#id" });
$scope.GetUserRoles(1, "");
});
}
$scope.GetSecurityLevelData();
Then when I go to post my new user role, I set the user role field like this:
NewUserRole.SCRTY_LVL_CD = $scope.AddedSecurityLevel;
but this remains to be the first item EVEN though I have updated the dropdown, which according the watch function, it has changed to the correct value. What am I missing here?
You faced this issue because of the prototypical nature inheritance in Javascript. Let me try and explain. Everything is an object in Javascript and once you create an object, it inherits all the Object.Prototype(s), which eventually leads to the ultimate object i.e. Object. That is why we are able to .toString() every object in javascript (even functions) because they are all inherited from Object.
This particular issue on directives arises due to the misunderstanding of the $scope in Angular JS. $scope is not the model but it is a container of the models. See below for the correct and incorrect way of defining models on the $scope:
...
$scope.Username = "khan#gmail.com"; //Incorrect approach
$scope.Password = "thisisapassword";//Incorrect approach
...
$scope.Credentials = {
Username: "khan#gmail.com", //Correct approach
Password: "thisisapassword" //Correct approach
}
...
The two declarations make a lot of difference. When your directive updated its scope (isolated scope of directive), it actually over-rid the reference completely with the new value rather then updating the actual reference to the parent scope hence it disconnected the scope of the directive and the controller.
Your approach is as follows:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>
The problem with this approach is that although it works, but it not the recommended solution and here is why. What if your directive is placed inside another directive with another isolated scope between scope of your directive and the actual controller, in that case you would have to do $parent.$parent.AddedSecurityLevel and this could go on forever. Hence NOT a recommended solution.
Conclusion:
Always make sure there is a object which defines the model on the scope and whenever you make use of isolate scopes or use ng directives which make use of isolate scopes i.e. ng-model just see if there is a dot(.) somewhere, if it is missing, you are probably doing things wrong.
The issue here was that my directive was being transcluded into another directive. Making the scope im passing in a child of the directive it was in. So something like $parent -> $child -> $child. This of course was making changes to the third layer and second layer. But the first layer had no idea what was going on. This fixed it:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>

Categories

Resources