How to encapsulate single and temporal events in a service? - javascript

I'm trying to encapsulate the events in a service in order to implement a mechanics to subscribe / unsubscribe the listeners when a controller's scope is destroyed. This because I have been using the rootScope.$on in the following way:
if(!$rootScope.$$listeners['event']) {
$rootScope.$on('event', function(ev, data){
// do some...
});
}
or
$scope.$on('$destroy', function(ev, data){
// unsubscribe the listener
});
So I just need one listener of this event, I need to delete the existing listener when the controller is no longer alive, because the function I registered earlier is still being triggered.
So I need to implement a $destroy event listener on my controller, to destroy the listener when the scope is destroyed, but I don't want to do that code each time I create an event.
That's why I want to create a service in where I'm going to encapsulate the events.
angular.module('core').factory('event', [
function() {
var service = {};
service.events = {};
service.on = function(scope, eventId, callback) {
scope.$on('$destroy', function(ev, other){
//unsubscribe
});
service.events[eventId] = callback;
// scope = null; I guess ?
};
service.emit = function(eventId, data){
if (service.events[eventId])
service.events[eventId](data);
else
return new Error('The event is not subscribed');
};
return service;
}
]);
This could be done using $rootScope instead of my own methods but encapsulating the $on and $emit of $rootScope, but at the end I'll have the same issue here.
So these are my questions:
Is a good practice to pass the scope ref value to a service?
What is the meaning of $$destroyed? when this is true means that angularJS has no internal references to the instance?
Should I do a scope = null in my service to let GC delete the object or does angularJS handle an explicit delete?
Is there a better way to do what I want?

What you are trying to accomplish is basically an event bus.
You have also described very well what is wrong with the current implementation.
A different way to approach the problem is to decorate the $rootScope with your bus (or any other event bus for that matter). Here is how:
app.config(function ($provide) {
$provide.decorator('$rootScope', ['$delegate', '$$bus', function ($delegate, $$bus) {
Object.defineProperty($delegate.constructor.prototype, '$bus', {
get: function () {
var self = this;
return {
subscribe: function () {
var sub = $$bus.subscribe.apply($$bus, arguments);
self.$on('$destroy',
function () {
console.log("unsubscribe!");
sub.unsubscribe();
});
},
publish: $$bus.publish
};
},
enumerable: false
});
return $delegate;
}]);
});
Considering the following $$bus implementation (kept basic for simplicity):
app.factory('$$bus', function () {
var api = {};
var events = {};
api.subscribe = function (event) {
if (!events.hasOwnProperty(event.name)) {
events[event.name] = [event];
} else {
events[event.name].push(event);
}
return {
unsubscribe: function () {
api.unsubscribe(event);
}
}
};
api.publish = function (eventName, data) {
if (events.hasOwnProperty(eventName)) {
console.log(eventName);
angular.forEach(events[eventName], function (subscriber) {
subscriber.callback.call(this, data);
});
}
};
api.unsubscribe = function (event) {
if (events.hasOwnProperty(event.name)) {
events[event.name].splice(events[event.name].indexOf(event), 1);
if (events[event.name].length == 0) {
delete events[event.name];
}
}
};
return api;
});
Now all you have to do is subscribe or publish events. The unsubscribe will take place automatically (when the $scope is destroyed):
$scope.$bus.subscribe({
name: 'test', callback: function (data) {
console.log(data);
}
});
And later on publish an event:
$scope.$bus.publish('test', {name: "publishing event!"});
An important point to make is that the events themselves are subscribed to each individual $scope and not on the $rootScope. That is how you "know" which $scope to release.
I think it answers your question. With that in mind, you can obviously make this mechanism much sophisticated (such as controller event listener released when a view routed, unsubscribe automatically only to certain events, etc.).
Good luck!
** This solution is taken form Here which uses a different bus framework (other then that it is the same).

Related

Modifying callback parameters in JavaScript

I'm creating a service in angular to track events, and I want the controllers to be as dumb as possible about the event tracking. For this service each controller will need to supply its own copy of the event function. My current solution is below.
//tracking module
let tracked_events = ['Event1','Event2']
.map(function(e){
return {
eventName:e,
eventProperties: {}
}
});
let finishingTracking = (event) => {
console.log("prentend this does something fancy...",event);
}
let track = (eventName,fn) => {
var event = tracked_events.filter(e => e.eventName===eventName)[0];
fn(event.eventProperties);
//some other internal tracking function
finishTracking(event);
}
//end tracking module
//called from a controller
track("Event 1",(event) => {
event["User ID"] = 123;
event["Company ID"] = 12
});
//some other controller
track("Event 2",(event) => {
event["Manager ID"] = 345;
event["Time Sent"] = Date.now()
}
This works well because each controller will only have to provide its own way to modify the event object and it won't know anything else about the tracking module. From a design perspective, is this ok? I'm not sure about modifying the callback parameters (V8 optimizations/side effects), but I can't think of another way that doesn't cause more changes to each controller that needs to do its own event tracking. Any suggestions on this?
EDIT (prior to refactor)
var eventProperties = { table: "Machines, has_rows: /*rows...*/ }
some_service.list(data)
.then(function (response) {
tracking_service.track({
eventName: 'Event 1',
eventProperties: eventProperties
});
AFTER
some_service.trackEvent(some_service.events.EVENT1, function (event) {
event["Table"] = "Machines";
event["Has Rows"] = response.data.machines.length > 0;
});
An object of shape {eventName: "Name", eventProperties:{} } is currently being defined in each controller. My solution was to pass in the eventName from a constant defined in the service and the callback function modifies the eventProperties of the object. Each controller will have a different set of properties in eventProperties.
After the callback in version two runs, the service does the actual "tracking". My aim was to have a function that prepared the eventProperties object and added whatever properties it needed before being actually tracked.
self.trackEvent = function (eventName, fn) {
//var event = TRACKED_EVENTS.filter(e => e.eventName === eventName)[0];
var event = {
eventName: self.events[eventName],
eventProperties: { }
}
fn(event.eventProperties); //this is the callback that does the prep
self.track(event); //does the actually tracking
}

global communication in angular module: event bus or mediator pattern/service

So far I have seen many solutions of the problem. The simplest one is, of course, to $emit an event in $rootScope as an event bus e.g. ( https://github.com/btilford/anti-patterns/blob/master/angular/Angular.md )
angular.module('myModule').directive('directiveA', function($rootScope) {
return {
link : function($scope, $element) {
$element.on('click', function(event) {
$rootScope.$emit('directiveA:clicked', event);
});
}
}
});
angular.module('myModule').directive('directiveB', function() {
return {
link : function($scope, $element) {
$rootScope.on('directiveA:clicked', function(event) {
console.log('received click event from directiveA');
});
}
}
});
and another one is to declare a service with a mediator or pubsub functionality / an enclosed scope e.g. ( Communicating between a Multiple Controllers and a directive. )
module.factory('MessageService',
function() {
var MessageService = {};
var listeners = {};
var count = 0;
MessageService.registerListener = function(listener) {
listeners[count] = listener;
count++;
return (function(currentCount) {
return function() {
delete listeners[currentCount];
}
})(count);
}
MessageService.broadcastMessage = function(message) {
var keys = Object.keys(listeners);
for (var i = 0; i < keys.length; i++) {
listeners[keys[i]](message);
}
}
return MessageService;
}
);
The question are:
is there point to use the second one in an angular application?
and what are pros and cons of each of those in comparison to each other?
Creating your own implementation of event emitter is counter-productive when writing an AngularJS application. Angular already provides all tools needed for event-based communication.
Using $emit on $rootScope works nicely for global inter-service communication and doesn't really have any drawbacks.
Using $broadcast on a natural scope (one that is bound to a part of your DOM) provides scoped communication between view components (directives, controllers).
Using $broadcast on $rootScope brings the two previous points together (it provides a completely global communication platform). This is the solution used basically by any AngularJS-based library out there.
and
If you're worried about performance in the previous option and you really want your separate event emitter, you can easily create one by creating an isolated scope ($rootScope.$new(true)) and using $broadcast on it. (You can then wrap it into a service and inject it anywhere you want.)
The last option creates a full-fledged event emitter integrated into Angular (the implementation provided in your question would at least need to wrap all listener calls in $apply() to integrate properly) that can be additionally used for data change observation, if that fits a particular use-case.
However, unless your application is really humongous, or you're really paranoid about event name collisions, the first three options should suffice just fine.
I won't go into detail about other means of communication between your components. Generally speaking, when the situation calls for data sharing using scope, direct interaction of controllers, or communication through DOM Node attributes, you should know it.
I would say that broadcasting is an Angular way how to achieve this.
However your mediator can work, if you pass internal funcion of directive, in example I have used method on scope, but it can be done also with controller method.
I have used exact same factory as you post.
angular.module("sharedService", [])
.factory('MessageService',
function() {
var MessageService = {};
var listeners = {};
var count = 0;
MessageService.registerListener = function(listener) {
listeners[count] = listener;
count++;
return (function(currentCount) {
return function() {
delete listeners[currentCount];
};
})(count);
};
MessageService.broadcastMessage = function(message) {
var keys = Object.keys(listeners);
for (var i = 0; i < keys.length; i++) {
listeners[keys[i]](message);
}
};
return MessageService;
}
)
.directive("directiveA", function(MessageService) {
return {
link:function(scope) {
scope.click = function() {
MessageService.broadcastMessage("broadcasted message");
};
},
template: '<button ng-click="click()">Click</button>'
};
})
.directive("directiveB", function(MessageService) {
return {
link:function(scope) {
scope.callback = function(message) {
console.log(message);
};
MessageService.registerListener(scope.callback);
}
};
});
Full example: http://jsbin.com/mobifuketi/1/edit?html,js,console,output
Just to be complete, I would like to add, that angular also provides more posibilities how can directives communicate.
Require atribute
If your directives are connected in hierarchy, then you can use require attribute which let you to access other directives controller. This is ussually best solution for many cases.
.directive("directiveA", function() {
return {
require: "^directiveB",
link: function(scope, element, attrs, directiveCtrl) {
scope.click = function() {
directiveCtrl.call();
};
},
template: '<button ng-click="click()">Click</button>'
};
})
.directive("directiveB", function() {
return {
controller :function() {
this.call = function() {
console.log("method has been called");
};
}
};
});
Full example: http://jsbin.com/turoxikute/1/edit?html,js,console,output
Using $watch
If the functionality deppends on data and not on action, you cen use $watch and react on the changes of given model or model stored in shared service , its not like listener, its basicly checking of change. I have named method changeState() and log "state changed" for everybody see it clear.
angular.module("sharedService", [])
.service("MediatorService", function() {
this.state = true;
this.changeState = function() {
this.state = !this.state;
};
})
.directive("directiveA", function(MediatorService) {
return {
link:function(scope) {
scope.click = function() {
MediatorService.changeState();
};
},
template: '<button ng-click="click()">Click</button>'
};
})
.directive("directiveB", function(MediatorService) {
return {
link:function(scope) {
scope.mediator = MediatorService;
scope.$watch("mediator.state", function(oldValue, newValue) {
if (oldValue == newValue) {
return;
}
console.log("state changed");
});
}
};
});
Full example: http://jsbin.com/darefijeto/1/edit?html,js,console,output
I like an event bus.
Angular does provide $emit on $rootScope but I don't think that should bound your decision to use it for event-based flows if they are complex or foreseeably complex. Angular has lots of features and while most are great, even the authors admit they're mostly meant to compliment good software engineering principles, not replace them.
I like this post on using postal.js: An angular.js event bus with postal.js. The two main benefits are channels and envelopes, which will make for more explicit, understandable and flexible event-based logic.
I find service based approaches to be error prone if state is not managed tightly, which is hard with async calls and injections, where you can't be certain how a service will be multi-purposed in the future.

Listen document event in angular controller

How to catch event in angular's controller?
I have document level events, so I need to catch event in angular controller, is it possible?
Update
I have standalone js file with handling of some actions from camera.
document.addEventListener('myCameraEvent', handleMyCameraEvent);
And I want to trigger this event in angular controller or directive. Could anyone explain, how is it possible to implement with angular?
Here's a generic custom events observer service:
.service('Camera', function($document) {
//map of current subscribers
var subscribers = {};
//notifies all subscribers of particular event type
function notify(event) {
var handlers = subscribers[event.type] || [];
for (var i = 0; i < handlers.length; i++) {
handlers(i)(event);
}
}
//adds new handler to subscribers list
//returns object with unsubscribe() method
this.subscribe(eventType, handler) {
var handlers = subscribers[eventType] || [];
handlers.push(handler);
return (function(type, index) {
return {
unsubscribe: function() {
subscribers[type].splice(index);
}
}
})(eventType, handlers.length - 1);
}
//register custom events
$document.on('myCameraEvent', function(event) {
notify(event);
});
$document.on('myOtherCameraEvent', function(event) {
notify(event);
});
});
In your controller, you'd use it like this:
.controller('MyCtrl', function($scope, Camera) {
subscriber = Camera.subscribe('myCustomEvent', customEventHandler);
function customEventHandler(event) {
//process event
//..
//if handler is not needed anymore, unsubscribe
//subscriber.unsubscribe();
}
})
Of course, this is just general idea. You'd might want to have specific methods on Camera service, for example Camera.onSnapshot(snapshothandler); Camera.onTurnOff(turnOffHandler); which would register handlers to specific events to abstract away the event names.

Best approach to implement safe $apply in Angular Service/Controller

I'm using the socket.ioclient in my Angular WebApp. I wrapped it up in a service, but that might not necessarily be of interest. However, since the incoming data is in a different scope than the controller which is using the service, I always have to call $scope.$apply. Even worse I found a few situations (during connect/reconnect) where I would have to use a safeApply as explained here.
I understand that this is an Angular Anti-Pattern, however I don't see a way around this.
Is there a general way to solve this (preferably inside the service) that does not pollute the controllers with a lot of $scope.$apply/safeApply?
BR,
Daniel
Here is also some code, working, but not nice:
angular.module('mean')
.controller('ConnectionStateController', function ($scope) {
$scope.safeApply = function (fn) {
var phase = this.$root.$$phase;
if (phase == '$apply' || phase == '$digest') {
if (fn && (typeof (fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
var socket = io.connect('http://localhost:3000');
$scope.message = 'Not connected';
socket.on('connect', function () {
$scope.safeApply(function () {
$scope.message = "Connected";
});
});
});
In your case, you know that socket.io operations are always going to be run asynchronously and that makes it a valid place to use $scope.$apply (towards the bottom of the page).
I would recommend not using safeApply and explicitly calling $scope.$apply. Using safeApply is going to cover up for genuine mistakes in implementation. Also, if you are wrapping your code in a service, you can inject the $rootScope in the service and call the async functions in $rootScope.$apply. Then you would not need to call $scope.$apply in each controller.
I'll answer the question myself, since I have a working solution using defers. Nevertheless I'm not sure if I did it right. Here's the service:
angular.module('mean')
.factory('ConnectionService', function ($q, $timeout) {
var bindings = {};
var socket = io.connect('http://localhost:3000');
// catch all events by overriding the socket's $emit
var $emit = socket.$emit;
socket.$emit = function (event, obj) {
console.log('***', 'on', '"' + event + '"', obj);
if (bindings.hasOwnProperty(event)) {
bindings[event].notify(obj);
}
$emit.apply(socket, arguments);
};
return {
socket: socket,
observe: function (event, callback) {
if (!bindings.hasOwnProperty(event)) {
bindings[event] = $q.defer();
}
bindings[event].promise.then(null, null, function (data) {
callback.apply(this, [ data ]);
});
}
};
});
This allows a controller to use the service like this:
ConnectionService.observe('news', function (data) {
$scope.message = data;
});
Not sure, if that's a nice solution or if it has side effects, that I don't see yet.
BR,
Daniel

Get informed when listener has been attached to a Backbone.Model event

I'd like to know if its somehow possible to notify a Backbone Model that a listener has been attached to a certain event so it can react by doing some initialization process.
I'm asking, because I have a case where some events need too much costly preprocessing to just having the data available to fire a event that nobody listens to.
Is that possible at all with the Backbone Event system, or do I have to override the on() method?
There are a lot of ways to implement your idea.
Here is the first easy way:
_.extend(Backbone.Model.prototype, {
subscribe: function (name, callback, context) {
this.on(name, callback, context);
this.trigger('bind:' + name, callback, context);
}
});
So we will not override "on" method. We created own "subscribe" method. Here is usage example:
var Human = Backbone.Model.extend({
initialize: function () {
this.on('bind:change', function () {
console.log('Someone started listening to "change" event');
});
}
});
var human = new Human({ name: 'Christian Engel' });
human.subscribe('change', function () {
// do some stuff when a model's attributes have changed
});
But new method can make you confused. So we have an ability to override "on" for all the Backbone models:
_.extend(Backbone.Model.prototype, {
on: function (name, callback, context) {
var eventSplitter = /\s+/,
eventBinder = /bind:/;
Backbone.Events.on.call(this, name, callback, context);
if (!eventBinder.test(name) && !eventSplitter.test(name)) {
this.trigger('bind:' + name, name, callback, context);
}
}
});
Great! Now we can write pretty nice code:
var Human = Backbone.Model.extend({
initialize: function () {
this.on('bind:change', function () {
console.log('Someone started listening to "change" event');
});
}
});
var human = new Human({ name: 'Christian Engel' });
human.on('change', function () {
// do some stuff when a model's attributes have changed
});
And here is live demo / example / test:
http://jsfiddle.net/gmAL3/1/

Categories