I would like to notify multiple controllers of external changes from a service.
I know that i can do this by using a deferred object and calling notify on it and registering a callback on that.
e.g
// in service
$timeout(function() {
defered.notify('In progress')}
, 0)
//in controller
var promise = myService.promise
promise.then(function(success) {
console.log("success");
}, function(error) {
console.log("error");
}, function(update) {
console.log("got an update!");
}) ;
Is there a way to remove my notify callback when the controller is destroyed?
You can use $on, $broadcast and $emit to achieve a similar behavior.
Just $broadcast(name, args); an event and register a listener $on(name, listener);.
So when you got your data you just broadcast to everybody that you've got it.
$broadcast('gotTheData', actualDataObject);
Then in your controllers
$on('gotTheData', function(event, actualDataObject)
{
... update controller data ...
});
Note that in the example I've added only one actualDataObject, but you can pass multiple arguments from the $broadcast to $on. For more details refer to the documentation (see link above).
As per the comments to #Scott's answer you highlighted one important aspect: you need to unregister the $on when you destroy the controller through the "Returns a deregistration function for this listener." (see the docs) at $on returns. There is a question that targets specifically this issue: How can I unregister a broadcast event to rootscope in AngularJS?.
EDIT DUE TO FURTHER QUESTION DETAILS
You could cancel the $timeout when you destroy the controller:
Create the $timeout as so...
$scope.myTimeout = $timeout(function() {
defered.notify('In progress')}
, 0);
In the controller add this:
$scope.$on('$destroy', function () {
$timeout.cancel($scope.myTimeout);
});
In my experience this appears to clean everything up.
To clear just the notify callback itself, use the suggested $broadcast, $emit(see #Pio's answer) A guide to the difference between them is here.
Its important to know the difference!
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'm broadcasting an event from my navbar controller to another controller, but if I initialize the controller multiple times (when I'm going front and back through the application) the function that executes on my $on event runs multiple times because it's registered multiple times.
$rootScope.$on('submitBookingDialog', function(){
submitBookingDialog();
});
How can I prevent the submitBookingDialog() to happen more than once?
I found a solution, but I don't know if it's ideal.
First of all, do you need to send the event on the $rootScope? If not, then you could just register your event handler on the $scope. The event handler will be destroyed whenever your controller scope is destroyed. You would then send the event via $scope.$emit or $scope.$broadcast depending on your controller hierarchy.
That being said, all you need to do to destroy your event listener is call the deregistration function that is returned when registering the listener:
var offSubmitBookingDialog = $rootScope.$on('submitBookingDialog', function(){
submitBookingDialog();
});
$scope.$on('$destroy', function() {
// Call the deregistration function when the scope is destroyed
offSubmitBookingDialog();
});
This seems to do it for me:
var removeListener = $rootScope.$on('submitBookingDialog', function(){
submitBookingDialog();
// Remove listener
removeListener();
});
For posterity I ended up doing this:
.run(function($rootScope) {
$rootScope.once = function(e, func) {
var unhook = this.$on(e, function() {
unhook();
func.apply(this, arguments);
});
};
})
Because I kept needing to do this in a few places this just ended up being cleaner.
With that on your app module you can now just call once instead of $on:
$rootScope.once('submitBookingDialog', function() {
submitBookingDialog();
});
I ran into a similar situation, so I wrote a small library to make pub/sub stuff easier.
https://github.com/callmehiphop/hey
Maybe you shoud unsubscribe on controller destroy event
var removeSubmitBookingDialog = $rootScope.$on('submitBookingDialog',submitBookingDialog);
$scope.$on("$destroy", removeSubmitBookingDialog);
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;
});
}
I could not find any other question/answer that met my needs, so here it is:
In an AngularJS (1.2.14) controller, I have an event listener that executes an ajax call to fetch some data when an event ('fetchData') is heard. After the fetch is successful, an event (called 'fetchSuccess') is broadcasted, that a directive is listening for (though that part is irrelevant).
$scope.$on('fetchData', function () {
$scope
.submitSearch()
.then(function (results) {
$scope.$broadcast('fetchSuccess');
}, function () {
$scope.$broadcast('fetchError');
});
});
So, in my test I want to do something like this (assume that 'this.scope' is a new $scope object on the controller in the test suite):
it('should broadcast "fetchSuccess"', inject(function ($rootScope) {
var scope = this.scope,
spy = chai.spy(scope, '$broadcast');
// trigger the $broadcast event that calls the fetch method
scope.$broadcast('fetchData');
$rootScope.$apply();
expect(scope.$broadcast).to.be.called.with('fetchSuccess');
}));
But I am not clear on how to listen for the $broadcast event in an assertion. I keep getting this error: AssertionError: expected function (name, args) {...}
Just to be clear, my issue is not with the functionality of the event broadcaster or the listeners during runtime; the application works as expected. The problem is with listening to the events in the test suite.
Note that the above code is just the necessary snippet that is needed for this question. In my application, there are other variables/methods that get set/called and those things test out correctly. Meaning that if I test to see if the actual fetching method gets called, or if a particular variable is being set appropriately, those tests pass.
I have tried mixing and matching the scope variables and even listening for the $broadcast via scope.$on('fetchSuccess', fn) but nothing seems to work. The fetchSuccess event doesn't seem to get emitted, or I'm not listening for it properly.
Thanks in advance,
Ryan.
So, I have found the answer to my question and it was all my fault. Though I did have to modify the way the test was written, the core problem as simple as listening for the wrong event!
But, for those that want to know what my test looked like, here is the final test (rootscope is being set to $rootScope elsewhere):
it('should broadcast a "fetchSuccess" event', function (done) {
var eventEmitted = false;
this.scope.$on('fetchSuccess', function () {
eventEmitted = true;
done();
});
this.scope.$broadcast('fetchData');
rootscope.$apply();
eventEmitted.should.be.true;
});
There was no need to spy on the $broadcast event, the AngularJS $on listener is sufficient.
I have a service that make
$rootScope.$broadcast('myEvent', somedata)
from time to time. In controller I do
$scope.$on('myEvent', function (evt, somedata) { $scope.data = somedata })
The question is if I omit
if (!$scope.$$phase) { $scope.$apply(); }
in controller's event listener, then view won't change. Why is that? Is there any better way to do it?.
It is because $apply is a lazy worker and will do the job only there is enough stuff to refresh. You can't control when, unless you explicitly call $scope.$apply.
Yes, there is a better way to do this : call safeApply. Because calling explicitly many $apply can cause conflicts. There is no official safeApply implementation, so can choose you poison :
here : https://coderwall.com/p/ngisma
or there : AngularJS : Prevent error $digest already in progress when calling $scope.$apply()