I am using AngularJS and a phone web service to make calls through WebSockets.
The web service has several callbacks such as Phone.onIncomingCall
When I use this function to set a $scope variable the view is not updated automatically except if I use $scope.$apply right after.
Phone.onIncomingCall = function(){
$scope.myVar = "newValue";
$scope.$apply(); // only works if I call this line
};
What is the reason for this behaviour (is it expected) and is there a way around using $scope.apply() in each function?
Angular is "unaware" of the update to the scope variable you've made, since you're updating it from outside of the Angular context. From the docs for $apply:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
By running $apply on the scope, $digest is called from $rootScope, which will trigger any $watchers registered on $scope.myVar. In this case, if you're using the variable in your view via interpolation, this is where the $watcher was registered from.
It is the expected behavior, angular works like that internally.
I recommend the following:
Phone.onIncomingCall = function () {
$scope.$apply(function () {
$scope.myVar = 'newValue';
});
}
Related
In AngularJS, can I access $timeout without resorting to using the DI container?
Edit:
For those asking for "why". I am using an older version of AngularJS and want to create a utility function that will perform a digest asynchronously.
The intention being that I can place logic inside a promise then for execution after a digest has occurred and the UI has taken into account the model change.
I do not want client code to have to use the injector to use said function.
I wanted something like this:
my-file.js
//...
model.watchedProperty = 'new value';
// Now I want to wait for a digest to occur so that I can ensure the UI is updated before proceeding...
digestAsync(localScope)
.then(function() {
// continue...
});
// ...
digest-async.js
function digestAsync(scope) {
return $timeout(function() { // I don't want to have to use the injector...
scope.$digest();
});
}
You can manually get the injector and then get the $timeout service.
var $injector = angular.injector(['ng']);
var $timeout = $injector.get('$timeout');
If you don't want to inject $timeout you can add $injector as a DI, and in your code you can put this:
$timeout = $injector.get('$timeout');
No, you can not. A lot of angular is itself written in angular, including $timeout. So you can access it in any way you can access any other self-written service - by Dependency Injection
You only need to use $timeout if you want the callback function to be executed inside an Angular digest cycle, and if you pass 0 as the interval then it will be executed in the next digest.
The setTimeout function from JS will execute the callback using the next "thread" cycle. That means that the current thread has to terminate first before the callback can be execute. That doesn't mean that the next thread cycle will also be an Angular digest.
This doesn't matter in your example because you are forcing a $digest, except that you should be using $apply and not $digest.
I think what you are trying to do is create a promise that resolves inside an Angular digest. That's not really the proper use for promises because a digest is not a resource to be resolved.
I think you can skip everything related to the $timeout and just use $apply as it was designed.
localscope.$apply(function(){
// do digest work here
});
That is the same as the following.
$timeout(function(){
// do digest work heere
},0);
Both can be executed outside of Angular, and both will execute the callback during the next digest cycle. $apply will call $digest if needed and it does state this in the documentation.
For times when you don't know if a $digest is in progress.
/**
* Executes the callback during a digest.
*
* #param {angular.IScope|angular.IRootScopeService} $scope
* #param {function()} func
*/
function safeApply($scope, func) {
if ($scope.$$phase) {
func();
} else {
$scope.$apply(function () {
func();
});
}
};
Since you plan to use it outside the app, there is zero chance that you will stumble upon infamous '$digest already in progress' error. Why? Because $digest isn't asynchronous process, more like the opposite of it. All that $digest() function does is calculating current scope state in loop - no promises, no timeouts.
That's exactly what
Don't do if (!$scope.$$phase) $scope.$apply(), it means your
$scope.$apply() isn't high enough in the call stack.
well-known statement refers to. The only time when 'already in progress' will happen is when $digest is triggered during $digest or within $apply, e.g. when outer JS function is used as Angular callback. This indicates poor application design and should be treated accordingly.
Thereby $digest function can be exposed to window
app.run(function ($rootScope) {
window.$digest = angular.bind($rootScope, $rootScope.$digest);
});
And used in synchronous manner. No promises. No timeouts.
model.watchedProperty = 'new value';
$digest();
// 'watchedProperty' watcher has been digested ATM
And I assume that you already know why mixing Angular and outer code like that is considered a bad practice and should be avoided.
I'm developing a Cordova/PhoneGap app, and I'm using the $cordovaPush plugin (wrapped for PushPlugin) to handle push notifications.
The code looks something like this:
var androidConfig = {
"senderID" : "mysenderID",
"ecb" : "onNotification"
}
$cordovaPush.register(androidConfig).then(function(result) {
console.log('Cordova Push Reg Success');
console.log(result);
}, function(error) {
console.log('Cordova push reg error');
console.log(error);
});
The "ecb" function must be defined with window scope, ie:
window.onNotification = function onNotification(e)...
This function handles incoming events. I'd obviously like to handle incoming events in my angular code - how can I integrate the two so that my onNotification function can access my scope/rootScope variables?
Usually, you'll wrap your 3rd party library in a service or a factory, but in the spirit of answering your particular scenario...
Here's one possibility:
angular.module('myApp').
controller('myController', function($scope, $window) {
$window.onNotification = function() {
$scope.apply(function() {
$scope.myVar = ...updates...
});
};
});
A couple of things to notice:
Try to use $window, not window. It's a good habit to get into as it will help you with testability down the line. Because of the internals of Cordova, you might actually need to use window, but I doubt it.
The function that does all of the work is buried inside of $scope.apply. If you forget to do this, then any variables you update will not be reflected in the view until the digest cycle runs again (if ever).
Although I put my example in a controller, you might put yours inside of a handler. If its an angular handler (ng-click, for example), you might think that because the ng-click has an implicit $apply wrapping the callback, your onNotification function is not called at that time, so you still need to do the $apply, as above.
...seriously... don't forget the apply. :-) When I'm debugging people's code, it's the number one reason why external libraries are not working. We all get bit at least once by this.
Define a kind of a mail controller in body and inside that controller use the $window service.
HTML:
<body ng-controller="MainController">
<!-- other markup .-->
</body>
JS:
yourApp.controller("BaseController", ["$scope", "$window", function($scope, $window) {
$window.onNotification = function(e) {
// Use $scope or any Angular stuff
}
}]);
This question already has answers here:
How do I use $scope.$watch and $scope.$apply in AngularJS?
(6 answers)
Closed 7 years ago.
What does $scope.apply do? In some cases, if $scope.apply() is not present, $scope.watch() is not triggered. For example,
The controller has the following:
setTimeout(function(){
$scope.person = someperson;
}, 500);
person is being watched in the directive.
$scope.watch('person', function(){
console.log($scope.person);
$scope.apply();
});
In this case, watch is triggered only when apply is there.
$scope.apply() will trigger the $digest loop of AngularJS. To put is simple, it's just a convenient way to trigger the app rerender.
Usually it is used when you want to run a piece of code that is outside of angular app.
Direct from the documentation:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
Example of using scope.$apply() with jQuery Datepicker:
angular.module('customApp', []).directive('datepicker', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
jQuery(element).datepicker({
onSelect: function (date) {
scope.myDate = date;
scope.$apply();
}
});
}
};
});
If you modify an AngularJS model outside (from external JavaScript) - you should use $scope.$apply() to let AngularJS know that model has changed. In your example you use setTimeout() which is an async external js method. However if you use the AngularJS $timeout you wont need to call $scope.$apply().
setTimeout(function(){}:-It is javascript function which is out of the scope of anuglar js you need to manually apply the digest cycle with the help of $scope.apply();
But instead you can use $timeout service which is more angular way.
It is possible for a directive to update a service and then use the updated version?
In my service (cfg), I have a variable and an update function...
var test = "unfired";
function updateTest(){
console.log("LOG:","updateTest is firing");
test = "fired";
}
In the linking function of my directive I have
scope.$watch(watcher, function(newVal, oldVal) {
console.log("Before:",cfg.test);
cfg.updateTest();
console.log("After:",cfg.test);
}); //scope.$watch
Even though the updateTest function is firing, the console logs the same value before and after.
Now if cfg were a controller instead of a service I would do something like
function updateTest(){
console.log("LOG:","updateTest is firing");
test = "fired";
cfg.$apply() //or cfg.$digest()
}
But obviously that won't work. I have also tried injecting cfg to the controller and and $apply() to the link function...
console.log("Before:",cfg.test);
scope.$apply(function(){
cfg.updateTest()
});
console.log("After:",cfg.test);
which did trigger updateTest(), but it did not update the cfg service as the directive understands it.
Perhaps another way to say it is that I would like to "reinject" the service into the directive.
If you are wondering why I'd like to do this, it's because I have a bunch of d3.js animations as directives that share the same scales, and I'd like certain events to trigger changes in the scales' domains from one directive to the others.
Rather than using a service to communicate between directives. Try using "broadcast". You can throw an event into the air and anybody listening will run whatever function you want. It works like this.
Directive 1:
$rootScope.$broadcast('event:updateTest');
Directive 2:
$rootScope.$on("event:updateTest", function (event, next, current) { ... }
Then you can deal with local instances of your 'test' variable, rather than a service 'global' variable.
I have a click event that happens outside the scope of my custom directive, so instead of using the "ng-click" attribute, I am using a jQuery.click() listener and calling a function inside my scope like so:
$('html').click(function(e) {
scope.close();
);
close() is a simple function that looks like this:
scope.close = function() {
scope.isOpen = false;
}
In my view, I have an element with "ng-show" bound to isOpen like this:
<div ng-show="isOpen">My Div</div>
When debugging, I am finding that close() is being called, isOpen is being updated to false, but the AngularJS view is not updating. Is there a way I can manually tell Angular to update the view? Or is there a more "Angular" approach to solving this problem that I am not seeing?
The solution was to call...
$scope.$apply();
...in my jQuery event callback.
Why $apply should be called?
TL;DR:
$apply should be called whenever you want to apply changes made outside of Angular world.
Just to update #Dustin's answer, here is an explanation of what $apply exactly does and why it works.
$apply() is used to execute an expression in AngularJS from outside of
the AngularJS framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the AngularJS framework we need to perform proper scope life cycle of
exception handling, executing watches.
Angular allows any value to be used as a binding target. Then at the end of any JavaScript code turn, it checks to see if the value has changed.
That step that checks to see if any binding values have changed actually has a method, $scope.$digest()1. We almost never call it directly, as we use $scope.$apply() instead (which will call $scope.$digest).
Angular only monitors variables used in expressions and anything inside of a $watch living inside the scope. So if you are changing the model outside of the Angular context, you will need to call $scope.$apply() for those changes to be propagated, otherwise Angular will not know that they have been changed thus the binding will not be updated2.
Use
$route.reload();
remember to inject $route to your controller.
While the following did work for me:
$scope.$apply();
it required a lot more setup and the use of both .$on and .$broadcast to work or potentially $.watch.
However, the following required much less code and worked like a charm.
$timeout(function() {});
Adding a timeout right after the update to the scope variable allowed AngularJS to realize there was an update and apply it by itself.