Fire event from a service without "polluting" the $rootScope - angularjs

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;
});

Related

AngularJS 1.6.9 controller variable bound to service variable doesn't change

I have 2 components which are both accessing a service. One component delivers an object and the other one is supposed to display it or just receive it. The problem is that after the initialization process is finished the variable in the display component doesn't change.
I have tried using $scope , $scope.$apply(), this.$onChanges aswell as $scope.$watch to keep track of the variable, but it always stays the same.
This controller from the display component provides a text, which is from an input field, in an object.
app.controller("Test2Controller", function ($log, TestService) {
this.click = function () {
let that = this;
TestService.changeText({"text": that.text});
}
});
That is the the service, which gets the objekt and saves it into this.currentText.
app.service("TestService", function ($log) {
this.currentText = {};
this.changeText = function (obj) {
this.currentText = obj;
$log.debug(this.currentText);
};
this.getCurrentText = function () {
return this.currentText;
};
});
This is the controller which is supposed to then display the object, but even fails to update the this.text variable.
app.controller("TestController", function (TestService, $timeout, $log) {
let that = this;
this.$onInit = function () {
this.text = TestService.getCurrentText();
//debugging
this.update();
};
//debugging
this.update = function() {
$timeout(function () {
$log.debug(that.text);
that.update();
}, 1000);
}
//debugging
this.$onChanges = function (obj) {
$log.debug(obj);
}
});
I spent quite some time searching for an answer, but most are related to directives or didn't work in my case, such as one solution to put the object into another object. I figured that I could use $broadcast and $on but I have heard to avoid using it. The angular version I am using is: 1.6.9
I see a problem with your approach. You're trying to share the single reference of an object. You want to share object reference once and want to reflect it wherever it has been used. But as per changeText method, you're setting up new reference to currentText service property which is wrong.
Rather I'd suggest you just use single reference of an object throughout and it will take care of sharing object between multiple controllers.
Service
app.service("TestService", function ($log) {
var currentText = {}; // private variable
// Passing text property explicitly, and changing that property only
this.changeText = function (text) {
currentText.text = text; // updating property, not changing reference of an object
$log.debug(currentText);
};
this.getCurrentText = function () {
return currentText;
};
});
Now from changeText method just pass on text that needs to be changed to, not an new object.

problems with $watch function

I'm not really sure about this issue but it seems that sometimes when I activate $watch for a function then it doesn't work.
for example I have this simple service
angular.module('sp-app').factory('mediaSources', function() {
var storages = [];
return {
addStorage: function(storage) {
storages.push(storage);
},
getStorages: function() {
return storages;
}
}
});
and when I watch getStorage method in order to update my view it doesn't call change callback or calls only at initialization stage
$scope.$watch(function($scope) {
return mediaSources.getStorages();
}, function() {
console.log('call')
});
and I can only track changes by watching length property of returned array
return mediaSources.getStorages().length;
and I wonder because I have written similar think somewhere else within my application and it works fine.
If i interpret what you are trying to do, you should not need to set a watch on something like this, you can just use a factory like so :
angular.module('app').factory('mediaSources', function(){
var storages = {};
storages.list = [];
storages.add = function(message){
storages.list.push(message);
};
return storages;
});
then in the controller you want to receive/update the data to for instance, you would do
$scope.myControllerVar = mediaSources.list;
No need to watch over it, it should update for you.
You will have to set up watcher with equality flag as the third argument:
$scope.$watch(function($scope) {
return mediaSources.getStorages();
}, function() {
console.log('call');
}, true);

Angular, use a service or factory?

I am trying to set up a middle ground between modules to get a custom url so that the user may save (copy the url) the state and share it with other users (or return to it). I am trying to build a simple working copy, then my goal is to make it as polymorphic as possible.
I have a basic working copy right now using a url object to store some variables (in the url object right now), which are then read in the controllers on load to change to the desired state. For example, the very top of my first controller is
$scope.test = $location.search().test;
so whatever test= in the url, it will set to $scope.test (these control my state). I have everything broken out into individual modules right now, so my initial method of getting the modules to speak is setting a function like this (found elsewhere on stack)
function persistMod($scope, $window) {
$scope.$watch(function (){
return $window.test;
}, function(v) {
if(v == undefined ){}else{
if($scope.test !== v) {
$scope.test = v;
}
}
});
$scope.$watch('test', function(v) {
if($window.test !== v) {
$window.test = v;
}
});
$scope.$watch(function (){
return $window.test2;
}, function(v) {
if(v == undefined ){}else{
if($scope.test2 !== v) {
$scope.test2 = v;
}
}
});
$scope.$watch('test2', function(v) {
if($window.test2 !== v) {
$window.test2 = v;
}
});
}
And in the controllers I just call
persistMod($scope, $window);
The purpose of this is to let them keep track of each other separately so when the url is updated - each module can keep track of the other ones current state so there is no confusion. So I can call
$location.search({mod1: $scope.test, mod2: $scope.test2});
When the state changes in each and it will stay the same.
So the problem here is - I would like to turn this into a service I can inject into both modules as an in between to keep track of this for me and change the url accordingly. So I'm wondering is this more of a factory or a service's job.
I want to :
-keep track of how many states change (lets say there can be a min of 2 and a max of 15) and change the url accordingly
-be able to send the new url individually for each module and have it update the url string with kind of a post to the service like myService.urlChange("newurlvariable");
So the factory/service would have to keep track of how many modules are using it and change accordingly. This is still new to me so I could use some guidance, I've done a bunch of digging around and feel stuck right now. Any insight is more than welcome. Thanks for reading!
So you definitely want a factory for this. But you will need to rewrite most of this to do what you want.
It's important to think of factories as a singleton, almost like global functions and variables. You could have a factory like this...
app.factory('PersistMod', [
function() {
var service = {};
service.url;
service.addItem = function(item) {
service.url = service.url + item;
}
service.getUrl = function() {
return service.url;
}
return service;
}])
I won't write our the whole thing but hopefully this will give you an idea on how to set it up.
Once this is setup you can inject this factory into your controller and then call the functions or get the variables. For example
PersistMod.addItem('something');
var currentUrlString = PersistMod.getUrl;
console.log(currentUrlString) // 'something'
Hopefully that helps.
Something like that?
angular.module(...).factory('persistMod', function ($window) {
return function persistMod($scope, name) {
//watch $window[name]
//watch $scope[name]
}
});
[...]
angular.module(...).controller('MyCtrl', function($scope, persistMod) {
persistMod($scope, 'test');
persistMod($scope, 'test2');
});
Also, why not simplify this:
if($window.test !== v) {
$window.test = v;
}
...with this:
$window.test = v;
No need to test before assigning.

AngularJS, Using a Service to call into a Controller

I am new to Angular, what I would like to accomplish is: From a Service / Factory to call methods directly into a controller.
In the following code, I would like from the valueUserController I would like to create a method from the service myApi and set the value inside the valueController.
Here is my code:
modules/myApi.js
var MyApi = app.factory('MyApi', function()
var api = {};
api.getCurrentValue = function() {
// needs to access the Value controller and return the current value
}
api.setCurrentValue = function(value) {
// needs to access the Value controller and set current value
}
api.getValueChangeHistory = function() {
// access value controller and return all the values
}
);
controllers/value.js
app.controller('valueController', function($scope) {
var value = 0;
function getValue() {
return value;
}
function setValue(inValue) {
value = inValue;
}
// ......
});
controllers/valueUser.js
app.controller('valueUserController', function($scope, myApi) {
function doStuff() {
var value = myApi.getValue();
value++;
myApi.setValue(value);
}
});
I am finding to do this in AngularJS pretty difficult and I haven't found any similar post on here.
Thanks for any help,
Andrea
Trying to communicate with a specific controller from a service is not the correct way of thinking. A service needs to be an isolated entity (which usually holds some state), by which controllers are able to interact with.
With this in mind, you can use something like an event pattern to achieve what you are looking for. For example, when your service completes some particular process, you can fire an event like so:
$rootScope.$broadcast('myEvent', { myValue: 'someValue' });
Then any controller in your system could watch for that event and perform a specific task when required. For example, inside your controller you could do the following:
$scope.$on('myEvent', function(event, data){
// Do something here with your value when your service triggers the event
console.log(data.myValue);
});

angular JS - communicate between non-dependend services

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;
};
});

Resources