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.
Related
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');
},
});
I have the falling events hash -
events:
'click #someButton : 'someFunction'
To close the view I have tried
close:
$("#someButton").unbind("click")
and
`close:
$("#someButton").remove()`
But someFunction is still being fired more than once. How do I unbind this event from the button?
I've also tried
$(#el).find("#someButton").unbind("click") as well
Backbone.js view events are delegated to the view's el (so there is no event bound to your #someButton element but rather when a click event bubbles up to the el it checks to see if the event came from an element matching that selector), that being the case to remove the event you need to remove it from the el, for example
$(this.el).off('click', '#someButton');
If you want to remove all delegated events you can just use the view's undelegate method
Jack's explanation and answer are great. Unfortunately, his code example didn't work for me. Instead of using:
$(this.el).off('click', '#someButton');
I had to use:
this.$el.off('click', '#someButton');
which makes sense to me, because the event was bound to this.$el object.
To add further, I used this.$el.off(); inside an initialize within a subview to destroy all events tied to the subview. The same event would fire X number of times a data refresh was called.
I saw the duplicated targets using:
var $target = $(e.currentTarget);
console.log($target);
I would add a comment to an answer, but I have low reputation.
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.
using Backbone.js with Marionette.js (Go Derick Bailey!). Need to detect when a view is removed from the page. Specifically, I'm overwriting it with another view.
Is there an event I can detect of function I can overload to detect when this happens?
Thanks!
Marionette provides the View.onClose method for this purpose:
Backbone.Marionette.ItemView.extend({
onClose: function(){
// custom cleanup or closing code, here
}
});
In vanilla Backbone you can override the View.remove method:
Backbone.View.extend({
remove: function(){
// custom cleanup or closing code, here
// call the base class remove method
Backbone.View.prototype.remove.apply(this, arguments);
}
});
Neither of these methods will work if you are simply clobbering the view's DOM element. If that is your case, the solution is simple: Don't do that. Remove the previous view explicitly before rendering another view in its place.
The region show function is going to do most of what you are looking for
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#basic-use
And look at the on show event later in the page