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.
Related
I have an angular controller:
.controller('DashCtrl', function($scope, Auth) {
$scope.login = function() {
Auth.login().then(function(result) {
$scope.userInfo = result;
});
};
});
Which is using a service I created:
.service('Auth', function($window) {
var authContext = $window.Microsoft.ADAL.AuthenticationContext(...);
this.login = function() {
return authContext.acquireTokenAsync(...)
.then(function(authResult) {
return authResult.userInfo;
});
};
});
The Auth service is using a Cordova plugin which would be outside of the angular world. I guess I am not clear when you need to use a $scope.$apply to update your $scope and when you don't. My incorrect assumption was since I had wrapped the logic into an angular service then I wouldn't need it in this instance, but nothing gets updated unless I wrap the $scope.userInfo = statement in a $timeout or $scope.$apply.
Why is it necessary in this case?
From angular's wiki:
AngularJS provides wrappers for common native JS async behaviors:
...
jQuery.ajax() => $http
This is just a traditional async function with a $scope.$apply()
called at the end, to tell AngularJS that an asynchronous event just
occurred.
So i guess since your Auth service does not use angular's $http, $scope.$apply() isn't called by angular after executing the Async Auth function.
Whenever possible, use AngularJS services instead of native. If you're
creating an AngularJS service (such as for sockets) it should have a
$scope.$apply() anywhere it fires a callback.
EDIT:
In your case, you should trigger the digest cycle once the model is updated by wrapping (as you did):
Auth.login().then(function(result) {
$scope.$apply(function(){
$scope.userInfo = result;
});
});
Or
Auth.login().then(function(result) {
$scope.userInfo = result;
$scope.$apply();
});
Angular does not know that $scope.userInfo was modified, so the digest cycle needs to be executed via the use of $scope.$apply to apply the changes to $scope.
Yes, $timeout will also trigger the digest cycle. It is simply the Angular version of setTimeout that will execute $scope.$apply after the wrapped code has been run.
In your case, $scope.$apply() would suffice.
NB: $timeout also has exception handling and returns a promise.
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 have been working with the excelent ngStorage plugin for angular.
When setting it up you can declare a $scope-node connected to the localstorage like this:
$scope.$store = $localStorage;
$scope.$store is now accessible in all controllers etc.
I want to remove some stuff from localstorage and access it using broadcast instead.
In my init I performed:
$scope.taskarr = [];
$rootScope.$broadcast('taskarrbroad',$scope.taskarr);
What is required in order to add, remove and $watch this array, none of the mentioned seem to work.
Here, nothing happens
controller('textController', function($scope,$routeParams){
$scope.$watch('taskarrbroad.length', function(){
console.log($scope.taskarr.map(function(task){
return task.content;
}).join('\n'));
})
})
Here I can access $scope.taskarr and update it, but the view isn't updated. $scope.$apply() didn't help either (the timeout is because it's already within a digest.
controller('stateSwitchController', function($scope, $routeParams, $timeout){
$scope.taskarr = $scope.$store[$routeParams.state].taskarr || [];
console.log($scope.taskarr);
$timeout(function() {
$scope.$apply();
})
}).
$broadcast is a way to send events to other parts of your application. When you broadcast an event, someone else has to listen to that even with $on(). Something like:
// Some controller
$rootScope.$broadcast('my-event', eventData);
// Some other controller
$scope.$on('my-event', function() {
console.log('my-event fired!')
});
$watch is something else, it's not an event listener per se, it's a way to attach a function that gets called when that value changes, and that value has to be on the scope. So your watch should look like this:
$scope.$watch('taskarr.length', function(){
});
Since you've named the array taskarr on the scope.
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.