Simple event aggregator - javascript

I'm looking for a simple event aggregator that works with require.js. I have two modules, one containing a view model and another with a "listener" of some sort:
// view model
define(['lib/knockout-2.2.1', 'events/aggregator'], function(ko, events){
var squareViewModel = function(contents) {
this.click = function(){
events.publish('squareClicked', this);
};
};
return squareViewModel;
});
// some listener of some kind
define(['events/aggregator'], function(events){
events.subscribe('squareClicked', function(e){
alert("hurrah for events");
});
});
Is there anything out there that does this? Is this kind of architecture even a good idea? This is my first foray into client-side architecture.

This is similar to what you posted, but I've had good luck with extending Backbone events (you don't actually have to use anything else about Backbone for this to work), something like this:
define(['underscore', 'backbone'], function( _, Backbone ) {
var _events = _.extend({}, Backbone.Events);
return _events;
});
And then all your viewmodels (or any code really) can use it:
define(['knockout', 'myeventbus'], function(ko, eventBus){
return function viewModel() {
eventBus.on('someeventname', function(newValue) {
// do something
});
this.someKOevent = function() {
eventBus.trigger('someotherevent', 'some data');
};
};
});
The original idea came from this article by Derick Bailey. One of my other favorites on client-side events is this one by Jim Cowart
Again, it amounts to nearly the same thing as what you posted. However, the one thing I don't like about the approach of tying it to jQuery document DOM node is that you might have other types of events flying through there as well, bubbled up from child nodes, etc. Whereas by extended backbone events you can have your own dedicated event bus (or even more than one if you e.g. wanted to have separate data events vs. UI events).
Note regarding RequireJS in this example: Backbone and Underscore are not AMD-compatible, so you'll need to load them using a shim config.

I ended up using jQuery to make my own:
define([], function(){
return {
publish: function (type, params){
$(document.body).trigger(type, params);
},
subscribe: function(type, data, callback){
$(document.body).bind(type, data, callback);
},
};
});
It works for what I want it for, but it has not been extensively tested.
As explunit points out in his answer, this will capture any events on document.body. I also discovered a scoping issue when accessing this inside of a passed callback function.

Related

Impossible Backbone Zombies

I've been trying to debug my Backbone multi-page app for most of the day now to get rid of 'zombies', but unfortunately to no avail. Before today, I didn't even realize I have a zombie problem. What am I doing wrong?
This is my RegionManager:
var regionManager = (function() {
var currView = null;
var rm = {};
var closeView = function(view) {
if (view && view.close) {
view.close();
}
};
var openView = function(view) {
view.render();
if (view.onShow) {
view.onShow();
}
};
rm.show = function(view) {
closeView(currView);
currView = view;
openView(currView);
};
return rm;
})();
This is my View cleaning up function:
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
if (this.views) {
_.invoke(this.views, 'close');
}
// Unbind any view's events.
this.off();
// Unbind any model and collection events that the view is bound to.
if (this.model) {
this.model.off(null, null, this);
}
if (this.collection) {
this.collection.off(null, null, this);
}
// Clean up the HTML.
this.$el.empty();
};
I tried appending the View els directly to the body and using this.remove(); in the View clean-up function (instead of using a common el: $('#content') to which I am appending elements, then cleaning up by this.$el.empty()), but that didn't work either.
It might have something to do with my "global Events":
Backbone.Events.on('letterMouseDown', this.letterMouseDown, this);
But I take care of them with the onClose function:
onClose: function() {
Backbone.Events.off('letterMouseDown');
}
One problem I see is that your close function never removes the event delegator from the view's el. A view's events are handled by using the delegator form of jQuery's on to attach a single event handler to the view's el. Your close does:
this.$el.empty();
but that only removes the content and any event handlers attached to that content, it does nothing at all to the handlers attached directly to this.el. Consider this minimal example:
var V = Backbone.View.extend({
events: {
'click': 'clicked'
},
clicked: function() {
console.log('still here');
}
});
var v = new V({ el: '#el' });
v.close();
After that, clicking on #el will throw a 'still here' in the console even though you think that the view has been fully cleaned up. Demo: http://jsfiddle.net/ambiguous/aqdq7pwm/
Adding an undelegateEvents call to your close should take care of this problem.
General advice:
Don't use the old-school on and off functions for events, use listenTo and stopListening instead. listenTo keeps track of the events on the listener so it is easier to remove them all later.
Simplify your close to just this:
Backbone.View.prototype.close = function() {
if(this.onClose)
this.onClose();
if(this.views)
_.invoke(this.views, 'close');
this.remove();
};
Don't bind views to existing els. Let the view create (and own) its own el and let the caller place that el into a container with the usual:
var v = new View();
container.append(v.render().el);
pattern. If you must attach to an existing el then the view should override remove with a slightly modified version of the standard implementation:
remove: function() {
this.$el.empty(); // Instead of removing the element.
this.undelegateEvents(); // Manually detach the event delegator.
this.stopListening();
return this;
}
I'm pretty sure I found the root for my problem.
mu is too short was right, with the close() method I wasn't removing the events bound directly to my el (which I tried to do by this.off() - this.$el.off()/this.undelegateEvents() is the correct way). But for me, it only fixed the problem that events got called multiple times unnecessarily.
The reason I was plagued by 'zombie views' or unintended behavior was that I wasn't freeing up the memory in the View..
this.remove() only gets rid of the el and it's elements/events, but not the View's internal variables. To elaborate - in my View I have an array declared like so this.array: [] and I didn't have it freed in the onClose function.
All I had to do was empty it in the onClose function or initially declare the array as this.array: null so on recurrent View renderings it would at least free the previous array (it still should be freed on the onClose method though, because the array/object is still going to sit in the memory until browsing away from the page).
It was excruciating to debug, because it's a crossword game (at least my code is hard to read there) and sometimes the words didn't match up, but I didn't know where the problem was coming from.
Lessons learned.

Marionette.js questions about events and how to correctly unbind them

Good day to all.
I'm writing an application using Marionette.js and recently I started noticing that moving from view to view and starting/stopping different modules memory consumption grows and not getting released. I started wondering whether I unbind my events correctly and whether I bind to them correctly as well.
So, I have the following cases
Modules
My application consists of sub-applications (modules). When I define a module I do some binding to global event aggregator. Something like this:
MyApplication.module(...) {
var api = { ... some functions here ... }
// Binding to events
MyApplication.vent.on('some:event', function() {...});
MyApplication.vent.on('some:other:event', function() {...});
}
I have checked the documentation and understand that "on" is not a very good choice, I should probably use "listenTo":
MyApplication.module(...) {
var api = { ... some functions here ... }
// Binding to events
this.listenTo(MyApplication.vent, 'some:event', function() {...});
this.listenTo(MyApplication.vent, 'some:other:event', function() {...});
}
But, here is the question, when module gets stopped, does it call "stopListening" or some other internal method that unbinds all the events I have bound in it? I checked the source code of the marionette's module and documentation but, if I understood correctly, when stop is called I need to take care of unbinding everything myself. Am I right?
Controllers
Can be initialized and closed. From the documentation I see that:
Each Controller instance has a built in close method that handles unbinding all of the events that are directly attached to the controller instance, as well as those that are bound using the EventBinder from the controller.
Does it mean that if do the following I correctly unbind all of the events I bound in the controller? I guess the answer is yes.
MyApplication.module(...) {
var controller = Marionette.Controller.extend({
...
// This will be unbinded as I understand?
this.listenTo(someObject, 'some:event', _.bind(function() {
// This will also be unbinded
this.listenTo(someOtherObject, 'some:event', function() {
// This won't be, because in this case this is not in a "controller"
// context but in a function's context which wasn't bound to "controler"
// context.
this.listenTo(some3rdObject, 'some:event', function() { ... });
});
}, this));
});
// Create controller when this sub-application gets initialized.
Contents.addInitializer(function () {
MyModule.Controller = new controller();
});
// Destroy controller and unbind all its event handlers.
Contents.addFinalizer(function () {
MyModule.Controller.close();
delete Contents.Controller;
});
}
So, with controllers I don't need to do anything as long as I use "listenTo", correct?
Views
In views, according to documentation, all gets unbinded when the view gets closed. And again, as long as I use
this.listenTo(..., 'some:event', function() {...});
I should be ok, correct?
To summarize... I only need to take care of unbinding in module's stop event, in all other cases it is taken care of by marionette's core as long as I don't use direct "on" and use "this.listenTo" instead.
Thank you all very much in advance for your answers.
Controllers and Views do their cleaning work correctly but Modules doesn't do it.
Here is more detailed info:
Controller
If you close controller it will unbind all events that are bonded using listenTo in context of controller. You can look in in controller source code.
View
According to Backbone.View source code remove method does stopListening. Also Marionette.View's close calls backbone's remove under the hood. Here is source code.
Module
I've checked Marionette.Module source code but there is no stopListening in stop method. So, Marionette.Module#stop does not do unbinding of events and you should do it manually in finalizer or in onStop, onBeforeStop handlers.
UPDATED: After Marionette.js v1.7.0 Marionette.Module calls stopListening on stop to unbind all events.

jQuery, finding selector for triggered event's elements

I'm working on developing a rather lightweight realtime application.
We're using jQuery's event system to talk between modules in our application. So everything that makes changes in the DOM that should affect anything outside the module must do so by events.
These events are caught by a clientside socket.io implementation which asks the nodejs socket.io server to broadcast the events to all other clients.
MY QUESTION:
Is there a way figure out on what selector the event was triggered? So that if I call
$("tag.class").trigger("myevent");
I can somehow do
$(document).on("myevent", function() {
console.log(this.selector)
});
And it will print "tag.class"
This is interesting because it would be interesting to know that a user triggered a certain event on a selection, rather than a certain event on a number of elements which might appear quite random afterwards.
I have read this question but I wish to be able to get the selector on all events fired in the application.
Thank you for your time :)
Taking inspiration from the linked answer, you can override the jQuery trigger function. Something like:
$.fn.origTrigger = $.fn.trigger;
$.fn.trigger = function (fn) {
var selector = this.selector;
this.origTrigger(function (ev) {
fn(ev, selector);
});
};
Your best bet is to send data with the event. It is also more robust, as you have more control. Selectors might not be pretty, but the data are.
$('tag.class').trigger('myevent', ['some', 'param']);
$(document).on('myevent', function(param1, param2) {
console.log(param1); // "some"
console.log(param2); // "param"
});
Of course, if you want to send as many parameters as you want, you can use an "options" object and handle it in such a way:
$('tag.class').trigger('myevent', [{some: 'option', other: 'option'}]);
$(document).on('myevent', function(options) {
var opts = {
default: 'option',
some: 'default',
other: 'default'
};
$.extend(opts, options);
console.log(opts); // {default: "option", some: "option", other: "option"}
});

Cache backbone views with its events

I am working on a relatively big application which is like some sort of app collection.
All of my apps got a bootstrapping view which loads the base layout and the nested views.
I now started to implement a singleton pattern to the views:
var SomeView = Backbone.View.extend({});
if (SomeView._instance) return SomeView._instance;
SomeView._instance = new SomeView();
return SomeView._instance;
Now I mentioned that when I switch between different apps (views) the event system is broken. This is actually quite logic finally we remove the view out of the document. However I have some sort of resistance against always building up the views new. This is quite ineffective: Everything has to get reloaded (data), and rebuilt.
So is there a way to rebind events to a cached views or is this whole idea bad and I should accept that views have to get rebuilt?
Update:
define(['jquery', 'underscore', 'backbone', 'views/settings/profile'], function($, _, Backbone, ProfileSettingsView) {
var ContentView = Backbone.View.extend({
tagName: "div",
className: "content well",
initialize: function() {
this.on('change:section', this.change, this);
this.views = {};
this.views.profile = new ProfileSettingsView();
},
render: function() {
this.$el.empty();
return this;
},
events: {
"click": "click"
},
// the router triggers this one here
change: function(query) {
console.log(query);
// if I uncomment this then nothing is rendered at all
//this.$el.detach();
var el;
if (query === 'profile') {
el = this.views.profile.render().el;
} else {
this.$el.empty();
}
if (el) {
this.$el.empty().append(el);
}
},
click: function(e) {
console.log('clicked, content should disapear');
}
});
if (ContentView._instance) return ContentView._instance;
ContentView._instance = new ContentView();
return ContentView._instance;
});
I am a bit confused about how I can use jQuery's detach().
I looked at the demo in the official docs and found out that it is not enough to call .detach() on a jQuery object. .detach returns a new object which looks like a jQuery one and contains the events bound. The hard thing about this is that I have to save this returnment of detach() somewhere and I have to now from who it's coming. And now I don't have any look through. I will now search for some Backbone.View example using detach() but I think it is to specific....
Update2:
Yes! I found a workaround: Instead of saving the events and then reinserting it in to the DOM. We can just call this.delegateEvents() to rebind all events. This truly is just a workaround and I would be happy if somebody could provide me an example :)
Personally, I prefer to rebuild my views.
However, I know a lot of people that prefer to re-use them. In that case, follow the instructions in this blog post from Tim Branyen: http://tbranyen.com/post/missing-jquery-events-while-rendering

Is there a better way/pattern to write this jQuery plugin?

I have a search plugin that is decently complex: it has different versions of UI and functionality as well as a bunch in interdependent domElements. Multiple instances of the plugin will exist on a page at once.
I am using the basic jQuery authoring pattern: http://docs.jquery.com/Plugins/Authoring
In order to save the options, interdependent events and and all sorts of dom lookups across multiple objects, I've come to passing the element in question to every function, and storing state/options/interdependencies in a data attribute which I retrieve each time. It works, and keeps events from colliding, but it seems like a messy way to write code.
What is the best way to store state across multiple instances? Is the way I am doing it a huge overkill and I am missing something? It probably stems from my misunderstanding of creating class like objects in a jQuery plugin pattern.
(function($) {
var _options = {};
var methods = {
init: function(options) {
return this.each(function() {
if (options) {
_options = $.extend($.fn.examplePlugin.defaults, options);
} else {
_options = $.fn.examplePlugin.defaults;
}
$this = $(this);
var data = $this.data('examplePlugin');
if (!data) {
$this.data('examplePlugin', {
target: $this
});
$.each(_options, function(key, value){
$this.data('examplePlugin')[key] = value;
});
data = $this.data('examplePlugin');
}
//Cache dom fragment plugin is in (if passed)
if (data.domContextSelector == null || data.domContextSelector == "") {
data.domContext = $(body);
} else {
data.domContext = $(data.domContextSelector);
}
init($this);
});
}
};
var init = function(element) {
data = getData(element);
//Storing dom elements to avoid lookups
data.relatedElement = $(data.relatedElementSelector, data.domContext);
element.click(function(event){
doSomethingCool($(event.currentTarget));
});
};
var doSomethingCool = function(element) {
data = getData(element);
element.slideUp();
data.relatedElement.slideDown();
};
var adjustHeight = function(element) {
data = getData(element);
element.height(data.relatedElement.height());
};
var getData = function(element) {
return $(element).data('examplePlugin');
};
$.fn.examplePlugin = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
}
else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
}
else {
$.error('Method ' + method + ' does not exist on jQuery.examplePlugin');
}
return false;
};
$.fn.examplePlugin.defaults = {
defaultA: 'something',
relatedElementSelector: '#related',
domContextSelector: 'header.header'
};})(jQuery);
Yup, if you follow the jQuery guide, you are building it according to how it's supposed to be built and taking advantage of what it was designed to do (especially chaining).
However, I don't necessarily follow that path. There are a lot of ways you can do these plugins, take for example this guy who made a boilerplate for jQuery plugins which are NOT based on jQuery's design but rather in the OOP perspective (which I prefer). I see it as cleaner, but has the sacrifice of not following the usual syntax (the element.myPlugin({options}) and not being able to chain (until you modify a bit)
The same guy has an older post which is a boilerplate for the usual jQuery plugin design.
I've found your tweet when checking how my plugin saves a state, while learning plugin developing along this tutorial:
http://tutsplus.com/lesson/head-first-into-plugin-development/
In this massive lesson, we’ll dive into jQuery plugin development.
Along the way, we’ll review various best practices and techniques for
providing the highest level of flexibility for the users of your
plugins.
Personally, I suggest sticking to what the jQuery team recommends, in terms of plugin design patterns. It helps keeps consistency, and makes your plugin more community friendly.
Having said that...
I've run into the problem of trying to keep the state of multiple elements as well. One solution I've found is to use the jQuery Data API (which looks like this: $( selector ).data( key, value ) ) to keep meta information like an element's state or the application state.
The nice thing about using data() is that it's not updating/acessing the DOM, rather it's using jQuery's internal meta stuff, so it's faster to access than trying to store info hidden input fields, changing class names, or doing other funky tricks that developers have tried to use to store data on the clientside. ( Keep in mind too that you don't need to use the HTML5 doctype to use the data API, but if you do data-*key attributes are extremely helpful! )
It gets tricky when all the elements have their own states but the current element is the one that is controlling the overall plugin state. For this scenario I use the body tag to store data bout the current element, something like this:
$('body').data('myPluginNameSpace.current', selectorRef );
That way, when I need to check the state of my plugin/page/application, or listen for my plugin-specific event that's bubbled up to the document object, I can do a quick lookup for the current/selected element, and apply any UI changes or behaviors to it:
var currentElementRef = $('body').data('myPluginNameSpace.current');
doFunStuff( currElementRef );
There are a number of other ways you can do this too, like creating a custom Event object and attaching custom parameters to it:
var myPluginEvent = jQuery.Event( 'customEvent.myPluginNameSpace', { myProp : myValue });
$( document ).trigger( myPluginEvent );
When your custom Event gets triggered and later handled via a callback function, your custom parameters are attached to the Event Object passed to the handler:
$( document ).on( 'customEvent.myPluginNameSpace', function( e ){
doStuff( e.myProp ); //you can access your custom properties attach to the event
});
You can get to the same destination via many, different roads; that's the beauty and horror of JavaScript.
In your particular case keep in mind that you don't have to have everything running inside return this.each({ }) portion of the methods.init function for your plugin:
For example, unless you are setting specific options for each element, I would take out the part where you're extending the options object for every element!
var methods = {
init: function(options) {
//DO OPTIONS/EVENTLISTENER/etc STUFF OUT HERE
return this.each(function() {
//DONT DO THIS
if (options) {
_options = $.extend($.fn.examplePlugin.defaults, options);
} else {
_options = $.fn.examplePlugin.defaults;
}
Try this instead:
...
var methods = {
init : function( options ){
//do setup type stuff for the entire Plugin out here
var _options = $.MyPlugin.options = $.extend( defaults, options );
//add some listeners to $(document) that will later be handled
//but put them in an external function to keep things organized:
//methods.addListeners()
//this refers to the array of elements returned by $(selector).myPlugin();
//this.each() iterates over, EACH element, and does everything inside (similar to Array.map())
//if the selector has 100 elements youre gonna do whats in here 100 times
return this.each(function(){
//do function calls for individual elements here
});
},
Also, taking advantage of custom events will help you! Add some event listeners to the document object, and let the event handlers figure out which element to interact with using the data API or custom event parameters.

Categories

Resources