Currently I am dealing with zombies in my backbone.js application. I have read this exelent article http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/ about zombies and extended my project with this:
Backbone.View.prototype.close = function(){
this.remove();
this.unbind();
if (this.onClose){
this.onClose();
}
}
My question is, how to revert this close process? Can I just call render on the object or do I have to re initiate the object by just overwriting it with a new instance?
The this.remove() call removes the view's el from the DOM:
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this.$el.remove();
this.stopListening();
return this;
},
so you'd have to recreate that and rebind all the DOM events. You'll also lose all the Backbone event bindings during the this.unbind() and this.stopListening() calls. Then there's whatever your onClose does (if you have one) so you'd need "undo onClose" methods.
Basically, don't try to re-use views, just destroy them (with proper cleanup) and create new ones. Your views should be lightweight enough that killing and rebuilding them shouldn't matter.
Related
I am having an issue in my Backbone Marionette application where my child views are not being destroyed completely. How do you properly destroy a nested layout view that you are replacing with another layout/item view?
I was under the impression from the Marionette documentation on destroying layout views, that when I set a region to display a new view, that the old view is destroyed. However, events that are triggered via vent are still visible by the old view that was supposedly destroyed.
I created a sample of this issue here: https://jsfiddle.net/dhardin/5j3x2unx/
I believe the issue stems from my router:
App.Router = Marionette.AppRouter.extend({
routes: {
'': 'showView1',
'view1': 'showView1',
'view2': 'showView2'
},
showView1: function() {
var view1 = new App.View1();
App.Layout.mainRegion.empty();
App.Layout.mainRegion.show(view1);
},
showView2: function() {
var view2 = new App.View2();
App.Layout.mainRegion.empty();
App.Layout.mainRegion.show(view2);
}
});
The App.Layout.mainRegion.empty() is not required to my understanding as that is taken care of when the view is destroyed in the Region Manager's show() function.
To see the issue, navigate to another view via navigation, and click the button. You will see that the alert is fired for both the old view and the new view.
Back in my pre-marionette applications, I followed a clean-up pattern to avoid these memory leaks discussed here.
Essentially, my displayed view would call the following function when my app changes to a new view:
Backbone.View.prototype.close = function(){
this.remove();
this.unbind();
}
Please let me know if you need any additional info. Thanks in advance!
For cases such as this you should take advantage of the onDestroy function to do additional clean-up work beyond what Marionette provides. Marionette automatically calls onDestroy when a view is replaced or removed.
onDestroy: function() {
App.vent.off('ButtonClicked', this.onButtonClicked, this);
}
From the Marionette documentation:
By providing an onDestroy method in your view definition, you can
run custom code for your view that is fired after your view has been
destroyed and cleaned up. The onDestroy method will be passed any arguments
that destroy was invoked with. This lets you handle any additional clean
up code without having to override the destroy method.
See the working fiddle here: https://jsfiddle.net/ocfn574a/
Note that I did update a typo in your routes config: 'showVeiw1' -> 'showView1'
You should be using this.listenTo(App.vent, 'ButtonClicked', this.onButtonClicked) instead of App.vent.on('ButtonClicked', this.onButtonClicked, this); this way marionette takes care to take off all the listeners when the view is destroyed and you do not need to explicitly handle onDestory event to take off the listener. see the updated fiddle here.
So basically there is no issue in your router but there is issue in registering the listener since the listener is not present in the view object it is not getting unregistered.
So Im new at backbone, and Im trying to make a single page app, Im using routes to manage certain things, and I want to remove a view when the user gets to another route
Im using this method to destroy the view
destroy_view: function() {
// COMPLETELY UNBIND THE VIEW
this.undelegateEvents();
this.$el.removeData().unbind();
// Remove view from DOM
this.remove();
Backbone.View.prototype.remove.call(this);
}
also this is my route element
Router = Backbone.Router.extend({
routes: {
'':'index',
'#':'index',
'events/*event' : 'events'
},
index: function(){
this.indexView = new VistaIndex();
},
events: function(params) {
if( this.indexView )
this.indexView.destroy_view()
this.eventView = new EventView({currentEvent: params})
}
});
the problem with this is that if I do this the app crashes, so what do you recommend me to do :)
Here’s how I do it:
Backbone.View.extend({
//some other view stuff here...
destroy: function () {
this.undelegateEvents();
this.$el.removeData().unbind();
this.remove();
//OR
this.$el.empty();
}
});
First we want to make sure we’re removing all delegated events (the ones in the events:{"event selector": "callback"} hash). We do this so we can avoid memory leaks and not have mystery bindings that will fire unexpectedly later on. undelegateEvents() is a Backbone.View prototype function that removes the view’s delegated events. Simple.
Next we want to cleanup any data in the view object that is hanging around and unbind any events that we bound outside the events hash. jQuery provides a removeData() function that allows us to to do that.
You may also have bound event listeners to your view chain unbind() with no arguments to remove all previously-attached event handlers from your $el. this.$el.removeData().unbind();
Now you may want to do one of two things here. You may want to remove your view element completely OR you just want to remove any child elements you’ve appended to it during it’s life. The latter would be appropriate if, for example, you’ve set the $el of your view to be some DOM element that should remain after your view behavior is complete
In the former case, this.remove() will obliterate your view element and it’s children from the DOM.
In the later case, this.$el.empty() will remove all child elements.
Check out this fiddle if you want to fool around with my solution.
http://jsfiddle.net/oakley349/caqLx10x/
We are using Backbone views to enhance our app, and the architecture of our application dictates that we attach our views to existing DOM elements.
We recently encountered a situation in which it is necessary to detach a Backbone view from a DOM element without removing the DOM element itself. Thus, we cannot use View.remove() since it will in turn invoke this.$el.remove();
I had originally created the following method for detaching the view, but I am concerned that I may be causing a memory leak:
detach: function() {
this.stopListening();
this.unbind();
this.setElement(null);
}
This effectively replaces the View's element with empty jQuery object, and it unbinds all Backbone events. However, it occurred to me that jQuery might be storing a reference to the new empty object. I am not entirely clear on jQuery's internals so I can't comment on the effectiveness of this method.
I then modified the method as follows:
detach: function() {
this.stopListening();
this.unbind();
this.undelegateEvents();
this.el = null;
this.$el = null;
}
I think this better achieves the desired result as it removes all references to the DOM and to jQuery. I am not 100% confident in this approach, so I was wondering how others have handled this scenario.
Does this method seem sound? Is there a better way to achieve this?
Extend the View and override remove so that it no longer calls this.$el.remove();
Example
var EnhanceView = Backbone.View.extend({
remove: function() {
this.stopListening();
return this;
}
});
should be as simple as that.
I need to run a layout script as soon as my views are inserted into the DOM. So...
$(".widgets").append(widgets.render().el)
$(".widgets .dashboard").isotope # <-- This needs to be called whenever new widgets are inserted
The problem is I have to insert new widgets a few different views and re-call this script a few different places, which is not DRY. I am wondering how I can define the isotope in the View class.
Would it be a good idea to define an event listener to watch for append into the ".widgets" and to run the script? Is there a built in way of building views that are smart about when they are added to the DOM?
(For that matter, it would be also useful to define a callback for when a View is removed from the DOM.)
How about calling the isotope each time the view is rendered? You'll need to be careful to call render() only after the widget is injected, but this ought to take care of your problem:
//in Backbone.view.extend({
initialize: function() {
// fix context for `this`
_.bindAll(this);
},
render: function() {
// .. do rendering..
this.isotope();
return this;
}
// }) // end .extend
use:
var self = this;
this.$el.on('DOMNodeInserted', function(evt){
self.isotope();
$(evt.target ).stopPropagation();
})
In my view I don't declare this.el because I create it dinamically, but in this way the events don't fire.
This is the code:
View 1:
App.Views_1 = Backbone.View.extend({
el: '#content',
initialize: function() {
_.bindAll(this, 'render', 'renderSingle');
},
render: function() {
this.model.each(this.renderSingle);
},
renderSingle: function(model) {
this.tmpView = new App.Views_2({model: model});
$(this.el).append( this.tmpView.render().el );
}
});
View 2:
App.Views_2 = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
},
render: function() {
this.el = $('#template').tmpl(this.model.attributes); // jQuery template
return this;
},
events: {
'click .button' : 'test'
},
test: function() {
alert('Fire');
}
});
});
When I click on ".button" nothing happens.
Thanks;
At the end of your render() method, you can tell backbone to rebind events using delegateEvents(). If you don't pass in any arguments, it will use your events hash.
render: function() {
this.el = $('#template').tmpl(this.model.attributes); // jQuery template
this.delegateEvents();
return this;
}
As of Backbone.js v0.9.0 (Jan. 30, 2012), there is the setElement method to switching a views element and manages the event delegation.
render: function() {
this.setElement($('#template').tmpl(this.model.attributes));
return this;
}
Backbone.View setElement: http://backbonejs.org/#View-setElement
setElementview.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use
setElement, which will also create the cached $el reference and move
the view's delegated events from the old element to the new one.
Dynamically creating your views in this fashion has it's pros and cons, though:
Pros:
All of your application's HTML markup would be generated in templates, because the Views root elements are all replaced by the markup returned from the rendering. This is actually kind of nice... no more looking for HTML inside of JS.
Nice separation of concerns. Templates generate 100% of HTML markup. Views only display states of that markup and respond to various events.
Having render be responsible for the creation of the entire view (including it's root element) is in line with the way that ReactJS renders components, so this could be a beneficial step in the process of migrating from Backbone.Views to ReactJS components.
Cons: - these are mostly negligible
This wouldn't be a painless transition to make on an existing code base. Views would need to be updated and all templates would need to have the View's root elements included in the markup.
Templates used by multiple views could get a little hairy - Would the root element be identical in all use cases?
Prior to render being called, the view's root element is useless. Any modifications to it will be thrown away.
This would include parent views setting classes/data on child view elements prior to rendering. It is also bad practice to do this, but it happens -- those modifications will be lost once render overrides the element.
Unless you override the Backbone.View constructor, every view will unnecessarily delegate it's events and set attributes to a root element that is replaced during rendering.
If one of your templates resolves to a list of elements rather than a single parent element containing children, you're going have a bad time. setElement will grab the first element from that list and throw away the rest.
Here's an example of that problem, though: http://jsfiddle.net/CoryDanielson/Lj3r85ew/
This problem could be mitigated via a build task that parses the templates and ensures they resolve to a single element, or by overriding setElement and ensuring that the incoming element.length === 1.