There is 1 angular app, with 1 parent controller, and a child controller.
In the child, there is 1 $watch WATCH-CHILD for OBJ-CHILD, which triggers an $emit.
In the parent, there is a listener for the $emit, we'll call it ON-LISTENER, and a $watch WATCH-PARENT for OBJ-PARENT (which uses true as the 3rd argument).
When the child's OBJ-CHILD is changed, it triggers WATCH-CHILD, which triggers the $emit.
The parent listener ON-LISTENER is fired, and changes OBJ-PARENT. It also sets some $location properties.
The $watch WATCH-PARENT for OBJ-PARENT is never fired (even though the value has changed), as well as the properties set on $location not changed in the browser URL (I know they are indeed changed inside the JavaScript, cause I print them).
In order to make sure that ON-LISTENER is called within a $digest, I tried to call $digest at the end of ON-LISTENER, and got the expected exception.
Any idea if I'm doing something wrong? I expect the changes that occur in ON-LISTENER to trigger WATCH-PARENT and browser URL change.
I will try to reproduce on jsfiddle and edit this post if successful.
The code looks like:
CHILD:
$scope.$watch('vars.model', function(newValue, oldValue) {
console.log('model changed');
$scope.$emit('highlightChange', newValue);
}, true);
PARENT:
$scope.$watch('vars.model.highlight', function(newValue, oldValue) {
console.log('highlight changed');
}, true);
$scope.$on('highlightChange', function(event, value) {
console.log('listener', $scope.vars.model.highlight.categoryId, value.categoryId);
$location.search('category-id', value.categoryId);
$scope.vars.model.highlight.categoryId = value.categoryId;
}
Next time please provide more code which works, that way you can get better answers.
Here is a Demo plunker which I created to test the code which you provided. It works just fine. If you could provide more code then we could find the real reason why it did not work.
I created two controllers parentCtrl and childCtrl which uses your code and object of provided structure.
$scope.vars = {
model:{
highlight:{
categoryId : 5 //This value is set for testing purposes
}
}
};
Also, I changed watch target (vars.model -> vars.model.highlight) to be the same as in parent controller
$scope.$watch('vars.model.highlight', function(newValue, oldValue) {
console.log('child model changed (new/old)', newValue, oldValue);
$scope.$emit('highlightChange', newValue);
console.log('Emited change event');
}, true);
Thanks for the help. I found out that my event originated from a manual call to $scope.$digest() due to the originating event being triggered from a daterangepicker 'apply.daterangepicker' event.
When I changed that to $scope.$apply, the problem seemed to go away.
I can assume from that, that $apply() is the one in charge of keep calling $watch as long as there are changes, and that $digest() doesn't do so.
For future reference, I placed the problem here:
https://plnkr.co/edit/ItkALhw16Aqukk3EFrRz?p=preview
$scope.digest();
should become
$scope.apply();
Related
I've use dropdown list which render some results via $scope.$watch listener callback in Angular JS controller.
When i change dropdown list other value it works fine.
But in some case i need to keep last selected results(does not need rise that event).
I've tried ng-change and ng-click but it works after
$scope.$watch raised.
Any help would be appreciated
There is an overload on $scope.$watch() where you can also capture the previous value of the watched property.
$scope.$watch(propertyExpression, newValue, oldValue)
Is this what you are looking for? It is hard to read your mind without code samples ;)
If you want to keep even older values, you might want to consider keeping some state in your controller.
BTW, it can be interesting to always use this overload and check first whether the object you are watching really changed using (newValue !== oldValue) because (if I recall correctly) $scope.$watch is also triggered when $scope.$apply is called on a higher level node.
See if this works,
var unregisterWatch =
$scope.$watch('param', function(newVal, oldVal) {
if (newVal === oldVal)
return; //
});
call this function when required, it will cancel Angular's watch on the variable.
I often updated model variables corresponding to DOM expression ({{}}) within the controllers. e.g.
$scope.myVar = new_value;
Some times the corresponding DOM expression {{myVar}} is updated automtically, others it's not.
I understand that sometimes we need to call $scope.$apply but...
I don't understand when I should call it
Some times I call it (let's say, just to be "sure") but I get this error (I guess since it's already being executed):
Error: [$rootScope:inprog]
http://errors.angularjs.org/1.3.6/$rootScope/inprog?p0=%24digest
Any clue?
Apply essentially "refreshes" your front end with the changes that had occurred to your scope.
Most of the time you dont need to do apply as it already is done for you.
Lets say that you do an ng-click(); Apply is done for you.
However, there are cases where apply is not triggered, and you must do it yourself.
Example with a directive:
.directive('someCheckbox', function(){
return {
restrict: 'E',
link: function($scope, $el, $attrs) {
$el.on('keypress', function(event){
event.preventDefault();
if(event.keyCode === 32 || event.keyCode === 13){
$scope.toggleCheckbox();
$scope.$apply();
}
});
}
}
})
I have made changes to my scope but no apply was done for me thus i need to do it myself.
Another example with a timeout:
$scope.getMessage = function() {
setTimeout(function() {
$scope.message = 'Fetched after two seconds';
console.log('message:' + $scope.message);
$scope.$apply(); //this triggers a $digest
}, 2000);
};
Understanding apply and digest
A good way to understand the purpose of $scope.apply() is to understand that basically it does an internal .digest() within angular to make sure the scope is in sync (double check if anything has changed, etc).
Most of the time, you never need it! Most typical angular things you'll do, ng-click for example, will automatically trigger it for you when you make any changes to the scope.
But take as an example a jQuery UI dialogBox.
Let's say you prompt the user something, and you need to update your scope when they push the OK button.
Angular isn't aware of that button, nor does it know when any event is fired on it.
Hence, this is a very common use-case for $scope.apply()
Inside of that OK buttons event, you'd simply do:
$scope.apply(function () {
// Angular is now aware that something might of changed
$scope.changeThisForMe = true;
});
In a nutshell $scope.$apply tells Angular and any watchers that values have been changed and to go back and check if there are any new values. This keeps things within the Angular context regardless of how you made a change, like in a DOM event, jQuery method, etc.
I have an asynchronous call which is making a change a value on $scope. When it completes, I don't see my view updated, but if I append a $scope.digest() I do see the view update. e.g.
// a doesn't update in view
$rootScope.$on('some_event', function() {
$scope.a = true;
});
// a does update in view
$rootScope.$on('some_event', function() {
$scope.a = true;
$scope.$digest();
});
According to http://www.sitepoint.com/understanding-angulars-apply-digest/ the $digest cycle repeats itself until the $scope has settled (a minimum of two times).
Why wouldn't I see this update?
Thanks!
Dispatching event either by $emit or $broadcast functions call don't start digest cycle itself. Thus, if you have an event dispatched from async code like window.setTimeout or some third-party library, you must use $scope.$apply in event handler or $scope.$digest in code which fire event.
I have a list of hidden items.
I need to show the list and then scroll to one of them with a single click.
I reproduced the code here: http://plnkr.co/edit/kp5dJZFYU3tZS6DiQUKz?p=preview
As I see in the console, scrollTop() is called before the items are visible, so I think that ng-show is not instant and this approach is wrong.
It works deferring scrollTop() with a timeout, but I don't want to do that.
Are there other solutions?
I don't see any other solution than deferring the invocation of scrollTop() when using ng-show. You have to wait until the changes in your model are reflected in the DOM, so the elements become visible. The reason why they do not appear instantly is the scope life cycle. ng-show internally uses a watch listener that is only fired when the $digest() function of the scope is called after the execution of the complete code in your click handler.
See http://docs.angularjs.org/api/ng.$rootScope.Scope for a more detailed explanation of the scope life cycle.
Usually it should not be a problem to use a timeout that executes after this event with no delay like this:
setTimeout(function() {
$(window).scrollTop(50);
}, 0);
Alternative solution without timeout:
However, if you want to avoid the timeout event (the execution of which may be preceded by other events in the event queue) and make sure that scrolling happens within the click event handler. You can do the following in your controller:
$scope.$watch('itemsVisible', function(newValue, oldValue) {
if (newValue === true && oldValue === false) {
$scope.$evalAsync(function() {
$(window).scrollTop(50);
});
}
});
The watch listener fires within the same invocation of $digest() as the watch listener registered by the ng-show directive. The function passed to $evalAsync() is executed by angular right after all watch listeners are processed, so the elements have been already made visible by the ng-show directive.
You can use the $anchorScroll.
Here is the documentation:
https://docs.angularjs.org/api/ng/service/$anchorScroll
Example:
$scope.showDiv = function()
{
$scope.showDivWithObjects = true;
$location.hash('div-id-here');
$anchorScroll();
}
I'm having an issue getting a watch to work within a directive. I've put together a simple example here. http://plnkr.co/edit/A7zbrsh8gJhdpM30ZH2P
I have a service and two directives. One directive changes a property in the service, and another directive has a watch on that property. I expected the watch to fire when the property is changed but it doesn't.
I've seen a few other questions like this on the site, but the accepted solutions on them have not worked here. I've considered using $broadcast or trying to implement an observer, but it seems like this should work and I don't want to over complicate things if possible.
Mark Rajcok' answer is incomplete. Even with angular.copy(), $watch listener will be called once and never again.
You need to $watch a function:
$scope.$watch(
// This is the important part
function() {
return demoService.currentObject;
},
function(newValue, oldValue) {
console.log('demoService.currentObject has been changed');
// Do whatever you want with demoService.currenctObject
},
true
);
Here the plunker that works: http://plnkr.co/edit/0mav32?p=preview
Open your browser console to see that both the directive and the demoService2 are notified about demoService.currentObject changes.
And btw angular.copy() is not even needed in this example.
Instead of
this.currentObject = newObject;
use
angular.copy(newObject, this.currentObject);
With the original code, the viewer directive is watching the original object, {}. When currentObject is set to newObject, the $watch is still looking for a change to the original object, not newObject.
angular.copy() modifies the original object, so the $watch sees that change.