Within an angular controller I am attaching to a websocket service. When the controllers scope is destroyed I obviously want to remove the subscription.
Is it safe to pass the current scope to my service subscription function so it can auto remove on scope destroy? If I dont then each controller who attaches to a socket listener has to also remember to clean up.
Basically is it safe to pass current $scope to a service function or is there a better way of doing this?
I had similar need in my project. Below is the object returned in a AngularJS factory (which initializes WebSocket). The onmessage method automatically unsubscribes a callback if you pass in its associated scope in the second argument.
io =
onmessage: (callback, scope) ->
listeners.push callback
if scope then scope.$on "$destroy", => #offmessage callback
offmessage: (callback) -> listeners.remove callback
The JavaScript equivalence is below.
var io = {
onmessage: function(callback, scope) {
var _this = this;
listeners.push(callback);
if (scope) {
scope.$on("$destroy", function() {
_this.offmessage(callback);
});
}
},
offmessage: function(callback) {
listeners.remove(callback);
}
};
I would not pass the scope. Instead, I would explicitly, in your controller, hook up the unsubscribe.
From http://odetocode.com/blogs/scott/archive/2013/07/16/angularjs-listening-for-destroy.aspx :
$scope.$on("$destroy", function() {
if (timer) {
$timeout.cancel(timer);
}
});
I think having this done explicitly is not as magical, and easier to follow the logic. I think the service would be doing too much if it were to also unsubscribe. What if a controller wants to unsubscribe early?
However, if you do have a very specific use case that's used everywhere, it would be fine to pass the scope in. The amount of time the service needs the scope is very small, basically when the controller first executes so that the service can listen to the $destroy event.
Related
I'm having an issue where old controllers seem to stay active, even after using ui-router's $state.go function to go to a different state with another controller.
I've tested this by adding an interval to each controller, logging it's name to the console. After changing the state multiple times, all the previously visited controllers stay active:
$interval(function () {
console.info("ACCOUNT")
}, 1000)
Based on the official documentation available here, intervals created using $interval are not automatically destroyed when the controller's scope is destroyed.
Note: Intervals created by this service must be explicitly destroyed
when you are finished with them. In particular they are not
automatically destroyed when a controller's scope or a directive's
element are destroyed. You should take this into consideration and
make sure to always cancel the interval at the appropriate moment.
It is a good practice to listen to the scope's $destroy event and destroy all the intervals that were created in the controller.
Here is a good way to do it.
var intervalRef;
$scope.someFunction = function () {
// Save a reference to the interval's promise so that it can be canceled later
intervalRef = $interval(function () {
console.info("ACCOUNT")
}, 1000);
}
$scope.$on("$destroy", function () {
// When the scope of the controller is destroyed, cancel the interval
if (intervalRef) {
$interval.cancel(intervalRef);
}
});
I have one controller.
app.controller('first',['$scope','scopeService', function ($scope,scopeService){
$scope.initialize = function()
{
scopeService.store('value', $scope);
}
}]);
My second controller is
app.controller('second',['$scope','scopeService', function ($scope,scopeService){
$scope.initialize = function()
{
scopeService.get('value', $scope);
}
}]);
But my second controller is loaded before first so i am getting value as undefined..
You can pass data between the controller in two different ways. One way is to use a service to get and store data. Then both controllers can get the data from the service itself. Services are singleton so if it stores data once in its variable then another controller can get is as well.
Another way is to use Angular events. You can emit an event from your second controller and have the first controller listen for the event.
Example code, emit event:
$scope.$emit('event-name', {data: someDate});
Then receive the event using $rootScope:
$rootScope.$on('event-name', function (event, data) {
//do something with data
});
In your case, you should emit the event when your controller receives the data. Then the first controller listening to this event will get the data as well.
Imagine I have a controller which handles, for example, view changes:
function Controller($scope){
var viewModel = this;
viewModel.goBack= function(){
viewModel.visible = visibleLinks.pop(); //get last visible link
viewModel.swipeDirection = 'left';// for view change animation
}
}
But I want to handle it not only for example with HTML buttons inside <body>, but also with Back button on device. So I have to add Event Listener for deviceready event, and also explicit call $scope.$apply() in order to fact, that it is called outside of AngularJS context, like this:
document.addEventListener("deviceready", function(){
document.addEventListener("backbutton", function(){
viewModel.goBack();
$scope.$apply();
}, false);
}, false);
}
But I also want to follow (relatively :) ) new controllerAssyntax, cause this is recommended now e.g. by Todd Motto: Opinionated AngularJS styleguide for teams and it allows to remove $scope from controllers when things like $emit or $on are not used. But I can't do it, case I have to call $apply() cause my context is not Angular context when user clicks on device back button. I thought about creating a Service which can be wrapper facade for cordova and inject $scope to this service but as I read here: Injecting $scope into an angular service function() it is not possible. I saw this: Angular JS & Phonegap back button event and accepted solution also contains $apply() which makes $scope unremovable. Anybody knows a solution to remove Cordova specific events outside Angular controller, in order to remove $scope from controllers when not explicity needed? Thank you in advance.
I don't see a reason why to remove the $scope from the controller. It is fine to follow the best practice and to remove it if not needed, but as you said you still need it for $emit, $on, $watch.. and you can add it $apply() in the list for sure.
What I can suggest here as an alternative solution is to implement a helper function that will handle that. We can place it in a service and use $rootScope service which is injectable.
app.factory('utilService', function ($rootScope) {
return {
justApply: function () {
$rootScope.$apply();
},
createNgAware: function (fnCallback) {
return function () {
fnCallback.apply(this, arguments);
$rootScope.$apply();
};
}
};
});
// use it
app.controller('SampleCtrl', function(utilService) {
var backBtnHandler1 = function () {
viewModel.goBack();
utilService.justApply(); // instead of $scope.$apply();
}
// or
var backBtnHandler2 = utilService.createNgAware(function(){
viewModel.goBack();
});
document.addEventListener("backbutton", backBtnHandler2, false);
});
In my case I was simply forwarding Cordova events with the help of Angular $broadcast firing it on the $rootScope. Basically any application controller would then receive this custom event. Listeners are attached on the configuration phase - in the run block, before any controller gets initialized. Here is an example:
angular
.module('app', [])
.run(function ($rootScope, $document) {
$document.on('backbutton', function (e) {
// block original system back button behavior for the entire application
e.preventDefault();
e.stopPropagation();
// forward the event
$rootScope.$broadcast('SYSTEM_BACKBUTTON', e);
});
})
.controller('AppCtrl', function ($scope) {
$scope.$on('SYSTEM_BACKBUTTON', function () {
// do stuff
viewModel.goBack();
});
});
Obviously in the $scope.$on handler you do not have to call $scope.$apply().
Pros of this solution are:
you'll be able to modify an event or do something else for the entire application before the event will be broadcasted to all the controllers;
when you use $document.on() every time controller is instantiated, the event handler stays in the memory unless you manually unsibscribe from this event; using $scope.$on cares about it automatically;
if the way a system dispatches Cordova event changes, you'll have to change it in one place
Cons:
you'll have to be careful when inheriting controllers which already have an event handler attached on initialization phase, and if you want your own handler in a child.
Where to place the listeners and the forwarder is up to you and it highly depends on your application structure. If your app allows you could even keep all the logic for the backbutton event in the run block and get rid of it in controllers. Another way to organize it is to specify a single global callback attached to $rootScope for example, which can be overriden inside controllers, if they have different behavior for the back button, not to mess with events.
I am not sure about deviceready event though, it fires once in the very beginning. In my case I was first waiting for the deviceready event to fire and then was manually bootstrapping AngularJS application to provide a sequential load of the app and prevent any conflicts:
document.addEventListener('deviceready', function onDeviceReady() {
angular.element(document).ready(function () {
angular.bootstrap(document.body, ['app']);
});
}, false);
From my point of view the logic of the app and how you bootstrap it should be separated from each other. That's why I've moved listener for backbutton to a run block.
I am facing a race condition. One of the service is changing the view using ui-router and then broadcasting event from $rootscope. The scope of the controller(tied to the changed view) is listening on this event, but whats happening is when the view is changed my controller scope is reinitialized and till the time it is ready the event has already been dispatched.
So the new controller scope never listens for the event.
Please provide some way to handle this situation.
What you can do is, Instead of listening for event in controller, listen for the event in a service. Since, service are bound to have only a single instance, you won't face such issue in service.
And, as for as the controller, you can first check for the presence of a flag in the service, then retrieve the updated value.
Consider this sample code taken from here.
app.factory('userService', ['$rootScope', function ($rootScope) {
var service = {
model: {
name: '',
email: ''
},
SaveState: function () {
sessionStorage.userService = angular.toJson(service.model);
},
RestoreState: function () {
service.model = angular.fromJson(sessionStorage.userService);
}
}
$rootScope.$on("savestate", service.SaveState);
$rootScope.$on("restorestate", service.RestoreState);
return service;
}]);
I am playing with Angular and SignalR, I have tried to create a service which will act as a manager.
dashboard.factory('notificationsHub', function ($scope) {
var connection;
var proxy;
var initialize = function () {
connection = $.hubConnection();
proxy = connection.createHubProxy('notification');
proxy.on('numberOfIncidents', function (numOfIncident) {
console.log(numOfIncident);
$scope.$emit('numberOfIncidents', numOfIncident);
});
connection.start()
.done(function() {
console.log('Connected');
})
.fail(function() { console.log('Failed to connect Connected'); });
};
return {
initialize: initialize
};
});
however I get the error Error: Unknown provider: $scopeProvider <- $scope <- notificationsHub.
How can I use pubsub to pass all the notifications to the controllers? jQuery maybe?
$scope does not exist in this context as that's something injected when a controller is created and a new child scope is made. However, $rootScope is available at the time you need.
Also, be aware $emit() goes upward and your controller scopes wont see it. You would either need to switch to $broadcast() so the event goes downwards or inject $rootScope as well to the controllers you want to be able to subscribe to 'numberOfIncidents'
Check out the angular docs and a useful wiki on scopes.
Here is a great example showing how to wrap the proxy in a service and use $rootScope for event pub/sub.
http://sravi-kiran.blogspot.com/2013/09/ABetterWayOfUsingAspNetSignalRWithAngularJs.html
As already noted in johlrich's answer, $scope is not avaliable inside proxy.on. However, just switching to $rootScope will most likely not work. The reason for this is because the event handlers regisrered with proxy.on are called by code outside the angular framework, and thus angular will not detect changes to variables. The same applies to $rootScope.$on event handlers that are triggered by events broadcasted from the SignalR event handlers. See https://docs.angularjs.org/error/$rootScope/inprog for some more details.
Thus you want to call $rootScope.$apply() from the SignalR event handler, either explicitly
proxy.on('numberOfIncidents', function (numOfIncident) {
console.log(numOfIncident);
$scope.$apply(function () {
$rootScope.$emit('numberOfIncidents', numOfIncident);
});
});
or possibly implicitly through $timeout
proxy.on('numberOfIncidents', function (numOfIncident) {
console.log(numOfIncident);
$timeout(function () {
$rootScope.$emit('numberOfIncidents', numOfIncident);
}, 0);
});
I tried to use $apply() after changing value, i tried to use $apply(functuin() {value = 3}), and also i tried to use $emit and $broadcast for changing value and it doesn't help.
But i found solution we need in html after in controller you can use
var scope2 = angular.element("#test").scope();
scope2.point.WarmData.push(result);
$scope.$apply();
P.s. I understand that it is very old question, but may by smb, as i, need this solution.