In a Backbone application, I instantiate a view for every model in a collection.
If one of these views is clicked, I want to call a function.
Unfortunately, the function is called n times, where n is the number of models/view instantiated. I’ve managed to get around this by finding out what element has been clicked on, but I still don’t feel comfortable knowing that one event might be triggered 200+ times in the very same moment.
The event is bound like this:
var Item = Backbone.View.extend({
events: {
'click .list-group-item': function(event) { this.doSomething(event); },
},
doSomething: function(event) {
$(event.currentTarget).toggleClass('active');
},
});
In the code above you can also see my workaround using event.currentTarget, but how can I avoid this? Is there a way to distinguish the .list-group-item elements without resorting to event.currentTarget, so preferable right in the moment an element is clicked?
Another approach would be to bind the event to the parent element, so it is only triggered once and then using event.currentTarget, but that also seems kind of fishy to me.
Since you want to attach to a click anywhere in the view, you don't need to specify .list-group-item. Also, you only need to specify the name of the event callback function:
var Item = Backbone.View.extend({
events: {
'click': 'doSomething'
},
doSomething: function(event) {
$(event.currentTarget).toggleClass('active');
},
});
Related
I'm wondering what is the best way to handle the following situation. Imagine we have a collection of records that will each be attached to a view. Each view will have a button that runs particular functionality. For arguments sake, there could be up to one hundred of these views on the page at once. An example of this view would be below:
var RecordView = Backbone.View.extend({
events: {
'click .js-cta': 'onCTAClick'
},
onCTAClick: function(event) {
// Do something.
}
});
This obviously would bind 100 DOM Listeners and take up much more memory than we'd like. One way of handling this would be to have a View for the Collection, and put a single delegated listener at that level, which would determine what view should be notified, and then trigger an event, like so:
var CollectionView = Backbone.View.Extend({
events: {
'click .js-cta': 'onCTAClick'
},
onCTAClick: function(event) {
// Use event data to determine appropriate child view to notify.
// Notify the child view by triggering an event on it (childView.trigger('CTAClick')).
}
});
var RecordView = Backbone.View.Extend({
initialise: function() {
this.on('CTAClick', this.onCTAClick);
},
onCTAClick: function(event) {
// Use event data to determine appropriate child view to notify.
// Notify the child view by triggering an event on it (childView.trigger('')).
}
});
I wonder if this is any better than the 100+ DOM listener method... You'd only have one DOM listener, but then 100 listeners in the RecordView waiting to be notified of the event. Are DOM event listeners "heavier" than Backbone event listeners? You could remove the listeners entirely and have the CollectionView directly trigger a method, reducing listeners to a single one, but this tightly couples the views. Does this tight coupling matter though if these two views are so close in function and are almost interwoven anyway?
What is the correct way of approaching this situation?
IMOH I don't think there is a correct way. Both approaches you outline work. However when dealing with a lot of views, the delegated method leveraging DOM bubbling would be optimal from a memory perspective. In this approach I would probably be inclined to store an id of the record view in the record view DOM as a data attribute, and store a map of the view against this id in the collection view. Then just call the method directly. Wouldn't worry about it being tightly coupled.
I am attempting to extend existing jQuery widgets with Backbone events so they can publish Backbone events that can be listened to. My strategy is to proxy the event inside of the widget's native event callback method. For example, in the jQueryUI slider widget's change event, I want to trigger an event named "trigger." My conundrum is that this code works as intended:
$(function() {
var $slider = $("#slider");
_.extend($slider, Backbone.Events);
$slider.on("trigger", function(msg){
alert("triggered: " + msg)
});
$slider.slider({
change: function(event, ui) {
$slider.trigger("trigger", ui.value);
}
});
});
Which is 3/4 of the way to where I want to go, but I'd prefer to be able to just do something like this in the change event:
change: function(event, ui) {
$(this).trigger("trigger", ui.value);
}
...to completely encapsulate the widget and not worry about the actual singleton instance of the widget. My issue is that this second approach doesn't work and I was wondering if someone can explain to me why. In Firebug, both $(this) and $slider point to the same DOM element.
The context of your change handler (this) is the DOM element which triggered the event. This is the same DOM element you've already assigned to your $slider variable.
I think you may be complicating things unnecessarily, though it might be that I don't fully understand what you're trying to do. However, you should be able to pick up events triggered by plug-ins on the child elements of a backbone view using the built-in events hash. Try the following (and see fiddle here):
var SliderView = Backbone.View.extend({
// Set the view's el property to existing element in DOM
el: '#slider-view',
events: {
// jQuery UI slider triggers a slidechange event which we can subscribe
// to using the view's event object
'slidechange #slider': 'handleSliderChange'
},
render: function() {
// Worth caching a reference to the element in case we want to refer
// to it again elsewhere in the view - maybe to clean it up on exit
this.$slider = this.$el.find('#slider').slider({});
},
handleSliderChange: function(e, ui) {
// Our handler is invoked with the same ui parameter received
// by the slider's change handler.
console.log('Slider changed: ', e, ui.value);
}
});
// Create and render an instance of the view
new SliderView().render();
You can use _.extend(jQuery.fn, Backbone.Events) to add all the event methods to anything wrapped in $()
Have look at this jsFiddle.
http://jsfiddle.net/Zct7D/
It works similarly to what you are looking for. The biggest shortfall I see is that you have to .trigger on the same reference you bound (.on) to.
In the example I use.
var derpDiv = $("#derp").on("....
derpDiv.trigger("update....
That works. But something like this does not appear to work.
$("#derp").on("update", function() {});
// somewhere else in the code
$("#derp").trigger("update", "Message");
But with some more tweaking it could be made to work.
I've been working with Backbone a few days, reading up on design patterns and what have you. Today I was messing with sub-views, after reading a bunch of resources. Primarily, these 2 posts-
Derrick Bailey
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Ian Storm Taylor
http://ianstormtaylor.com/assigning-backbone-subviews-made-even-cleaner/
These and others were very useful for helping me set up some subViews and handle their closing in what I thought was a correct pattern:
Backbone.View.prototype.close = function(){
var ctx = this;
_.each(ctx.subViews(), function(view) {
view.close();
});
this.remove();
this.unbind();
}
No problems here, seems to do what I expected. But I wanted to test it, just to see what happened. So I stopped calling close on subViews and looped my render like 20,000 times:
Backbone.View.prototype.close = function(){
var ctx = this;
_.each(ctx.subViews(), function(view) {
//view.close();
});
this.remove();
this.unbind();
}
No zombie event handlers or DOM nodes here. This was a little surprising to me - I'm not an expert in jQuery's internals and I expected to still have the event handlers from the child nodes at least. But I guess because my subViews are all contained within the parent view, which was still being removed and unbound, jQuery clears all the children fine. So I stopped unbinding the parent element:
Backbone.View.prototype.close = function(){
var ctx = this;
_.each(ctx.subViews(), function(view) {
//view.close();
});
this.remove();
//this.unbind();
}
My event handler count in the Chrome inspector still didn't go up.
So my question are:
What is a "real" example of when you need to cleverly handle event unbinding and subViews in this way? Is it any object reference outside of the immediate scope of your View? Is it only if your subviews aren't contained by the parent view's $el?
When you remove a parent view from the DOM, jQuery does clean up any DOM events that were hooked up in the children. unbind() is an alias for Backbone's Events.off, which removes any events you may have hooked up using myChildView.on('someEvent', ...). For example, a parent view might listen to an event you trigger inside a child view. If you did that, you would need the call to this.unbind() or this.off().
Now that Backbone.Events (as of 0.9.9) has listenTo() and stopListening(), you could consider adding this.stopListening() to your close(). Then if, within your view, you used something like this.listenTo(this.model, ...) they would also be cleaned up properly.
I'm wondering about where events should be set in a parent-child relationship. It's not a specific problem, just a best practices I guess. My specific case:
I have a Dropdown which contains of a Button and a List. Describing the action, you press the button and the list appears. You click an item in the list, and that item is selected and the list disappears.
So the first thing is that since the view is initialized as a Dropdown, the person making it shouldn't need to reach in and deal with the Button or List view. All methods should be called on the parent view, and events might need to be bubbled up from the child.
So for example, instead of doing: this.dropdown.button.press();, Dropdown provides its own press method which just calls Button's press method.
When the user presses the button, Button fires a press event that Dropdown is listening to.
onButtonPress : function () {
if (!this.isExpanded()) {
this.expand();
}
this.trigger('press');
},
And Dropdown triggers press on itself, so that developer can get the press event without reaching in to dropdown.button.
Here's where the first question comes. Should Dropdown expand itself in onButtonPress or should onButtonPress just trigger a press, and then have the expansion listening on Dropdown's own press event:
onButtonPress : function () {
this.trigger('press');
},
onPress : function () {
if (!this.isExpanded()) {
this.expand();
}
},
And then it gets more complicated, should Dropdown's expand method just trigger expand on itself:
expand : function () {
if (this.isEnabled()) {
this.setState('expanded', true);
this.trigger('expand');
}
return this;
},
onExpand : function () {
this.list.show();
},
or should it be the one that shows the List:
expand : function () {
if (this.isEnabled()) {
this.setState('expanded', true);
this.list.show();
this.trigger('expand');
}
return this;
},
I guess I'm just wondering about best practices for deciding where to bind events in a parent/child relationship. How to avoid confusing situations and possibly circular event calling.
Anyone have any thoughts?
I would simply use the DOM for this instead of triggering an event manually. If you want to have an event hit the parent first, then set the capture to true, or false if you want the event to bubble.
this allows an event handler to be called during the capture phase, so parent DOM elements get event first:
el.bind('someevent', somefunction, true);
this bubbles, hence children receive event first, and handler is called when children receive event.
el.bind('someevent', somefunction, false);
In general keep it simple. You can also use the delegation programming pattern where the parent will get the events for itself and the children, so you can avoid confusion and potentially can save memory with larger applications.
Using a Backbone.js View, say I want to include the following events:
events: {
'click a': 'link',
'click': 'openPanel'
}
How can I avoid openPanel to be fired when I click on a link. What I want is to have a clickable box which will trigger an action, but this box can have elements which should trigger other actions, and not the parent action. Think for example Twitter.com, and links in Tweets/right hand panel.
I've been using e.stopImmediatePropagation(); in order to keep the event from propagating. I wish there was a shorter way to do this. I would like return false; but that is due to my familiarity with jQuery
The JQuery preventDefault method would also be a good option.
window.LocationViewLI = Backbone.View.extend({
tagName: "li",
template: _.template('<%= name %>'),
events: {
"click a": "handleClick"
},
handleClick: function(event) {
event.preventDefault();
console.log("LocationViewLI handleClick", this.model.escape("name") );
// debugger;
},
...
Each of your event handlers will be passed an event object when it's triggered. Inside your handler, you need to leverage jQuery's event.stopPropagation() method. For example:
link: function(event) {
//do some stuff here
event.stopPropagation();
}
Two other methods that might work for you:
1
events: {
'click a': 'link',
'click *:not(a, a *)': 'openPanel'
}
Then openPanel will not capture click events on any <a> or child of an <a> (in case you have an icon in your <a> tag).
2
At the top of the openPanel method, make sure the event target wasn't an <a>:
openPanel: function(event) {
// Don't open the panel if the event target (the element that was
// clicked) is an <a> or any element within an <a>
if (event && event.target && $(event.target).is('a, a *')) return;
// otherwise it's safe to open the panel as usual
}
Note that both of these methods still allow the openPanel function to be called from elsewhere (from a parent view or another function on this view, for example). Just don't pass an event argument and it'll be fine. You also don't have to do anything special in your link function -- just handle the click event and move on. Although you'll probably still want to call event.preventDefault().
Return "false" in your "link" function.