Can I prevent AngularJS watch from firing on variable initialization? [duplicate] - angularjs

I have a web page that serves as the editor for a single entity, which sits as a deep graph in the $scope.fieldcontainer property. After I get a response from my REST API (via $resource), I add a watch to 'fieldcontainer'. I am using this watch to detect if the page/entity is "dirty". Right now I'm making the save button bounce but really I want to make the save button invisible until the user dirties the model.
What I am getting is a single trigger of the watch, which I think is happening because the .fieldcontainer = ... assignment takes place immediately after I create my watch. I was thinking of just using a "dirtyCount" property to absorb the initial false alarm but that feels very hacky ... and I figured there has to be an "Angular idiomatic" way to deal with this - I'm not the only one using a watch to detect a dirty model.
Here's the code where I set my watch:
$scope.fieldcontainer = Message.get({id: $scope.entityId },
function(message,headers) {
$scope.$watch('fieldcontainer',
function() {
console.log("model is dirty.");
if ($scope.visibility.saveButton) {
$('#saveMessageButtonRow').effect("bounce", { times:5, direction: 'right' }, 300);
}
}, true);
});
I just keep thinking there's got to be a cleaner way to do this than guarding my "UI dirtying" code with an "if (dirtyCount >0)"...

The first time the listener is called, the old value and the new value will be identical. So just do this:
$scope.$watch('fieldcontainer', function(newValue, oldValue) {
if (newValue !== oldValue) {
// do whatever you were going to do
}
});
This is actually the way the Angular docs recommend handling it:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization

set a flag just before the initial load,
var initializing = true
and then when the first $watch fires, do
$scope.$watch('fieldcontainer', function() {
if (initializing) {
$timeout(function() { initializing = false; });
} else {
// do whatever you were going to do
}
});
The flag will be tear down just at the end of the current digest cycle, so next change won't be blocked.

I realize this question has been answered, however I have a suggestion:
$scope.$watch('fieldcontainer', function (new_fieldcontainer, old_fieldcontainer) {
if (typeof old_fieldcontainer === 'undefined') return;
// Other code for handling changed object here.
});
Using flags works but has a bit of a code smell to it don't you think?

During initial loading of current values old value field is undefined. So the example below helps you for excluding initial loadings.
$scope.$watch('fieldcontainer',
function(newValue, oldValue) {
if (newValue && oldValue && newValue != oldValue) {
// here what to do
}
}), true;

Just valid the state of the new val:
$scope.$watch('fieldcontainer',function(newVal) {
if(angular.isDefined(newVal)){
//Do something
}
});

Related

$watch is not trigger without a complete refresh

I have a $watch setup to monitor an array (menuItems) of values in a service (MenuFilter).
$scope.filterMenuItems = MenuFilter.menuItems;
$scope.$watch(function () {
return MenuFilter.menuItems;
}, function (newVal, oldVal) {
if ( newVal !== oldVal ) {
$scope.filterMenuItems = newVal;
}
});
At a certain moment a menu item is deleted and I expect the list to be updated by the watch but it isn't. It is only updated when I refresh the page.
Make sure to add true as the third parameter to make a deep watch or better use $watchCollection.
The issue is that $watch only uses reference comparison, i.e. it does not monitor items inside your array - only the array reference itself (oldArray === newArray).

AngularJS 1.5.x $onChanges Not Working with One-Way Binding Changes

I don't understand why $onChanges isn't kicked off when I change a bound primitive in an input. Can someone see what I've done wrong, and explain this in an uncomplicated way? I made a plunkr of a quick test application after I couldn't get it to work in my actual application either.
angular
.module('test', [])
.component('test', {
template: '<child application="vm.application"></child>',
controller: 'testCtrl as vm'
})
.controller('testCtrl', function() {
var vm = this;
vm.$onInit = function () {
vm.application = {
data: {
name: 'Test'
}
}
};
})
.component('child', {
template: '<input type="text" ng-model="vm.application.data.name">',
bindings: {
application: '<'
},
controller: 'childCtrl as vm'
})
.controller('childCtrl', function() {
var vm = this;
vm.$onChanges = function (changes) {
console.log('CHANGED: ', changes);
};
})
The $onChanges method is not called for changes on subproperties of an object. Default changes to objects generally follow this sequence within a components lifetime:
UNINITIALIZED_VALUE to undefined
undefined to {} or { someAttribute: someValue, .. }
({..} to undefined if you delete the object in a parent scope)
In order to watch subproperties you could use the $doCheck method that was added in 1.5.8. It is called on every digest cycle and it takes no parameters. With great power comes great responsibility. In that method you would put logic that detects whether a certain value has been updated or not - the new value will already be updated in the controller's scope, you just need to find a way to determine if the value changed compared to the previously known value.
You could set a previousValueOfObjectAttribute variable on the controller before you start to expect changes to this specific attribute (e.g. when subcomponent B calls an output binding function in component A, based on which the target object - which is an input binding to B - in A changes). In cases where it is not predictable when the change is about to occur, you could make a copy of the specific atributes of interest after any change observed via the $doCheck method.
In my specific use case, I did not explicitly check between an old and new value, but I used a promise (store $q.defer().promise) with the intention that any change I would 'successfully' observe in the $doCheck method would resolve that promise. My controller then looked something like the following:
dn.$doCheck = function () {
if (dn.waitForInputParam &&
dn.waitForInputParam.promise.$$state.status === 0 &&
dn.targetObject.targetAttribute !== false)
dn.waitForInputParam.resolve(dn.targetObject.targetAttribute);
}
dn.listenToInputChange = function () {
dn.waitForInputParam = $q.defer();
dn.waitForInputParam.promise.then(dn.onInputParamChanged);
}
dn.onInputParamChanged = function (value) {
// do stuff
//
// start listening again for input changes -- should be async to prevent infinite $digest loop
setTimeout(dn.listenToInputChange, 1);
}
(w.r.t. promise.$$state.status, see this post).
For all other intents and purposes, watching changes to primitive data types, you should still use $onChanges. Reference: https://docs.angularjs.org/guide/component
It's $onChanges and not $onChange.
Also, the onChange only updates when the parent value is changed, not the child. Take a look at this plunkr. Note the console.log only fires when you type in the first input.
As others said above, Angular does not watch for changes in object properties, however, you can make Angular believe that your object is changed by reference.
It is sufficient to do a shallow copy of the object in order to trigger an $onChanges event:
vm.campaign = angular.extend({}, vm.campaign);
Credits to #gkalpak
Dealing with $onChanges is tricky. Actually, thats why in version 1.5.8 they introduced the $doCheck, similar to Angular 2 ngDoCheck.
This way, you can manually listen to changes inside the object being listened, which does not occur with the $onChanges hook (called only when the reference of the object is changed). Its the same thing, but it gets called for every digest cycle allowing you to check for changes manually (but better then watches).
For more details, see this blog post.

Re-registering $watch after de-registering

I have ng-model on a date variable. On changing that variable, I want to make some validations and during those validations I might change the start variable itself, and to make sure I don't get infinite-watch trigger, I'm de-registering the watch before, and re-registering it when the validation finishes.
The re-registering doesn't work. What am I doing wrong?
var watchStartTime = $scope.$watch('timeSelection.startTime', function(newValue, oldValue) {
if (newValue === oldValue) return;
validateStartEndTime();
}, true);
function validateStartEndTime() {
// De-register watch
watchStartTime();
// Do some stuff that might change timeSelection.startTime
// Re-register watch
watchStartTime();
}
I also tried setTimeout with 0 on the re-registering, thought it might work, but it doesn't.
the timeSelection.startTime changes (I know for a fact it does) but the function in $watch doesn't get called again.
What am I missing?
As stated by #Nikos Paraskevopoulos $watch returns the deregistration fn.
I think you should do something like the following...
var register = function() {
var unregister = $scope.$watch('timeSelection.startTime', function(newVal, oldVal) {
if (newVal === oldVal) {
return;
}
unregister();
validateStartEndTime();
register();
}, true);
};
register();

Angular service watch creates $digest loop

I am trying to get a scope to update when the return value of a service function changes. From reading, it sounds like I'm supposed to use $watch, but following the angular docs creates an infinite $digest loop in my application, here the controller:
.controller('navCtrl', function(CookieHandler, $scope){
// this works, but doesn't update when CookieHandler.get() changes
// $scope.user = CookieHandler.get();
$scope.$watch(
function() {return CookieHandler.get()},
function(newValue, oldValue) {
if( newValue !== oldValue) {
$scope.user = newValue
}
}
)
//etc
From the error message there's something about using $watch on functions that return an array, but CookieHandler.get() returns an object?
I looked around some more and read that I should use $apply instead of $watch here because it's easier to test, so I tried this:
.controller('navCtrl', function(CookieHandler, $scope){
// this works, but doesn't update when CookieHandler.get() changes
// $scope.user = CookieHandler.get();
$scope.$apply(function(scope){
scope.user = CookieHandler.get();
})
})
But this throws an error about the digest cycle already running. Is there a way to fix my $watch to stop the infinite loop? CookieHandler.get just checks the value with $cookieStore
$watch performs an equal comparison between the current value and the value from the previous digest cycle. If they are different then the function is executed.
If the object returned by CookieHandler.get() is always the same reference then the watch function will never get called. Even if you update properties of that object.
Arrays are a bad idea because they are often always a new object and JavaScript will see them as never equal.
You could serialize the object/array to JSON and use that value, but this won't work if there are changes in key/value orders.
It looks like you are trying to monitor when the current user is changed. Assuming you have a unique identifier for a user, then you might want to try something like this.
$scope.$watch(
function() {
var user = CookieHandler.get();
return (typeof user === 'undefined') ? 0 : user.id;
},
function(newValue, oldValue) {
if( newValue !== oldValue) {
$scope.user = CookieHandler.get();
}
}
);
Note that I call .get() twice. I don't know how your service works. Maybe that's not possible, but the point is to watch a value that is easily comparable.

How do I ignore the initial load when watching model changes in AngularJS?

I have a web page that serves as the editor for a single entity, which sits as a deep graph in the $scope.fieldcontainer property. After I get a response from my REST API (via $resource), I add a watch to 'fieldcontainer'. I am using this watch to detect if the page/entity is "dirty". Right now I'm making the save button bounce but really I want to make the save button invisible until the user dirties the model.
What I am getting is a single trigger of the watch, which I think is happening because the .fieldcontainer = ... assignment takes place immediately after I create my watch. I was thinking of just using a "dirtyCount" property to absorb the initial false alarm but that feels very hacky ... and I figured there has to be an "Angular idiomatic" way to deal with this - I'm not the only one using a watch to detect a dirty model.
Here's the code where I set my watch:
$scope.fieldcontainer = Message.get({id: $scope.entityId },
function(message,headers) {
$scope.$watch('fieldcontainer',
function() {
console.log("model is dirty.");
if ($scope.visibility.saveButton) {
$('#saveMessageButtonRow').effect("bounce", { times:5, direction: 'right' }, 300);
}
}, true);
});
I just keep thinking there's got to be a cleaner way to do this than guarding my "UI dirtying" code with an "if (dirtyCount >0)"...
The first time the listener is called, the old value and the new value will be identical. So just do this:
$scope.$watch('fieldcontainer', function(newValue, oldValue) {
if (newValue !== oldValue) {
// do whatever you were going to do
}
});
This is actually the way the Angular docs recommend handling it:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization
set a flag just before the initial load,
var initializing = true
and then when the first $watch fires, do
$scope.$watch('fieldcontainer', function() {
if (initializing) {
$timeout(function() { initializing = false; });
} else {
// do whatever you were going to do
}
});
The flag will be tear down just at the end of the current digest cycle, so next change won't be blocked.
I realize this question has been answered, however I have a suggestion:
$scope.$watch('fieldcontainer', function (new_fieldcontainer, old_fieldcontainer) {
if (typeof old_fieldcontainer === 'undefined') return;
// Other code for handling changed object here.
});
Using flags works but has a bit of a code smell to it don't you think?
During initial loading of current values old value field is undefined. So the example below helps you for excluding initial loadings.
$scope.$watch('fieldcontainer',
function(newValue, oldValue) {
if (newValue && oldValue && newValue != oldValue) {
// here what to do
}
}), true;
Just valid the state of the new val:
$scope.$watch('fieldcontainer',function(newVal) {
if(angular.isDefined(newVal)){
//Do something
}
});

Resources