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/
Related
I'm trying to understand whether is necessary to unbind an event that is binded on the current instance of a view.
For instance when I do:
$(this.el).on('click', callback);
is it necessary to unbind the events (e.g. using off() or $(this).unbind('click') inside the callback function) or maybe the view will destroy the event and will give it to garbage collector?
You should setup all of your events via the View's events hash and only unbind them when removing a view via .remove().
Backbone Views use Event Delegation for the DOM Event handlers, so you can set up events for View HTML that doesn't exist yet, and, once the HTML is generated, the event handlers will catch the events as expected. All events handlers are attached to the views root element and watch for specific events that occur within that element or it's children.
Events will be unbound when you remove the view via Backbone.View.remove()
view.remove();
If you need to unbind events while the view is displayed (not common), you can specifically unbind that event via jQuery's .off(), but, you shouldn't have to (or want to) manage binding/unbinding your events.
The problem with manually unbinding events is that you may/probably will quickly find yourself conditionally unbinding and binding these event handlers according to user input. You'll go down the path of "unbind an event here, rebind it here, but unbind when this condition is true or this one is false"... it gets confusing, fragile and unmaintainable very quickly.
Instead, you should keep your DOM Bindings bound all of the time and have their execution dependent on the State of the view... sometimes the event handlers may do nothing, but that's fine. With this style of writing views, you're only concern with DOM events is that you remove your views properly. Inside the the view's state, you can consolidate the business logic behind when the views should respond to certain events.
What does State look like, in code?
initialize: function {
this.state = new Backbone.Model({ /* initial state */ });
}
Boom. It's that easy. State is just an object (or backbone model) where you can store data about the current state of the view. It's like the Views little junk drawer of useful data.
Should the save button be disabled?
this.state.set('save_button_disabled', true);
this.state.set('save_button_disabled', false);
Is the form validated? Errors?
this.state.set('form_valid', false);
this.state.set('form_errors', errorsArray);
Then bind some handlers to it, and when user does something, update the state and let the handlers handle it. Recording and responding to state changes will force you to write your views with lots of small functions that are easy to test, easy to name and easy to maintain. Having a dedicated object to store state is a great way to organize and consolidate the logic & conditions within the view.
this.listenTo(this.state, {
'change:save_button_disabled': this.toggleSaveButton,
'change:form_valid': this.onFormValidationChange
});
You can also tap into state within your views event handlers:
events: {
'click button.save': 'onSaveClicked'
},
onSaveClicked: function() {
if ( this.state.get('form_valid') ) {
/* do save logic */
}
}
As your application grows, you might also want to look into separating state into View State, Environment State (test, prod, dev, versions etc), User State (logged in/out, admin, permissions, users birthday? etc) and others. View State is usually the first step.
Your view should essentially be a bunch of small concise functions that respond to DOM, State and Model/Collection events. It should not contain the complex logic that evaluates, responds to and interprets user input and model data.. that complex stuff should exist in the Collections, Models and State. The view is simply a representation of those items and interface for the user to interact with them, just like the front-end of a web-app is an interface for the user to interact with a database.
View Code:
var MyView = Backbone.View.extend({
events: {
"click": "onClick"
},
"onClick": function() {
if ( this.state.get('clickable') ) {
/* do callback */
}
},
initialize: function(options) {
this.options = options;
this.state = new Backbone.Model({ clickable: true });
},
getTemplate: function() { /*...*/ },
render: function() {
var template = this.getTemplate(),
data = {
options: this.options,
data: this.model.toJSON(),
state: this.state.toJSON()
};
return this.$el.html(template(data));
}
});
Template Code:
{{#if state.logged_in}}
<p {{#if options.large}}class="font-big"{{/if}}>
Welcome back, {{data.userName}}.
</p>
{{else}}
<p>Hello! {{#sign_up_link}}</p>
{{/if}}
Here's a simple example: http://jsfiddle.net/CoryDanielson/o505ny1j/
More on state and this way of developing views
In a perfect world, a View is a merely a interactive representation of data and the many different states of an application. Views are state machines. The view is also a small buffer between the user and data. It should relay the user's intentions to models/collections as well as the model/collection responses back to the user.
More about this:
Model-View-Intent: http://futurice.com/blog/reactive-mvc-and-the-virtual-dom
Model-View-Intent slides: http://staltz.com/mvi-freaklies/#/
Also read the react.js docs to learn more about State as it applies to reactjs components which are an even lower-level representation of things than Backbone.Views. http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html
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.
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.
when my page opens, I call the collection and populate the view:
var pagColl = new pgCollection(e.models);
var pagView = new pgView({collection: pagColl});
Separately (via a Datepicker), I wish to want to populate the same collection with different models and instantiate the view again.
The problem I have is how to close the original pagView and empty the pagColl before I open the new one, as this "ghost view" is creating problems for me. The variables referred to above are local variables? is it that I need to create a global pagColl and reset() this?
well there has been many discussion on this topic actually,
backbone does nothing for you, you will have to do it yourself and this is what you have to take care of:
removing the view (this delegates to jQuery, and jquery removes it from the DOM)
// to be called from inside your view... otherwise its `view.remove();`
this.remove();
this removes the view from the DOM and removes all DOM events bound to it.
removing all backbone events
// to be called from inside the view... otherwise it's `view.unbind();`
this.unbind();
this removes all events bound to the view, if you have a certain event in your view (a button) which delegates to a function that calls this.trigger('myCustomEvent', params);
if you want some idea's on how to implement a system I suggest you read up on Derrick Bailey's blogpost on zombie views: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/.
another option
another option would be to reuse your current view, and have it re-render or append certain items in the view, bound to the collection's reset event
I was facing the same issue. I call the view.undelegateEvents() method.
Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily.
I use the stopListening method to solve the problem, usually I don't want to remove the entire view from the DOM.
view.stopListening();
Tell an object to stop listening to events. Either call stopListening
with no arguments to have the object remove all of its registered
callbacks ... or be more precise by telling it to remove just the
events it's listening to on a specific object, or a specific event, or
just a specific callback.
http://backbonejs.org/#Events-stopListening
Here's one alternative I would suggest to use, by using Pub/Sub pattern.
You can set up the events bound to the View, and choose a condition for such events.
For example, PubSub.subscribe("EVENT NAME", EVENT ACTIONS, CONDITION); in the condition function, you can check if the view is still in the DOM.
i.e.
var unsubscribe = function() {
return (this.$el.closest("body").length === 0);
};
PubSub.subscribe("addSomething",_.bind(this.addSomething, this), unsubscribe);
Then, you can invoke pub/sub via PubSub.pub("addSomething"); in other places and not to worry about duplicating actions.
Of course, there are trade-offs, but this way not seems to be that difficult.
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.