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.
Related
I'm starting with AngularJS and I have a question related to the way a method is invoked when setting a new controller.
Let's say I have a route configured like this:
$routeProvider.when('/myApp/:id', {controller: 'MyAppCtrl'});
What's the difference between these 2 controller codes, regarding the execution context and the $scope life cycle?
How many times each alternative runs after the partial is loaded?
.
app.controller('MyAppCtrl',function($scope,$routeParams){
$scope.$on('$routeChangeSuccess', function(){
$scope.data = getNewData($routeParams.id);
});
function getNewData(id){
...
}
});
And:
app.controller('MyAppCtrl',function($scope,$routeParams){
$scope.data = getNewData($routeParams.id);
function getNewData(id){
...
}
});
Thank you very much.
In my opinion, I would use resolve in route config instead of your 2 options
back to your question.
I believe controller only execute once after the partial is loaded.
and these 2 cases are pretty much doing the same thing. The first one relies on event, which is an extra step comparing with the 2nd one.
$on assigns a listener to an event. Meaning You could trigger $on manually by sending $broadcast('routeChangeSucess').
The second code is run one time, once The partial is loaded.
So using $on for a Controller load dosn't do You any good
One thing I still don't understand about Angular is...
Why use $window when i could just use the window global object and get the same result? Why use $timeout when I could use setTimeout, etc.
I use this native javascript code sometimes and it works just fine, so why did AngularJS created these wrappers in the first place?
It is integrated into the digest cycle (will trigger the HTML compiler and the DOM refreshes). Also makes the code easier to test because you can mock the $timeout object and test that it was called.
For example with $timeout, you can call $timeout.flush() in your unit tests and it will act as if the timeout waited the appropriate amount of time and trigger the callback. This makes your tests run much faster which is also good for TDD.
Here is a simple async example - assume that asyncThing.method() uses $timeout and $log to output a message
describe('Async test', function () {
var asyncThing, $timeout, $log;
beforeEach(module('async'));
beforeEach(inject(function (_asyncThing_, _$timeout_, _$log_) {
asyncThing = _asyncThing_;
$timeout = _$timeout_;
$log = _$log_;
}));
it('should do some async stuff', function () {
asyncThing.method(some_arguments);
$timeout.flush();
expect($log.info.logs).toContain(['Some output']);
});
});
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';
});
}
I want to update an Angular scope with data returned by some jQuery ajax call. The reason why I want to call the Ajax from outside Angular is that a) I want the call to return as fast as possible, so it should start even before document.ready b) there are multiple calls that initialize a complex model for a multiple-page web app; the calls have dependencies among themselves, and I don't want to duplicate any logic in multiple Angular controllers.
This is some code from the controller. Note that the code is somewhat simplified to fit here.
$scope.character = {};
$scope.attributeArray = [];
$scope.skillArray = [];
The reasoning for this is that a character's attributes and skills come as objects, but I display them using ng-repeat, so I need them as arrays.
$scope.$watch('character',function(){
$scope.attributeArray = getAttributeArray($scope.character);
$scope.skillArray = getSkillArray($scope.character);
});
In theory, when $scope.character changes, this piece of code updates the two arrays.
Now comes the hard part. I've tried updating $scope.character in two ways:
characterRequestNotifier.done(function() { // this is a jQuery deferred object
$scope.$apply(function(){ // otherwise it's happening outside the Angular world
$scope.character = CharacterRepository[characterId]; // initialized in the jquery ajax call's return function
});
});
This sometimes causes $digest is already in progress error. The second version uses a service I've written:
repository.getCharacterById($routeParams.characterId, function(character){
$scope.character = character;
});
, where
.factory('repository', function(){
return {
getCharacterById : function(characterId, successFunction){
characterRequestNotifier.done(function(){
successFunction( CharacterRepository[characterId] );
});
}
};
});
This doesn't always trigger the $watch.
So finally, the question is: how can I accomplish this task (without random errors that I can't identify the source of)? Is there something fundamentally wrong with my approaches?
Edit:
Try this jsfiddle here:
http://jsfiddle.net/cKPMy/3/
This is a simplified version of my code. Interestingly, it NEVER triggers the $watch when the deferred is resolved.
It is possible to check whether or not it is safe to call $apply by checking $scope.$$phase. If it returns something truthy--e.g. '$apply' or '$digest'--wrapping your code in the $apply call will result in that error message.
Personally I would go with your second approach, but use the $q service--AngularJS's promise implementation.
.factory('repository', function ($q) {
return {
getCharacterById : function (characterId) {
var deferred = $q.defer();
characterRequestNotifier.done(function () {
deferred.resolve(CharacterRepository[characterId]);
});
return deferred.promise;
}
};
});
Since AngularJS has native support for this promise implementation it means you can change your code to:
$scope.character = repository.getCharacterById(characterId);
When the AJAX call is done, the promise is resolved and AngularJS will automatically take care of the bindings, trigger the $watch etc.
Edit after fiddle was added
Since the jQuery promise is used inside the service, Angular has no way of knowing when that promise is resolved. To fix it you need to wrap the resolve in an $apply call. Updated fiddle. This solves the fiddle, I hope it solves your real problem too.
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.