$scope.$watch doesn't trigger on every change - javascript

I'm using angularJS 1.4.8, yesterday i noticed that the $scope.$watch doesn't trigger on every change which caused bug in my application.
is there a way to force it to work on every change immediately ?
like in this code, in every change on message i want the function in watch to trigger:
(function(){
angular.module('myApp', [])
.controller('myAppController', myAppController)
function myAppController($scope){
console.log('controller loading !');
$scope.message = 'message1';
$scope.$watch('message', function(newMessage){
console.log('newMessage', newMessage)
});
function changeMessage(){
$scope.message='hi';
$scope.message='hi12';
}
changeMessage();
}
})();
the console will print:
controller loading !
newMessage hi22
plunker link https://plnkr.co/edit/SA1AcIVwr04uIUQFixAO?p=preview
edit:
I would really like to know if there are any other ways than wrapping the change with timeout and using scope apply, in my original code iv'e multiple places where i change the scope property and i would like to avoid using this every change.

This happens because the watch will only be triggered if the value is changed "between" digest loops.
Your function is changing the message value on the scope in the same function. This will be executed in the same digest loop.
When angular moves on to the next loop it will only see the last changed value which in your case will be hi22.
Here's a great article which makes this behaviour clear

update your changeMessage function so that it uses $scope.$apply function which will ensure that your changes are reflected and angular is aware of your changes to the variable.
changeMessage() {
setTimeout(function () {
$scope.$apply(function () {
$scope.message = "Timeout called!";
});
}, 2000);
}

If you change value into the same digest cycle the watcher is not triggered and last value is taken. When we run $timeout, we change $scope.message value in next digest cycle and watcher catches it as expected.
Take look on simple test:
$scope.$watch(function(){
console.log('trigger');
return $scope.message;
},
function(newMessage){
console.log('newMessage', newMessage)
});
function changeMessage(){
$scope.message='hi';
$timeout(function(){
$scope.message='hi12';
});
}
Output:
controller loading !
trigger
newMessage hi
trigger
trigger
newMessage hi12
trigger

There is no need to wrap changeMessage in setTimeout and $apply at the same time. If you need to skip some time before execution, just use:
function changeMessage(){
$timeout(function(){
$scope.message = 'message';
}/* or add time here, doesn't matter */);
}
Or just:
function changeMessage(){
$scope.message = 'message';
$scope.$apply();
}
Both methods calls $rootScope.$digest in the end. Here is more information: https://www.codingeek.com/angularjs/angular-js-apply-timeout-digest-evalasync/

$watch() only triggers between every $digest().
Detailed explaination about the $apply() and $digest()
In your case you keep updating the $scope.message in the current $digest() cycle.
You could change that by applying each new value to the $scope using $apply(). Like #Ajinkya wrote.
The only problem, with setting 2000ms as timeout, doesn't allways ensure it executes after the $digest(). On top of that, Angular has a build in timeout function. See below.
(function(){
angular.module('myApp', [])
.controller('myAppController', myAppController)
function myAppController($scope, $timeout){
console.log('controller loading !');
$scope.message = 'message1';
$scope.$watch('message', function(newMessage){
console.log('newMessage', newMessage)
});
function changeMessage(){
setTimeout(function () {
$scope.$apply(function () {
$scope.message='hi12';
});
}, 2000);
}
changeMessage();
}
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myAppController"></div>
Solution
The best way would be to call the build in $timeout function, without setting the time in milliseconds.
This way, angular allways ensures the $timeout will run after the latest $digest(). On top of that. You dont have to use the $scope.$apply(). Because the $timeout allready runs a $digest(), where $scope.$apply() is manually invoking a new $diggest() cycle.
(function(){
angular.module('myApp', [])
.controller('myAppController', myAppController)
function myAppController($scope, $timeout){
console.log('controller loading !');
$scope.message = 'message1';
$scope.$watch('message', function(newMessage){
console.log('newMessage', newMessage)
});
function changeMessage(){
$timeout(function () {
$scope.message='hi12';
});
}
changeMessage();
}
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myAppController"></div>

Related

Angular $timeout for delay

I want the browser to wait for 3 seconds before executing the next line of code.
$timeout(3000); in script is not seeming to do the trick. Am I doing anything wrong here.
I use it inside a $scope function
app.expandController = function ($scope,$interval, $timeout) {
$scope.nextLevel = function () {
//stop numbers
$scope.StopTimer();
$timeout(function(){return true;},3000);
//restart timer with new numbers
$scope.StartTimer();
$scope.thresholdwatch == false
}
}
and I split controller file by passing $scope and $timeout to another function
app.controller('myCtrl', function ($scope, $interval, $timeout) {
app.expandController($scope, $interval,$timeout);
});
If you are using $timeout you need to place your next executing code in timeout callback. But a simple hack with vanila js would be,
function sleep(ms) {
var dt = new Date();
while (Date.now() - dt.getTime() <= ms) {}
return true;
}
console.log('timer start');
sleep(3000);
console.log('Printing after 3000ms');
I think you get the idea about $timeout in AngularJS or generally JS incorrect. Both of the below examples will not work.
$timeout(function(){return true;},3000);
$timeout(3000);
$timeout is a non-blocking function so calling it will not stop JS from running the code below it. In Javascript and AngularJS, there is no truly sleep function (a NodeJS lib for sleep is actually the C++ binding for it, and therefore, not applicable to client-side app using AngularJS). Instead, to make sure that your code will be run after a specific amount of time, you put it inside the the $timeout function:
$timeout(function(){
//code need to be delayed must be in here
}, TIME_TO_WAIT_FOR);
The usage of $timeout is wrong. Use like this:
$timeout(function(){
// Do something in timeout
}, 3000);

Two way binding in Angular JS

I get a problem with follwoing code.
my html page code :
<body ng-app="myapp">
<div ng-controller="myController">
The message is {{message}}
<input type="button" value="Change Message" ng-click="changeMessage()">
</div>
My Controller code:
app.controller('myController',function($scope)
{
$scope.changeMessage=function()
{
setTimeout(function(){
console.log("Message changed");
$scope.message="Hurray !!! New Message";
},3000);
$scope.newMessage=function()
{
$scope.message="hello";
console.log("new message");
};
But if I use changeMessage function I am not able to see the changed Message property even though the console.log message comes.
what is missing here in both cases.
Thanks in advance
The reason that the change isn't reflected in the view is that because of the assignment is done in the callback in setTimeout which results in angular not noticing the change. This has to do with the so called digest cycle. There are different ways to solve this.
use $scope.$apply() to wrap your assignment
or even better use the existing $timeout service provided by angular instead of the call to setTimeout which already handles the problem above for you.
For details, see https://docs.angularjs.org/api/ng/service/$timeout for usage of $timeout
and https://docs.angularjs.org/api/ng/type/$rootScope.Scope for the reasons behind $apply.
A general explanation of what is happening here is described at http://www.sitepoint.com/understanding-angulars-apply-digest/
every change should happen within an angular-digest-cycle. if you change values from the outside (which is exactly what happens, if you use setTimeout instead of angular's $timeout) angular does not update your view until the next digest-cycle (https://www.ng-book.com/p/The-Digest-Loop-and-apply/). so in your case the message is already set, but the view has not been updated.
try something like this:
app.controller('myController', function($scope, $timeout) {
$scope.changeMessage = function() {
$timeout(function(){
console.log("Message changed");
$scope.message="Hurray !!! New Message";
}, 3000);
$scope.newMessage=function() {
$scope.message="hello";
console.log("new message");
};
you should use $digest() after that the timeout is done:
$scope.changeMessage=function()
{
setTimeout(function(){
$scope.message="Hurray !!! New Message";
$scope.$digest();
},3000);
}
please note that it is much better to use $digest instead of $apply (performance related):
scope.$digest() will fire watchers on the current scope, and on all of its children, too. scope.$apply will evaluate passed function and run $rootScope.$digest()

How to select text in an input after a change in Angular at the right time?

I have an input box. When the text changes, I need to select the text. I understand that there are many events going on and I need to wait for them to finish. I put in a timeout and it works. However I don't want to rely on a constant time. Is there any way how to select the text when Angular is finished changing the text?
Example HTML:
<input type="text" value="{{txt}}">
<button ng-click="select()">Press</button>
Example JS:
angular.module('MyApp', []).controller('MyCtrl', function ($scope, $interval) {
$scope.txt = "Hello World";
$scope.select = function () {
$scope.txt = "Bye World";
// doesn't work, too early
document.querySelector("input").setSelectionRange(0, 4);
// works
$interval(function () {
document.querySelector("input").setSelectionRange(0, 4);
}, 10, 1);
}
});
Working example is JSFiddle.
EDIT: From the answers it looks like using timeouts (even with 0 delay) is a common practice, but the question remains whether this will guarantee that the selection happens after Angular finishes updating the text.
You can use $timeout with 0 delay for this purpose.
$timeout(function(){
document.querySelector("input").setSelectionRange(0, 4);
});
Angular changes the DOM in next $digest cycle, it is extremely fast, but it won't be available as soon as you run $scope.x = ??. Normally we would use $timeout to "wait" in this case.
$timeout 0 delay is in fact good enough since angular's dirty checking (and $digest cycle) happens synchronously. (0 delay would only fire up when the current process is free).
If you really really want to guarantee it, here's how:
angular.module('MyApp', []).controller('MyCtrl', function ($scope, $timeout) {
$scope.txt = "Hello World";
$scope.select = function () {
$timeout(function () {
$scope.$apply(function () {
$scope.txt = "Bye World";
});
document.querySelector("input").setSelectionRange(0, 4);
});
};
});
You have to use timeout to wrap the $apply, because the scope function would trigger a $digest and you cannot call $digest within a $digest cycle($apply calls $digest for you). $apply here guarantee the scope variable is updated, hence your setSelectionRange would only happen after the update.
If you do not want to use $timeout, you can fire event with ng-blur. A blur event fires when an element has lost focus.
Working fiddle
HTML:
<div ng-controller="MyCtrl">
<form>
<input type="text" ng-blur="select()" value="{{txt}}">
<button>Press</button>
</form>
</div>
Script:
angular.module('MyApp', []).controller('MyCtrl', function ($scope, $interval) {
$scope.select = function () {
document.querySelector("input").setSelectionRange(0, 4);
}
});

angular weirdness with element.click and $location.search

Here is the problem
<div id="my-id" ng-click="moveOn(10)">Click me</div>
In the directive I have
scope.moveOn = function (val) {
$location.search('id', val);
}
And finally in the parent of this directive I listen for a change of this id
$scope.$watch(function () {
return $routeParams.id;
}, function (newId, oldId) {
...
});
This setup works great, after the $location.search is called the $watcher is triggered immediately. But now I also have a directive which does it slightly different, as follows:
element.find('#my-id').click(function (val) {
$location.search('id', val);
});
In the template there is no ng-click!
In this situation I can also see that the call to $location.search is made, but now it takes a very long time (a couple of seconds) before the watcher goes off.
So for some reason there must be a difference between ngClick and binding to a click event. Any suggestions what might be going on here ?
You are updating angular within an event that is outside of angular.
Try using $apply to notify angular of the change so it can run a digest
element.find('#my-id').click(function (val) {
scope.$apply(function(){
$location.search('id', val);
});
});

AngularJS - triggering $watch/$observe listeners

I would like to fire all $watch/$observe listeners even if watched/observed value didn't change. This way I could provide a "testing only" feature to refresh current page/view without user interaction. I've tried to call $apply/$digest but that didn't worked:
$timeout(function(){
$scope.$apply();
});
$timeout(function(){
$scope.$digest();
});
Is there any other way to do it?
Best Regards,
Executing $scope.$apply() will trigger digest cycle as it internally calls $digest, below is example of manual change.
number variable won't get bound as timeout brings it out of angulars scope.
setTimeout(function () {
$scope.number = Math.random();
});
however you can "force" it to show up by manually applying scope changes:
setInterval(function () {
$scope.$apply();
}, 100);
Demos:
No change / Change with manual updates
This will not trigger watchers though. From $digest implementation, it checks if value has changed since the last watch evaluation and will run callback only if it did.
if ((value = watch.get(current)) !== (last = watch.last) ... [rootScope.js]
Therefore you will need somehow change value of the last execution and it's possible to do via $$watchers object on the scope:
$scope.digest = function () {
setTimeout(function () {
angular.forEach($scope.$$watchers, function (w) {
w.last.value = Math.random();
});
$scope.$apply();
});
}
DEMO
How about to use the $emit function then capture that event with $on function?
Within an $emit() event function call, the event bubbles up from the child scope to the parent scope. All of the scopes above the scope that fires the event will receive notification about the event.
We use $emit() when we want to communicate changes of state from within our app to the rest of the application.
_.bind($scope.$emit, $scope, 'myCustomEvent');
then on the capture phase:
$scope.$on('myCustomEvent', function() {
// do something
});

Categories

Resources