I am interested if any of you run into this issue and how you solved it. I have a page that has it's controller and in that page, each tab has it's own controller. Depending on how I access the page, via navigation or direct url link, I have issue that tab controllers are not yet ready, by the time outer controller raises event. My solution was to raise "ControllerCreated" event, passing instance of the controller as the event arg. from each of the tab controllers. Outer controller listens for these and invokes on them "load" operation. Is there other built in way to synch this without having to raise custom event?
Event Recorder Service
One approach is to store outer events in a service and have child controllers use the service to "catch-up" and subscribe to the events.
app.service("eventRecorder", function() {
var lastEvent;
var subscriberIdNum = 0;
var subscriberList = {};
this.record = function evRecorder (eventValue) {
lastEvent = eventValue;
};
this.subscribe = function evSubscribe(callbackFn) {
//callback for subscriber to "catch-up"
if (lastEvent) { callbackFn(lastEvent) };
//save subscriber
subscriberIdNum++
var subscriberIdString = 'id'+subscriberIdNum;
subscriberList[subscriberIdString] = callbackFn;
//return unSubscribe function
return function unSubscribe() {
delete subscriberList[subscriberIdString];
};
};
this.notify = function evNotify( eventValue ) {
this.record(eventValue);
angular.forEach(subscriberList, function (cb) {
cb(eventValue);
});
};
});
Client Controller Example
app.controller("client", function($scope, eventRecorder) {
var vm = $scope;
var unSubscribe = eventRecorder.subscribe(function(value) {
//callback executes immediately to "catch-up"
vm.eventValue = value;
});
$scope.$on('$destroy', unSubscribe);
});
Parent Controller Usage
app.controller("parent", function($scope, eventRecorder) {
var vm = $scope;
vm.onEvent(value) {
eventRecorder.notify(value);
});
});
In order to prevent memory leaks, controllers should unsubscibe on destruction of the scope.
This example shows an event recorder service that provides only one feed. It can be generalized to handle more that one feed.
UPDATE: Automatic Unsubscribe
"In order to prevent memory leaks, controllers should unsubscibe on destruction of the scope." I know what you mean, we had similar notification service, but we pulled it out, because somebody would always forget to unsubscribe.
One could require scope as a subscribe function argument and implement automatic unsubsciption.
this.subscribe = function evSubscribe(scope, callbackFn) {
//callback for subscriber to "catch-up"
if (lastEvent) { callbackFn(lastEvent) };
//save subscriber
subscriberIdNum++
var subscriberIdString = 'id'+subscriberIdNum;
subscriberList[subscriberIdString] = callbackFn;
var unSubscribe = function evUnsubscribe() {
delete subscriberList[subscriberIdString];
};
scope.$on("$destroy", unSubscribe);
return unSubscribe;
};
Related
In my AngularJS application, I have a controller-A and a factory. I am using the following code in factory to call the function in controller-A. In the initial call, the function in controller A's function executes 1 time; on the next call the controller-A's function executes 2 times. Hence the number of times executed get increased for each call. Is it possible to avoid this, please advise me. I have added the factory code and controller-A code below:
Factory code:
updateUserData: function (value, action) {
$("#myModalInsertUser").modal('hide');
var id = value.Id;
var params = {};
params.id = depotId;
$rootScope.selectedId = params;
$rootScope.$emit("EVENT_1", {id});
});
Controller-A code:
var listener = $rootScope.$on("EVENT_1", function(event, params, reload) {
$scope.confirmUserInfo(params);
});
$scope.confirmUserInfo = function(params) {
$('#myModalConfirmUser').modal('show');
$('#closeConfirmUser').unbind('click').click(function () {
$('#myModalConfirmUser').modal('hide');
var params = $rootScope.selectedId;
$scope.getUsers(params);
$scope.$on('$destroy', listener);
});
}
Attach the event listener to $scope and it will be automatically destroyed when the scope is destroyed:
̶v̶a̶r̶ ̶l̶i̶s̶t̶e̶n̶e̶r̶ ̶=̶ ̶$̶r̶o̶o̶t̶S̶c̶o̶p̶e̶.̶$̶o̶n̶(̶"̶E̶V̶E̶N̶T̶_̶1̶"̶,̶ ̶f̶u̶n̶c̶t̶i̶o̶n̶(̶e̶v̶e̶n̶t̶,̶ ̶p̶a̶r̶a̶m̶s̶,̶ ̶r̶e̶l̶o̶a̶d̶)̶ ̶{̶
var deregisterFn = $scope.$on("EVENT_1", function(event, params, reload) {
$scope.confirmUserInfo(params);
});
$scope.confirmUserInfo = function(params) {
$('#myModalConfirmUser').modal('show');
$('#closeConfirmUser').unbind('click').click(function () {
$('#myModalConfirmUser').modal('hide');
var params = $rootScope.selectedId;
$scope.getUsers(params);
̶$̶s̶c̶o̶p̶e̶.̶$̶o̶n̶(̶'̶$̶d̶e̶s̶t̶r̶o̶y̶'̶,̶ ̶l̶i̶s̶t̶e̶n̶e̶r̶)̶;̶
});
}
The recommended practice is to broadcast events from $rootScope and receive them on the $scope interested in the event.
is it possible to destroy the listener before the scope gets destroyed?
To remove the listener, simply invoke the de-register function:
deregisterFn();
I have an AngularJs app with a master details that I have changed to use
ui-router.
Previously, I had a SelectionService with a plain JavaScript observer that was
used by the master to notify selection and by the details to update himself. The
selection was just a technical identifier, so that the DetailsController has
to get the item from my BackendService which is basically a local cache.
Now with ui-router when an item is selected into the master, I go the details
state and the same flow remains (use the technical id to get details from backend).
My problem is that into the previous version all updates made on the details
where automagically updated on the master. But that is broken with the ui-router
version, probably because the MasterController and DetailsController don't
share the same scope.
So, my question is : How do you ensure that a list of items is updated when one
item is changed. Do you rely on some AngularJs functionalities (then how) or do
you use a classic events mechanism with $scope.$broadcast and $scope.$on ?
Edit, after more investigations
I have read some articles that are clearly against the usage of AngularJs events ($scope.$broadcast, $scope.$emit and $scope.$on) and recommand a custom event bus/aggregator.
But I would like to avoid that and thus rely on the AngularJs digest lifecycle. However what is suggest by #Kashif ali below is what I have but my master is not updated when the details changes.
angular
.module('masterDetails')
.service('BackendService', function($q){
var cache = undefined;
var Service = {}
Service.GetImages = function() {
var deferred = $q.defer();
var promise = deferred.promise;
if ( !cache ) {
// HTTP GET from server .then(function(response){
cache = JSON.parse(response.data);
deferred.resolve(cache);
});
} else {
deferred.resolve(cache);
}
return promise;
}
Service.GetImage = function(key) {
return GetImages().then(function(images){
return images[key];
});
}
Service.SetImage = function(key, updated) {
GetImages().then(function(images){
images[key] = updated;
// TODO; HTTP PUT to server
});
}
})
.controller('MasterController', function(BackendService){
var vm = this;
vm.items = [];
vm.onSelect = onSelect;
init();
function init() {
BackendService.GetImages().then(function(images){
// images is a map of { key:String -> Image:Object }
vm.items = Object.keys(images).map(function(key){
return images[key];
});
});
}
function onSelect(image) {
$state.go('state.to.details', {key: image.key});
}
})
.controller('DetailsController', function(BackendService, $stateParams){
var vm = this;
vm.image = undefined;
init();
function init() {
BackendService.GetImage($stateParams.key).then(function(image){
vm.image = image;
}).then(function(){
// required to trigger update on the BackendService
$scope.$watch('[vm.image.title, vm.image.tags]', function(newVal, oldVal){
BackendService.SetImage(vm.image.key, vm.image);
}, true);
});
}
});
Edit, this is due to the states
So, when I open the app on #/images the images state start. Then I select one image to go to the images.edit state and everything works well, the master is updated when details changes.
However if I start on #/images/:key which is the images.edit state, then the master ignore all changes mades on the master.
You can rely on both the solution you have mentioned
1.You can achieve this using factories in angularjs
Factories/services are the singleton objects that is shared along the the app:
example:
angular.module("app",[]).factory('myfactory',function(){
var data;
{
getData:getData,
setData:setData
};
function setData(data1)
{
data=data1;
}
function getData()
{
return data;
}
}
).controller('myclrl1',function($scope,myfactory){
}).controller('myclrl2',function($scope,myfactory){
});
you can inject these controller in different views and can access singleton factory(all controller will share the same object) "myfactory" in both controller using getter and setter
you can use $scope.$broadcast and $scope.$on to make nested contollers to communicate with each other
you can find the detailed Example over here.
$scope.$broadcast and $scope.$on
hope that would be helpful
Regards
I am using socket.io to enable chat in my app and i am using a service SocketService to perform all the socket stuff. When a message came then i want to trigger a function of a controller from the service SocketService to make some changes in the UI.
So i want to know that how can i access the function of a controller from the service.
Sample Code:
.service('SocketService', function ($http,$rootScope,$q) {
this.connect = function(){
var socket = io();
socket.on('connect',function(){
// Call a function named 'someFunction' in controller 'ChatController'
});
}
});
This is the sample code for service.
Now the code for controller
.controller('ChatController',function('SocketService',$scope){
$scope.someFunction = function(){
// Some Code Here
}
});
You could achieve this by using angular events $broadcast or $emit.
In your case $broadcast would be helpful,
You need to broadcast your event in $rootscope that can be listen by all the child scopes which has $on with same event name.
CODE
.service('SocketService', function($http, $rootScope, $q) {
this.connect = function() {
var socket = io();
socket.on('connect', function() {
// Call a function named 'someFunction' in controller 'ChatController'
$rootScope.$broadcast('eventFired', {
data: 'something'
});
});
}
});
.controller('ChatController', function('SocketService', $scope) {
$scope.someFunction = function() {
// Some Code Here
}
$scope.$on('eventFired', function(event, data) {
$scope.someFunction();
})
});
Hope this could help you, Thanks.
I know this is an old question, but I have another option. I have a personal bias against $broadcast - it just doesn't feel very 'angularish', I prefer making explicit calls in my code.
So instead of broadcasting to the controller and triggering another digest cycle, I prefer to have the controller register itself to the service, as below. Just be careful not to introduce any circular dependencies if the controller makes use of the same service. This works best with the controllerAs syntax, so that the calling service does not need to care about $scope.
Yes, this is more code than $broadcast, but it does give the service total access to the entire controller - all of it's methods and properties.
.service('SocketService', function ($http,$rootScope,$q) {
var _this = this;
this.chatController = null;
this.registerCtrlr = function (ctrlr) {
_this.chatController = ctrlr;
};
this.unRegisterCtrlr = function () {
_this.chatController = null;
};
this.connect = function(){
var socket = io();
socket.on('connect',function(){
// Call chatController.someFunction if chatController exists
if (_this.chatController) {
_this.chatController.someFunction();
}
});
};
});
.controller('ChatController',['SocketService', '$scope', function(SocketService, $scope){
SocketService.registerCtrlr(this);
//-- make sure controller unregisters itself when destroyed - need $scope for this
$scope.$on('$destroy', function () {
SocketService.unRegisterCtrlr();
});
this.someFunction = function(){
// Some Code Here
}
}]);
I realize this post is old but I'd like to give my two cents after dealing with Angular JS for several years. I personally would reconsider this approach. Ideally with AngularJS you'd modify your controller/directive to facilitate transferring data to the view model and ultimately bind an HTML template to what I call "the user friendly" view model. This view model should simply reflect what you want the user to see and when in general. Using this method the moment connect event happens your view model which should be bound to the service's data will reflect changes to the data the moment the data arrives.
I'm building an app in angularjs, where I have a central notification queue. Any controller can push into the queue and digest the messages.
I have built a service like:
angular.module('app').factory('notificationSvc', ['translateSvc', notification]);
function notification(translate) {
var notificationQ = [];
var service = {
add: add,
getAll: getAll
};
return service;
function add(message, type) {
notificationQ.push({
message: message,
type: type
});
}
function getAll() {
return notificationQ;
}
}
(One of the problems with this is that the notificationQ can be modified unsafely by calling svc.getAll()[3].message = "I have changed a message"; or something similar. I originally wanted a "push only" service with immutable messages, but this problem is outside of the scope of this question.)
If I digest this queue in a controller like:
$scope.notifications = svc.getAll();
$scope.current= 0; // currently visible in the panel
And use it like:
<div ng-repeat="notification in notifications" ng-show="$index == current">
<p>{{notification.message}}</p>
</div>
I can bind to it, see it changing and all is well. I can cycle through past notifications by changing the variable current.
The question:
When the queue gets a new element I want the $scope.index variable to change to notifications.length - 1. How do I do that?
I have seen examples using $rootScope.$broadcast('notificationsChanged'); and $scope.$on('notificationsChanged', function() { $scope.index = $scope.notifications.length - 1; });, but I did not really like the pattern.
I have a controller that knows about the service, has a direct reference to it, and yet we use $rootScope to communicate? Everything else sees the $rootScope, and all the events from different services will clutter up there.
Can't I just put the event on the service instead? Something like this.$broadcast('notificationsChanged') in the service and svc.$on('notificationsChanged', function() { ... }); in the controller.
Or would it be cleaner to watch the data directly? If yes, how? I don't like this as I was not planning on exposing the full array directly (I was planning on get(index) methods) it just sort of happened along the lines where I had no idea what I was doing and was happy that at least something works.
You could just manage events yourself. For example (untested):
function EventManager() {
var subscribers = [];
var service = {
subscribe: subscribe;
unsubscribe: unsubscribe;
publish: publish
}
return service;
function subscribe(f) {
subscribers.push(f);
return function() { unsubscribe(f); };
}
function unsubscribe(f) {
var index = subscribers.indexOf(f);
if (index > -1)
subscribers.splice(index, 1);
}
function publish(e) {
for (var i = 0; i < subscribers.length; i++) {
subscribers[i](e);
}
}
}
function notification(translate) {
var notificationQ = [];
var addEvent = new EventManager();
var service = {
add: add,
getAll: getAll,
onAdded: addEvent.subscribe;
};
return service;
function add(message, type) {
var notification = {
message: message,
type: type
};
notificationQ.push(notification);
addEvent.publish(notification);
}
function getAll() {
return notificationQ;
}
}
Then, from your controller:
...
var unsubscribe = notificationSvc.onAdded(function(n) { /* update */ });
Caveat: using this method the service will maintain a reference to the subscriber function that is passed to it using subscribe, so you have to manage the subscription using $scope.$on('$destroy', unsubscribe)
The notification approach would definitely work. Depending on your implementation it would be the right solution.
Another approach would be to watch the notifications array in your controller, like this:
$scope.$watchCollection('notifications', function(newValue, oldValue) {
$scope.index = newValue.length - 1;
});
This should work, because your controller receives a direct reference to the notifications array and therefore can watch it directly for changes.
As runTarm pointed out in the comments, you could also directly $watch the length of the array. If you're only interested in length changes this would be a more memory saving approach (since you don't need to watch the whole collection):
$scope.$watch('notifications.length', function (newLength) {
$scope.index = newLength - 1;
});
I am new in angular and encounter a catch-22:
Facts:
I have a service that logs my stuff (my-logger).
I have replaced the $ExceptionHandler (of angular), with my own implementation which forwards uncaught exceptions to my-logger service
I have another service, pusher-service, that needs to be notified whenever a fatal message is to be logged somewhere in my application using 'my-logger'.
Problem:
I can't have 'my-logger' be depend on 'pusher' since it will create circular dependency (as 'pusher' uses $http. The circle: $ExceptionHandler -> my-logger -> pusher -> $http -> $ExceptionHandler...)
My attempts:
In order to make these 2 services communicate with each other, I wanted to use $watch on the pusher-service: watches a property on $rootscope that will be updated in my-logger.
But, when trying to consume $rootScope in 'my-logger', in order to update the property on which the 'pusher' "watches", I fail on circular dependency as it turns out that $rootscope depends on $ExceptionHandler (the circle: $ExceptionHandler -> my-logger -> $rootScope -> $ExceptionHandler).
Tried to find an option to get, at runtime, the scope object that in its context 'my-logger' service works. can't find such an option.
Can't use broadcast as well, as it requires my-logger to get access to the scope ($rootScope) and that is impossible as seen above.
My Question:
Is there an angular way to have two services communicate through a 3rd party entity ?
Any idea how this can be solved ?
Use a 3rd service that acts as a notification/pubsub service:
.factory('NotificationService', [function() {
var event1ServiceHandlers = [];
return {
// publish
event1Happened: function(some_data) {
angular.forEach(event1ServiceHandlers, function(handler) {
handler(some_data);
});
},
// subscribe
onEvent1: function(handler) {
event1ServiceHandlers.push(handler);
}
};
}])
Above, I only show one event/message type. Each additional event/message would need its own array, publish method, and subscribe method.
.factory('Service1', ['NotificationService',
function(NotificationService) {
// event1 handler
var event1Happened = function(some_data) {
console.log('S1', some_data);
// do something here
}
// subscribe to event1
NotificationService.onEvent1(event1Happened);
return {
someMethod: function() {
...
// publish event1
NotificationService.event1Happened(my_data);
},
};
}])
Service2 would be coded similarly to Service1.
Notice how $rootScope, $broadcast, and scopes are not used with this approach, because they are not needed with inter-service communication.
With the above implementation, services (once created) stay subscribed for the life of the app. You could add methods to handle unsubscribing.
In my current project, I use the same NotificationService to also handle pubsub for controller scopes. (See Updating "time ago" values in Angularjs and Momentjs if interested).
Yes, use events and listeners.
In your 'my-logger' you can broadcast an event when new log is captured:
$rootScope.$broadcast('new_log', log); // where log is an object containing information about the error.
and than listen for that event in your 'pusher':
$rootScope.$on('new_log', function(event, log) {... //
This way you don't need to have any dependencies.
I have partially succeeded to solve the case:
I have created the dependency between 'my-logger' and 'pusher' using the $injector.
I used $injector in 'my-logger' and injected at "runtime" (means right when it is about to be used and not at the declaration of the service) the pusher service upon fatal message arrival.
This worked well only when I have also injected at "runtime" the $http to the 'pusher' right before the sending is to happen.
My question is why it works with injector in "runtime" and not with the dependencies declared at the head of the service ?
I have only one guess:
its a matter of timing:
When service is injected at "runtime", if its already exists (means was already initialized else where) then there is no need to fetch and get all its dependencies and thus the circle is never discovered and never halts the execution.
Am I correct ?
This is an easy way to publish/subscribe to multiple events between services and controllers
.factory('$eventQueue', [function() {
var listeners = [];
return {
// publish
send: function(event_name, event_data) {
angular.forEach(listeners, function(handler) {
if (handler['event_name'] === event_name) {
handler['callback'](event_data);
}
});
},
// subscribe
onEvent: function(event_name,handler) {
listeners.push({'event_name': event_name, 'callback': handler});
}
};
}])
consumers and producers
.service('myService', [ '$eventQueue', function($eventQueue) {
return {
produce: function(somedata) {
$eventQueue.send('any string you like',data);
}
}
}])
.controller('myController', [ '$eventQueue', function($eventQueue) {
$eventQueue.onEvent('any string you like',function(data) {
console.log('got data event with', data);
}])
.service('meToo', [ '$eventQueue', function($eventQueue) {
$eventQueue.onEvent('any string you like',function(data) {
console.log('I also got data event with', data);
}])
You can make your own generic event publisher service, and inject it into each service.
Here's an example (I have not tested it but you get the idea):
.provider('myPublisher', function myPublisher($windowProvider) {
var listeners = {},
$window = $windowProvider.$get(),
self = this;
function fire(eventNames) {
var args = Array.prototype.slice.call(arguments, 1);
if(!angular.isString(eventNames)) {
throw new Error('myPublisher.on(): argument one must be a string.');
}
eventNames = eventNames.split(/ +/);
eventNames = eventNames.filter(function(v) {
return !!v;
});
angular.forEach(eventNames, function(eventName) {
var eventListeners = listeners[eventName];
if(eventListeners && eventListeners.length) {
angular.forEach(eventListeners, function(listener) {
$window.setTimeout(function() {
listener.apply(listener, args);
}, 1);
});
}
});
return self;
}
function on(eventNames, handler) {
if(!angular.isString(eventNames)) {
throw new Error('myPublisher.on(): argument one must be a string.');
}
if(!angular.isFunction(handler)) {
throw new Error('myPublisher.on(): argument two must be a function.');
}
eventNames = eventNames.split(/ +/);
eventNames = eventNames.filter(function(v) {
return !!v;
});
angular.forEach(eventNames, function(eventName) {
if(listeners[eventName]) {
listeners[eventName].push(handler);
}
else {
listeners[eventName] = [handler];
}
});
return self;
}
function off(eventNames, handler) {
if(!angular.isString(eventNames)) {
throw new Error('myPublisher.off(): argument one must be a string.');
}
if(!angular.isFunction(handler)) {
throw new Error('myPublisher.off(): argument two must be a function.');
}
eventNames = eventNames.split(/ +/);
eventNames = eventNames.filter(function(v) {
return !!v;
});
angular.forEach(eventNames, function(eventName) {
if(listeners[eventName]) {
var index = listeners[eventName].indexOf(handler);
if(index > -1) {
listeners[eventName].splice(index, 1);
}
}
});
return self;
}
this.fire = fire;
this.on = on;
this.off = off;
this.$get = function() {
return self;
};
});