Backbonejs Marionettejs zombie views - javascript

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.

Related

Backbone Marionette - Layout View Zombies

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.

Backbone.js render view once again or re-create?

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.

Why is the click event triggered several times in my Backbone app?

I'm making a Backbone.js app and it includes an index view and several subviews based on id. All of the views have been bound with mousedown and mouseup events. But every time I go from a subview to the index view and then go to any of subviews again, the mousedown and mouseup events in the current subview will be triggered one more time, which means when I click the subview, there will be several consecutive mousedown events triggered followed by several consecutive mouseup events triggered.
After looking through my code, I finally found that it's the router that causes this problem. Part of my code is as follows:
routes: {
"": "index",
"category/:id": "hashcategory"
},
initialize: function(options){
this._categories = new CategoriesCollection();
this._index = new CategoriesView({collection: this._categories});
},
index: function(){
this._categories.fetch();
},
hashcategory: function(id){
this._todos = new TodosCollection();
this._subtodolist = new TodosView({ collection: this._todos,
id: id
});
this._todos.fetch();
}
As you can see, I create the index collection and view in the initialize method of the router, but I create the subview collection and view in the corresponding route function of the router. And I tried to put the index collection and view in the index function and the click event in index view will behave the same way as subviews. So I think that's why the mousedown and mouseup will be triggered several times.
But the problem is, I have to use the id as one of the parameters sent to subview. So I can't create subview in the initialize method. What's more, I've already seen someone else's projects based on Backbone and some of them also create sub collection and view in the corresponding route function, but their app runs perfectly. So I don't know what is the root of my problem. Could someone give me some idea on this?
Sounds like you're having a delegate problem because:
all sub views all use the a same <div> element
Backbone views bind to events using jQuery's delegate on their el. If you have a view using a <div> as its el and then you use that same <div> for another view by replacing the contained HTML, then you'll end up with both views attached to that <div> through two different delegate calls. If you swap views again, you'll have three views receiving events through three delegates.
For example, suppose we have this HTML:
<div id="view-goes-here"></div>
and these views:
var V0 = Backbone.View.extend({
events: { 'click button': 'do_things' },
render: function() { this.$el.html('<button>V0 Button</button>'); return this },
do_things: function() { console.log('V0 clicked') }
});
var V1 = Backbone.View.extend({
events: { 'click button': 'do_things' },
render: function() { this.$el.html('<button>V1 Button</button>'); return this },
do_things: function() { console.log(V1 clicked') }
});
and we switch between them with something like this (where which starts at 0 of course):
which = (which + 1) % 2;
var v = which == 0
? new V0({el: $('#view-goes-here') })
: new V1({el: $('#view-goes-here') });
v.render();
Then you'll have the multi-delegate problem I described above and this behavior seems to match the symptoms you're describing.
Here's a demo to make it easy to see: http://jsfiddle.net/ambiguous/AtvWJ/
A quick and easy way around this problem is to call undelegateEvents on the current view before rendering the new one:
which = (which + 1) % 2;
if(current)
current.undelegateEvents(); // This detaches the `delegate` on #view-goes-here
current = which == 0
? new V0({el: $('#view-goes-here') })
: new V1({el: $('#view-goes-here') });
current.render();
Demo: http://jsfiddle.net/ambiguous/HazzN/
A better approach would be to give each view its own distinct el so that everything (including the delegate bindings) would go away when you replaced the HTML. You might end up with a lot of <div><div>real stuff</div></div> structures but that's not worth worrying about.

Backbone JS: can one view trigger updates in other views?

In my simple project I have 2 views - a line item view (Brand) and App. I have attached function that allows selecting multiple items:
var BrandView = Backbone.View.extend({
...some code...
toggle_select: function() {
this.model.selected = !this.model.selected;
if(this.model.selected) $(this.el).addClass('selected');
else $(this.el).removeClass('selected');
return this;
}
});
var AppView = Backbone.View.extend({
...some code...
delete_selected: function() {
_.each(Brands.selected(), function(model){
model.delete_selected();
});
return false;
},
});
Thing is, I want to know how many items are selected. In this setup selecting is NOT affecting the model and thus not firing any events. And from MVC concept I understand that views should not be directly talking to other views. So how can AppView know that something is being selected in BrandViews?
And more specifically, I AppView to know how many items were selected, so if more than 1 is selected, I show a menu for multiple selection.
You might want to have a read of this discussion of Backbone pub/sub events:
http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/
I like to add it in as a global event mechanism:
Backbone.pubSub = _.extend({}, Backbone.Events);
Then in one view you can trigger an event:
Backbone.pubSub.trigger('my-event', payload);
And in another you can listen:
Backbone.pubSub.on('my-event', this.onMyEvent, this);
I use what Addy Osmani calls the mediator pattern http://addyosmani.com/largescalejavascript/#mediatorpattern. The whole article is well worth a read.
Basically it is an event manager that allows you to subscribe to and publish events. So your AppView would subscript to an event, i.e. 'selected'. Then the BrandView would publish the 'selected' event.
The reason I like this is it allows you to send events between views, without the views being directly bound together.
For Example
var mediator = new Mediator(); //LOOK AT THE LINK FOR IMPLEMENTATION
var BrandView = Backbone.View.extend({
toggle_select: function() {
...
mediator.publish('selected', any, data, you, want);
return this;
}
});
var AppView = Backbone.View.extend({
initialize: function() {
mediator.subscribe('selected', this.delete_selected)
},
delete_selected: function(any, data, you, want) {
... do something ...
},
});
This way your app view doesn't care if it is a BrandView or FooView that publishes the 'selected' event, only that the event occured. As a result, I find it a maintainable way to manage events between parts of you application, not just views.
If you read further about the 'Facade', you can create a nice permissions structure. This would allow you to say only an 'AppView' can subscribe to my 'selected' event. I find this helpful as it makes it very clear where the events are being used.
Ignoring the problems with this that you already mention in your post, you can bind and trigger events to/from the global Backbone.Event object, which will allow anything to talk to anything else. Definitely not the best solution, and if you have views chatting with one another then you should consider refactoring that. But there ya go! Hope this helps.
Here is my case with a similar need: Backbone listenTo seemed like a solution to redirect to login page for timed out or not authenticated requests.
I added event handler to my router and made it listen to the global event such as:
Backbone.Router.extend({
onNotAuthenticated:function(errMsg){
var redirectView = new LoginView();
redirectView.displayMessage(errMsg);
this.loadView(redirectView);
},
initialize:function(){
this.listenTo(Backbone,'auth:not-authenticated',this.onNotAuthenticated);
},
.....
});
and in my jquery ajax error handler:
$(document).ajaxError(
function(event, jqxhr, settings, thrownError){
.......
if(httpErrorHeaderValue==="some-value"){
Backbone.trigger("auth:not-authenticated",errMsg);
}
});
You can use Backbone object as the event bus.
This approach is somewhat cleaner but still relies on Global Backbone object though
var view1 = Backbone.View.extend({
_onEvent : function(){
Backbone.trigger('customEvent');
}
});
var view2 = Backbone.View.extend({
initialize : function(){
Backbone.on('customEvent', this._onCustomEvent, this);
},
_onCustomEvent : function(){
// react to document edit.
}
});
Use the same model objects. AppView could be initialized with a collection, and BrandView initialized with one model from that collection. When attributes of a branch object change, any other code that has a reference to that model can read it.
So lets so you have some brands that you fetch via a collection:
var brands = new Brands([]);
brands.fetch();
Now you make an AppView, and an array of BrandView's for each model.
var appView = new AppView({brands: brands});
var brandViews = brands.map(function(brand) {
return new BrandView({brand: brand});
});
The appView and the brandViews now both have access to the same model objects, so when you change one:
brands.get(0).selected = true;
Then it changes when accessed by the views that reference it as well.
console.log(appView.brands.get(0).selected); // true
console.log(brandViews[0].brand.selected) // true
Same as John has suggested above, the Mediator Pattern works really good in this scenario, as Addy Osmani summing this issue up again in Backbone fundamentals.
Wound up using the Backbone.Mediator plugin which is simple and great, and makes my AMD View modules working together seamlessly =)

Backbone refresh view events

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.

Categories