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.
Related
I have searched the internet for ways to trigger the destruction of old views.
There are functions to do this, however, I don't know how to trigger them. Ideally, there would be a way to trigger the destruction on the event of closing a view.
I can't find a way how to trigger that particular event.
You should call view.remove() to trigger its destruction as specified in the documentation http://backbonejs.org/#View-remove
For example, if you had:
var myView = Backbone.View.extend({
initialize: function() {
...
},
render: function() {
...
}
});
You can later call myView.remove() provided you have a reference to myView available.
This method should also remove any event listeners tied to the view if you are using the listenTo (recommended) method as opposed to the on listener. You could also add view.off() to ensure that the events are removed.
Additionally, you will need to add a way for views to listen to a close event so you can call the remove and off methods. You should refer to 1 and 2.
This old but fantastic piece by Derick Bailey does a great job at explaining the issue and how to solve it. As Monica rightly suggested this relies on view.remove() but you can update your router to destroy your existing view -
Try something similar to
if (currentView) {
currentView.remove();
currentView = newView();
}
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/
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
I am trying to play with Backbone using Marionete Module.
eg. implementing "Loading spinner" as it has been done in "contact manager" app by David Sulc, the "backboneye" plugin for Firefox showing me "Zombie View" after the "Spinner" has been replaced by content. Is it "real" zombie as it has "isDestroyed:true" attribute?
Also according to the Chrome plugin "Backbone debugger" the view has been removed
Should I worry about them?
here is controller:
define(["app", "apps/items/itemsView"], function(app, View){
app.module("ItemsApp.List", function(List, app, Backbone, Marionette, $, _){
List.Controller = {
listAllItems: function(){
require(["common/views", "entities/items"], function(CommonViews){
var loadingView = new CommonViews.Loading();
app.main.show(loadingView);
var fetchingItems = app.request("items:entities");
var itemsPageLayout = new View.Layout();
var panelView = new View.Panel();
$.when(fetchingItems).done(function(items){
var allItemsView = new View.Items({collection:items});
itemsPageLayout.on("show", function(){
itemsPageLayout.panelRegion.show(panelView);
itemsPageLayout.itemListRegion.show(allItemsView);
});
app.main.show(itemsPageLayout);
});
});
}
}
});
return app.ItemsApp.List.Controller;
});
You probably do not have a Zombie view.
If your main region uses the default Marionette Region type and your views inherit from Marionette View types (ItemView, CollectionView, CompositeView, Layout), Marionette ensures that Zombie Views are avoided.
When a view is swapped out of a region (which happens when you call app.main.show(itemsPageLayout)), the spinner view element is removed from the DOM and all listenTo-style event handlers unbound.
You can see this in the code for Region._destroyView, which is called on show:
_destroyView: function() {
var view = this.currentView;
if (view.destroy && !view.isDestroyed) {
view.destroy();
} else if (view.remove) {
view.remove();
}
},
Marionette View types have a destroy method that handles removing the View's DOM element and unbinding event handlers (that were bound with listenTo - it is not as simple to unbind events bound with on). Views based on vanilla Backbone.View, rather than a Marionette type, must unbind their own event handlers. Failure to do this correctly is the main cause of Zombie Views. By using Marionette Views, you are protected.
If you use a custom Region type that overrides Region.show, you need to ensure that it calls destroy on the view that is being swapped out.
I have a single page web app with multiple backbone.js views. The views must sometimes communicate with each other. Two examples:
When there are two ways views presenting a collection in different ways simultaneously and a click on an item in one view must be relayed to the other view.
When a user transitions to the next stage of the process and the first view passes data to the second.
To decouple the views as much as possible I currently use custom events to pass the data ($(document).trigger('customEvent', data)). Is there a better way to do this?
One widely used technique is extending the Backbone.Events -object to create your personal global events aggregator.
var vent = {}; // or App.vent depending how you want to do this
_.extend(vent, Backbone.Events);
Depending if you're using requirejs or something else, you might want to separate this into its own module or make it an attribute of your Application object. Now you can trigger and listen to events anywhere in your app.
// View1
vent.trigger('some_event', data1, data2, data3, ...);
// View2
vent.on('some_event', this.reaction_to_some_event);
This also allows you to use the event aggregator to communicate between models, collections, the router etc. Here is Martin Fowler's concept for the event aggregator (not in javascript). And here is a more backboney implementation and reflection on the subject more in the vein of Backbone.Marionette, but most of it is applicable to vanilla Backbone.
Hope this helped!
I agree with #jakee at first part
var vent = {};
_.extend(vent, Backbone.Events);
however, listening a global event with "on" may cause a memory leak and zombie view problem and that also causes multiple action handler calls etc.
Instead of "on", you should use "listenTo" in your view
this.listenTo(vent, "someEvent", yourHandlerFunction);
thus, when you remove your view by view.remove(), this handler will be also removed, because handler is bound to your view.
When triggering your global event, just use
vent.trigger("someEvent",parameters);
jakee's answer suggests a fine approach that I myself have used, but there is another interesting way, and that is to inject a reference to an object into each view instance, with the injected object in turn containing references to as many views as you want to aggregate.
In essence the view-aggregator is a sort of "App" object, and things beside views could be attached, e.g. collections. It does involve extending the view(s) and so might not be to everybody's taste, but on the other hand the extending serves as a simple example for doing so.
I used the code at http://arturadib.com/hello-backbonejs/docs/1.html as the basis for my ListView and then I got the following to work:
define(
['./listView'],
function (ListView) {
var APP = {
VIEWS : {}
}
ListView.instantiator = ListView.extend({
initialize : function() {
this.app = APP;
ListView.prototype.initialize.apply(this, arguments);
}
});
APP.VIEWS.ListView = new ListView.instantiator();
console.log(APP.VIEWS.ListView.app);
}
);