this is my controller:
.controller('mainController', function($http, $scope, $timeout, $sce, updateService) {
updateData = function() {
updateService.getDataA($http, $scope);
updateService.getDataB($http, $scope, $sce);
}
var updateIntervalId = setInterval(updateData, 1000);
})
Now when the user refreshes the page old requests are still being made, and I tried to cancel it by putting this under the interval call:
window.onbeforeunload = function() {
console.log('interval killed');
clearInterval(updateIntervalId);
}
the console is logging 'interval killed', so its being executed, but requests are still being made, why?
Let us do the angular way
Inject $interval service in your controller
And update your code to following
var intervalPromise = $interval(updateData, 1000);
$scope.$on('$destroy', function() { // Gets triggered when the scope is destroyed.
$interval.cancel(intervalPromise );
});
And remove window.onbeforeunload event handler.
When it comes to angularJS I would always recommend to use $interval to handle intervals, since it works better with the angularJS hidden mechanics.
Then if the event window.onbeforeunload lies outside the controller, you will never be able to access updateIntervalId since it lies inside another scope, hence you should make it global.
Related
I have an angular js app, that uses an ng-repeat with a directive like this:
<div data-ng-repeat="n in items">
<div data-my-directive
item="n"></div>
</div>
where items is an array with integers.
Depending on actions of the user, the items array can be completely destroyed and made anew with new integers.
First time, it may be [1,2,4,9]
and next it may be [1,3,6,7]
for instance. This is dependent on some user choices.
The directive my-directive will perform some business logic server-side, so it will call the server as soon as it gets loaded. And then after a result returns, it shows a nice table to the users.
The problem is that some users don't wait until everything is nice and loaded and switch their view (which means the array changes). In this case, I see that the calls to the server are still being executed and that the result-function is still being called, even though the directive itself has been destroyed because the ngRepeat has rebound and all of the directives are re-made.
For instance:
$http.get(service.url + '/filters').success(function(result) {
alert(result);
});
This will display all of the alerts, even of the directives that are no longer on the page.
This poses a few problems. Can I destroy the directives in the repeat when the array changes or something like that to make sure that no logic is executed in a directive that shouldn't exist anymore (or that isn't displayed on the page anymore) ?
Or do you have some other ideas on how best to approach this?
Ideally, the directives should just disappear after the ng-repeat has rebound itself, so no logic is executed as soon as data comes back from the server.
When the user changes the parameters you can cancel the running request and start a new one.
In this Scott Allen's blog post you can find the detailed explanation of how this work.
You start creating a service or a factory with the method you will call:
var getData = function(){
var canceller = $q.defer();
var cancel = function(reason){
canceller.resolve(reason);
};
var promise =
$http.get(service.url + '/filters', { timeout: canceller.promise})
.then(function(response){
return response.data;
});
return {
promise: promise,
cancel: cancel
};
};
Then you call it in this way:
var request = service.getData();
$scope.requests.push(request);
request.promise.then(function(movie){
$scope.movies.push(movie);
clearRequest(request);
}, function(reason){
console.log(reason);
});
You then provide a method that will cancel the request:
$scope.cancel = function(){
var request = // retrieve the correct request from the requests array
request.cancel("User cancelled");
// Remove the request from the array
};
So I have a few thoughts for your question.
First you could use ng-cloak which is used to prevent the Angular html template from being briefly displayed by the browser in its raw (uncompiled) form while your application is loading. I find this very helpful if I want the user to wait until all the data has returned to view the page. ex.
<div id="template1" ng-cloak>{{ 'hello' }}</div>
Second you could try a resolve. A resolve contains one or more promises that must resolve successfully before the route will change. This means you can wait for data to become available before actually changing routes.
$routeProvider
.when("/news", {
templateUrl: "newsView.html",
controller: "newsController",
resolve: {
message: function(messageService){
return messageService.getMessage();
}
}
})
The directive needs to use the $destroy event to cancel operations in progress.
app.directive("myDirective", function() {
return {
controller: function($scope, $http, $q) {
var canceller = $q.defer();
var cancel = function(reason){
canceller.resolve(reason);
};
$http.get(url, { timeout: canceller.promise})
.then(function(response){
$scope.data = response.data;
});
$scope.$on('$destroy', function () {
cancel("Scope destroyed");
});
}
}
});
When the ng-repeat removes an item, it destroys the scope of the item. Use the $destroy event to cancel asynchronous operations in progress.
From the Docs:
The $destroy() method is usually used by directives such as ngRepeat for managing the unrolling of the loop.
Just before a scope is destroyed, a $destroy event is broadcasted on this scope. Application code can register a $destroy event handler that will give it a chance to perform any necessary cleanup.
--AngularJS $rootScope.scope API Reference -- $destroy
I have a controller without $scope
angular.module('todoApp', [])
.controller('TodoListController', function() {
var todoList = this;
todoList.title = "Default title";
setTimeout(function() {
todoList.title = "Another title will appear after 5 seconds";
}, 5000);
// ...some magic here
});
And view:
<h1>Current title: {{TodoListController.title}}</h1>
This code won't works correctly, becaufe function in setTimeout won't run $digest() which can update my TodoListController.title.
I know I can use $scope and use $scope.$digest(). But - is it possible to run $digest() without it? I have always access to object angular. Maybe through this object?
You should use $timeout instead of vanilla setTimeout.
angular.module('todoApp', [])
.controller('TodoListController', function($timeout) {
var todoList = this;
todoList.title = "Default title";
$timeout(function() {
todoList.title = "Another title will appear after 5 seconds";
}, 5000);
// ...some magic here
});
Using $timeout from angular will handle starting digest cycle.
Angulars $timeout is also useful if you want to notify angular to do updates without delay. In this case you can invoke it without second parameter.
$timeout(function(){
//something outside angular ...
});
Function passed to $timeout will be invoked on next digest cycle.
This way is better than calling $digest manually because it will prevent digest already in progress errors.
You must use the angular version of timeout: $timeout.
https://docs.angularjs.org/api/ng/service/$timeout
This service trigger the angular $digest process.
In AngularJS, everywhere I read it says that you almost rarely ever call $apply() yourself, as it's a bad idea somehow (usually left unclear.)
Can someone explain to me:
How to avoid using $apply() but still updating variables in $scope
Why is it so terribly bad to call $apply()
And how to determine when it is OK to use $apply()
Example code:
app.controller('ExampleController', function($scope) {
$scope.variableForView = 'Initial value';
// for demonstration purposes
someEvent.on('eventFire', function(data) {
$scope.variableForView = data;
$scope.$apply(); // ???
});
});
Basically, $apply runs the $digest. If $digest is already running, then you can't initiate another $digest.
I have created the method $safeApply(), that I add to the prototype of my $rootScope inside the run() commend:
// $safeApply only calls $apply if $digest is not running
// Otherwise, will only call back fn()
Object.getPrototypeOf($rootScope).$safeApply = function(fn)
{
var phase = $rootScope.$$phase;
if (phase == '$apply' || phase == '$digest') {
if (_.isFunction(fn)) fn();
} else {
this.$apply(fn);
}
};
This basically makes sure you are not already in the $digest cycle before calling $apply. If you are (which means it is not necessary to call $apply, I just run the the fn. I never use $apply() and always use $safeApply().
Use the binding methods angular provides. Then it will do the $apply for you. The reason they say manually $applying is bad is because you should be using the angular methods for binding and if you are not using them and having to $apply manually, then your doing something wrong.
app.controller('ExampleController', function($scope) {
$scope.variableForView = 'Initial value';
// for demonstration purposes
$scope.$on('someEventFire', function(data) {
$scope.variableForView = data;
});
});
Also see https://github.com/angular/angular.js/wiki/When-to-use-%24scope.%24apply%28%29
This is probably a total newb question...apologies, but I can't get my head around it.
In a lot of angular documentation/examples I see asynchronous functions wrapped in 'timeout' blocks. Many are wrapped in setTimeout() and require the explicit use of
if (!$scope.$$phase) {
$scope.$apply();
}
Given that angular provides $timeout, the above code just seems outdated or wrong and within angular the use of $timeout should always be preferred. However, I digress.
Here is a snippet of some example code taken from: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/
var myModule = angular.module('myModule', []);
// From this point on, we'll attach everything to 'myModule'
myModule.factory('HelloWorld', function($timeout) {
var getMessages = function(callback) {
$timeout(function() {
callback(['Hello', 'world!']);
}, 2000);
};
return {
getMessages: getMessages
};
});
I see this wrapping of code in timeout blocks everywhere particularly related to asynchronous calls. But can someone explain why this is needed? Why not just change the code above to:
var myModule = angular.module('myModule', []);
// From this point on, we'll attach everything to 'myModule'
myModule.factory('HelloWorld', function() {
var getMessages = function(callback) {
callback(['Hello', 'world!']);
};
return {
getMessages: getMessages
};
});
Why wouldn't the code snippet above work just fine?
The use of $timeout or $interval is to implicitly trigger a digest cycle. The process is as follows:
Execute each task in the callback function
Call $apply after each task is executed
$apply triggers a digest cycle
An alternative is to inject $rootScope and call $rootScope.$digest() if you are using services that don't trigger a $digest cycle.
Angular uses a dirty-checking digest mechanism to monitor and update values of the scope during
the processing of your application. The digest works by checking all the values that are being
watched against their previous value and running any watch handlers that have been defined for those
values that have changed.
This digest mechanism is triggered by calling $digest on a scope object. Normally you do not need
to trigger a digest manually, because every external action that can trigger changes in your
application, such as mouse events, timeouts or server responses, wrap the Angular application code
in a block of code that will run $digest when the code completes.
References
AngularJS source: intervalSpec.js
AngularJS source: timeoutSpec.js
$q deferred.resolve() works only after $timeout.flush()
AngularJS Documentation for inprog | Digest Phases
The $timeout in your example is probably used just to simulate an async function, like $http.get. As to why $timeout and not setTimeout: $timeout automatically tells angular to update the model, without the need to call $scope.$apply()
Also, consider the following example:
$scope.func = function(){
$scope.showSomeLoadingThing = true;
//Do some long-running stuff
$scope.showSomeLoadingThing = false;
}
No loading thingy will be shown, you would have to write it like this:
$scope.func = function(){
$scope.showSomeLoadingThing = true;
$timeout(function(){
//Do some long-running stuff
$scope.showSomeLoadingThing = false;
});
}
Reading this excellent book, Mastering Web Development in AngularJS, I ran across this code:
var Restaurant = function ($q, $rootScope) {
var currentOrder;
this.takeOrder = function (orderedItems) {
currentOrder = {
deferred:$q.defer(),
items:orderedItems
};
return currentOrder.deferred.promise;
};
this.deliverOrder = function() {
currentOrder.deferred.resolve(currentOrder.items);
$rootScope.$digest();
};
this.problemWithOrder = function(reason) {
currentOrder.deferred.reject(reason);
$rootScope.$digest();
};
My understanding is that the $rootScope.$digest(); calls are made in order to alert Angular that the Promise's state has been updated.
Is my understanding correct? Also, is it necessary to make the above $rootScope.$digest(); calls?
$scope.$digest() is what processes all of the $watch events that are on the current and children scope. It essentially manually tells the scope to check if a scope variable has changed. You don't generally want to use this when you are inside of a controller or a directive, because the $scope.$apply() function calls the $digest anyway and it is called when you mutate a scope variable.
Checkout this link for an example.
You don't need a $rootScope.$digest here because resolving/rejecting the promise will fire a $rootScope.$digest internally, as $interval,$timeout,and $http (after request finished) do that for you. And this $digest can throw errors of $digest already in progress.