SocketIO duplicate events with AngularJS - angularjs

I've ran into issues in the past wherein SocketIO will emit duplicate events when using AngularJS. There are two common reasons as to why this occurs:
Creating your event listeners inside the connect event listener.
Event listeners being dynamically created multiple times (usual case with AngularJS).
Luckily, there are two common solutions that match with the above scenarios. Please see the accepted answer to understand these solutions.

Creating your event listeners inside the connect event listener
This bug occurs if you create listeners inside your connect event listener. Simply move these listeners outside the connect event listener.
Before:
socket.on('connection', _ => {
console.log('Connected');
socket.on('someOtherEvent', data => {
console.log(data);
});
});
After:
socket.on('connection', _ => {
console.log('Connected');
});
socket.on('someOtherEvent', data => {
console.log(data);
});
Event listeners being dynamically created multiple times (usual case with AngularJS).
This is a common problem if you register your event listeners inside a controller. To fix this, edit your service like so:
angular.module('socketIO', []).factory('socket', $rootScope => {
class SocketHandler {
constructor(url) {
this._socket = false;
this._url = url;
this._events = {};
this._build();
}
_build() {
// Change for your configuration options below.
this._socket = io.connect(this._URL);
}
on(event, callback) {
if(this._events.hasOwnProperty(event))
return this._events[event];
return this._events[event] = this._socket.on(event, (...args) => {
$rootScope.$apply(_ => {
callback.apply(this._socket, args);
});
});
}
emit(event, data, callback = false) {
return this._socket.emit(event, data, (...args) => {
if(!callback)
return;
$rootScope.$apply(_ => {
callback.apply(this._socket, args);
});
});
}
}
return { build: SocketHandler };
});
You can then use this in your constructor:
angular.module('someController', socketIO => {
const socket = socketIO.build('your socket url');
});
The reason this fix works is because when you switch pages in AngularJS (with the default router) it recreates an instance of the controller. This means that multiple event listeners are now bound to a single event. We prevent this from occurring by caching our event listeners in SocketHandler._events, and returning the cached listener if we have it cached.

Related

AngularJS - Remove custom event listener

I have a non AngularJS snippet that is communicating with my AngularJS modules via custom events. Each of these AngularJS modules represents a different route. When I am leaving each route $onDestroy is triggered but does not remove the event listener. What am I missing to un-register my custom event?
Non AngularJS HTML Snippet
<script>
function sendEvent() {
const payload = getPayload();
const event = new CustomEvent('myCustomEvent', { payload });
window.dispatchEvent(event);
}
</script>
<button onclick="sendEvent()">Send Custom Event</button>
AngularJS Component
Class ModuleA {
$onInit() {
this.$window.addEventListener('myCustomEvent', this.onCustomEventClick.bind(this));
}
$onDestroy() {
this.$window.removeEventListener('myCustomEvent', this.onCustomEventClick.bind(this), false);
}
}
Class ModuleB {
$onInit() {
this.$window.addEventListener('myCustomEvent', this.onCustomEventClick.bind(this));
}
$onDestroy() {
this.$window.removeEventListener('myCustomEvent', this.onCustomEventClick.bind(this), false);
}
}
Every time you call bind it will create a new function and return it instead of modifying the function itself. So the even listeners you provide to addEventListener and removeEventListener are different functions, thus the registered ones are not removed.
To solve it, call bind once in $onInit and keep a reference to the returned function and always use that reference:
Class ModuleB {
$onInit() {
this.onCustomEventClick = this.onCustomEventClick.bind(this)
this.$window.addEventListener('myCustomEvent', this.onCustomEventClick);
}
$onDestroy() {
this.$window.removeEventListener('myCustomEvent', this.onCustomEventClick, false);
}
}

Promise Chaining / Cascading to RxJS angular 1 to angular 2

I'm currently trying to convert some good old $http Promises into rxJs observables in angular2.
A very simple example of something I used to do frequently in angular 1:
// Some Factory in Angular 1
var somethings = [];
var service = {
all: all
};
return service;
function all() {
return $http
.get('api/somethings')
.then(function(response) {
somethings = parseSomethings(response.data);
return somethings;
});
}
// Some Controller in Angular 1
var vm = this;
vm.somethings = [];
vm.loading = true;
loadThings();
function loadThings() {
SomeFactory
.all()
.then(function(somethings) {
vm.somethings = somethings;
})
.finally(function() {
vm.loading = false;
})
}
Now I can achieve something similar with angular2 using RxJs Observable and Subject. But I am unsure on how to return 'completion hooks' down to the caller. (E.g.: The loading in the angular1 controller)
What I tried with for angular2 is something similar:
// Some Service in Angular 2
export class SomeService {
private _somethings$: Subject<Something[]>;
private dataStore: {
somethings: Something[]
};
constructor(private http: Http) {}
get _somethings$() : Observable<Something[]> {
return this._somethings$.asObservable();
}
loadAll() {
this.http
.get(`api/somethings`)
.map(response => response.json())
.subscribe(
response => {
this.dataStore.somethings = parseSomethings(response.data);
this._somethings$.next(this.dataStore.somethings);
}
);
}
}
//Some Component in Angular 2
export class SomeComponent() {
constructor(private someService: SomeService) {}
ngOnInit() {
this.someService.loadAll();
this.somethings = this.someService.somethings$;
}
}
Note
I used a Subject / Observable here so that when I create / update / remove 'something' I can call .next to notify the subscribers.
E.g.:
create(something: Something) : Observable<Something>{
return this.http.post(`api/somethings`, JSON.stringify(something))
.map(response => response.json())
.do(
something => {
this.dataStore.somethings.push(something);
this._somethings$.next(this.dataStore.somethings); //Notify the subscribers
}
);
}
Note
Here I used .do in the method and when calling the method in a 'form component' I subscribe just to get completion handler:
this.someService.create(something).subscribe(
(ok) => ...
(error) => ...
() => stop loading indicator
)
Questions
How can we pass down a completion-hook to callers (e.g. angular 1 controller) in angular 2 with RxJs
What alternative paths do exist to achieve similar behaviour ?
Your last code example is fine. Just use map() instead of subscribe() and subscribe at call site.
You can use
return http.post(...).toPromise(val => ...))
or
return http.post(...).map(...).toPromise(val => ...))
to get the same behavior as in Angular where you can chain subsequent calls with .then(...)

UI Notifications with angular js

I have to implement some standard notification UI with angular js. My approach is the following (simplified):
<div ng-controller="MainCtrl">
<div>{{message}}</div>
<div ng-controller="PageCtrl">
<div ng-click="showMessage()"></div>
</div>
</div>
And with the page controller being:
module.controller("PageCtrl", function($scope){
counter = 1
$scope.showMessage = function(){
$scope.$parent.message = "new message #" + counter++;
};
});
This works fine. But I really don't like the fact that I need to call $scope.$parent.
Because if I am in another nested controller, I will have $scope.$parent.$parent, and this becomes quickly a nightmare to understand.
Is there another way to create this kind of global UI notification with angular?
Use events to send messages from one component to another. That way the components don't need to be related at all.
Send an event from one component:
app.controller('DivCtrl', function($scope, $rootScope) {
$scope.doSend = function(){
$rootScope.$broadcast('divButton:clicked', 'hello world via event');
}
});
and create a listener anywhere you like, e.g. in another component:
app.controller('MainCtrl', function($scope, $rootScope) {
$scope.$on('divButton:clicked', function(event, message){
alert(message);
})
});
I've created a working example for you at http://plnkr.co/edit/ywnwWXQtkKOCYNeMf0FJ?p=preview
You can also check the AngularJS docs about scopes to read more about the actual syntax.
This provides you with a clean and fast solution in just a few lines of code.
Regards,
Jurgen
You should check this:
An AngularJS component for easily creating notifications. Can also use HTML5 notifications.
https://github.com/phxdatasec/angular-notifications
After looking at this: What's the correct way to communicate between controllers in AngularJS? and then that: https://gist.github.com/floatingmonkey/3384419
I decided to use pubsub, here is my implementation:
Coffeescript:
module.factory "PubSub", ->
cache = {}
subscribe = (topic, callback) ->
cache[topic] = [] unless cache[topic]
cache[topic].push callback
[ topic, callback ]
unsubscribe = (topic, callback) ->
if cache[topic]
callbackCount = cache[topic].length
while callbackCount--
if cache[topic][callbackCount] is callback
cache[topic].splice callbackCount, 1
null
publish = (topic) ->
event = cache[topic]
if event and event.length>0
callbackCount = event.length
while callbackCount--
if event[callbackCount]
res = event[callbackCount].apply {}, Array.prototype.slice.call(arguments, 1)
# some pubsub enhancement: we can get notified when everything
# has been published by registering to topic+"_done"
publish topic+"_done"
res
subscribe: subscribe
unsubscribe: unsubscribe
publish: publish
Javascript:
module.factory("PubSub", function() {
var cache, publish, subscribe, unsubscribe;
cache = {};
subscribe = function(topic, callback) {
if (!cache[topic]) {
cache[topic] = [];
}
cache[topic].push(callback);
return [topic, callback];
};
unsubscribe = function(topic, callback) {
var callbackCount;
if (cache[topic]) {
callbackCount = cache[topic].length;
while (callbackCount--) {
if (cache[topic][callbackCount] === callback) {
cache[topic].splice(callbackCount, 1);
}
}
}
return null;
};
publish = function(topic) {
var callbackCount, event, res;
event = cache[topic];
if (event && event.length > 0) {
callbackCount = event.length;
while (callbackCount--) {
if (event[callbackCount]) {
res = event[callbackCount].apply({}, Array.prototype.slice.call(arguments, 1));
}
}
publish(topic + "_done");
return res;
}
};
return {
subscribe: subscribe,
unsubscribe: unsubscribe,
publish: publish
};
});
My suggestion is don't create a one on your own. Use existing models like toastr or something like below.
http://beletsky.net/ng-notifications-bar/
As suggested above, try to use external notifications library. There're a big variety of them:
http://alertifyjs.com/
https://notifyjs.com/
https://www.npmjs.com/package/awesome-notifications
http://codeseven.github.io/toastr/demo.html

How to stop $broadcast events in AngularJS?

Is there a built in way to stop $broadcast events from going down the scope chain?
The event object passed by a $broadcast event does not have a stopPropagation method (as the docs on $rootScope mention.) However this merged pull request suggest that $broadcast events can have stopPropagation called on them.
Snippets from angularJS 1.1.2 source code:
$emit: function(name, args) {
// ....
event = {
name: name,
targetScope: scope,
stopPropagation: function() {
stopPropagation = true;
},
preventDefault: function() {
event.defaultPrevented = true;
},
defaultPrevented: false
},
// ....
}
$broadcast: function(name, args) {
// ...
event = {
name: name,
targetScope: target,
preventDefault: function() {
event.defaultPrevented = true;
},
defaultPrevented: false
},
// ...
}
As you can see event object in $broadcast not have "stopPropagation".
Instead of stopPropagation you can use preventDefault in order to mark event as "not need to handle this event". This not stop event propagation but this will tell the children scopes: "not need to handle this event"
Example: http://jsfiddle.net/C8EqT/1/
Since broadcast does not have the stopPropagation method,you need to use the defaultPrevented property and this will make sense in recursive directives.
Have a look at this plunker here:Plunkr
$scope.$on('test', function(event) {
if (!event.defaultPrevented) {
event.defaultPrevented = true;
console.log('Handle event here for the root node only.');
}
});
I implemented an event thief for this purpose:
.factory("stealEvent", [function () {
/**
* If event is already "default prevented", noop.
* If event isn't "default prevented", executes callback.
* If callback returns a truthy value or undefined,
* stops event propagation if possible, and flags event as "default prevented".
*/
return function (callback) {
return function (event) {
if (!event.defaultPrevented) {
var stopEvent = callback.apply(null, arguments);
if (typeof stopEvent === "undefined" || stopEvent) {
event.stopPropagation && event.stopPropagation();
event.preventDefault();
}
}
};
};
}]);
To use:
$scope.$on("AnyEvent", stealEvent(function (event, anyOtherParameter) {
if ($scope.keepEvent) {
// do some stuff with anyOtherParameter
return true; // steal event
} else {
return false; // let event available for other listeners
}
}));
$scope.$on("AnyOtherEvent", stealEvent(function (event, anyOtherParameter) {
// do some stuff with anyOtherParameter, event stolen by default
}));

Backbone.js:"Maximum call stack size exceeded" error

suppose I have a model and a view ,ths view have two method:one is bind the document mousemove event and the other is unbind method,defalut I give the document mousemove event, once the model's enable value changed I will call the view's unbind method:
window.ConfigModel = Backbone.Model.extend({
defaults: {
'enable':0
},
initialize: function(){
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
},
change:function () {
this.set('enable', 9);
}
})
window.PortView = Backbone.View.extend({
viewOn: function () {
$(document).on('mousemove', function () {
console.log('move')
})
},
viewOff: function () {
$(document).off('mousemove');
}
})
then I put an input on the document to call the model changed:
$('input').click(function () {
var configModel = new ConfigModel();
configModel.change();
})
the boot script is :
var portView1 = new PortView();
portView1.viewOn();
The problem is once I call the click the input button ,the chrome would tell me an error:Maximum call stack size exceeded it seems the change be invoke many times.So what's the problem with my problem ,how can I solve this problem
Backbone models already have a change method:
change model.change()
Manually trigger the "change" event and a "change:attribute" event for each attribute that has changed. If you've been passing {silent: true} to the set function in order to aggregate rapid changes to a model, you'll want to call model.change() when you're all finished.
Presumably something inside Backbone is trying to call configModel.change() and getting your version of change which triggers another change() call inside Backbone which runs your change which ... until the stack blows up.
You should use a different name for your change method.
That said, your code structure is somewhat bizarre. A model listening to events on itself is well and good but a model creating a view is odd:
initialize: function() {
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
}
And instantiating a view simply to call a single method and then throw it away is strange as is creating a new model just to trigger an event.
I think you probably want to have a single ConfigModel instance as part of your application state, say app.config. Then your click handler would talk to that model:
$('input').click(function () {
app.config.enable_level_9(); // or whatever your 'change' gets renamed to
});
Then you'd have some other part of your application (not necessarily a view) that listens for changes to app.config and acts appropriately:
app.viewOn = function() {
$(document).on('mousemove', function() {
console.log('move')
});
};
app.viewOff = function() {
$(document).off('mousemove');
};
app.init = function() {
app.config = new ConfigModel();
app.viewOn();
$('input').click(function () {
app.config.enable_level_9();
});
// ...
};
And then start the application with a single app.init() call:
$(function() {
app.init();
});

Resources