Backbone reusable view (set new model to existing view) - javascript

What is the best approach for the following setting: A long list if items (several hundred persons) and after clicking on a list entry a new dialog opens with item details.
There are several options (see also here):
use a "dummy" model and one view and change the attributes of the dummy models -> does not reflect changes to the original model
change the model of one already existing view
everytime a click on the list is made a new view is created which the model -> performance issues?
use the marionette framework -> there is little documentation which makes it hard to understand for me
use JSPerf View -> I tried the online demo but on fast scrolling there were several errors
I tried option 2 but there seem to be memory leakes.
ReusableView = Backbone.View.extend({
setModel: function( model) {
// unbind all events
this.model.stopListening();
this.undelegateEvents();
// set new model
this.model = model;
this.delegateEvents({
"click": "onClicked",
"mouseover": "onMouseOver"
});
this.initialize();
}
});
Here is a fiddle were lots of models can be created showed to the user with a single view. Type in the number of models to be created and click on create models.
Questions: Why are there memory leakes? How can I properly clean up after using a model?
For memory allocation I used chrome and its task manager. Memory consumption around 30M for 70000 views.

This is the solution I figured out after reading and testing a lot:
setModel: function( model) {
// unbind all view related things
this.$el.children().removeData().unbind();
this.$el.children().remove();
this.stopListening();
// clear model
if ( this.model) {
this.model.unbind();
this.model.stopListening();
}
// set new model and call initialize
this.model = model;
this.delegateEvents( this.events); // will call undelegateEvents internally
this.initialize();
}
The overall view stayed the same and all children are changed.
You can find the fiddle here http://jsfiddle.net/stot/H4qPG/20/
I created 1.000.000 models and according to chrome task manager the memory consumption was stable over the loooong time of this test.
Useful information:
Destroy or remove a view in Backbone.js
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/

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.

Efficient ways to remove a nested view in Backbone

I'm creating a single page app. I want to remove a view when switching to another view. In the current view, I have a collection of photos. How can I remove the view and the views of photo collection efficiently without memory leaks?
I did the following:
destroy method:
destroy: function(){
this.undelegateEvents();
this.stopListening();
this.$el.empty();
collection.reset();
}
event:
this.listenTo(collection, 'reset', this.resetBoard);
event handling:
resetBoard: function(collection, options){
var models = options.previousModels;
_.each(models, function(model){
model.id = null;
model.destroy();
});
}
here I empty the $el first ($el wiil be used by other views), so that DOM operation can be done at one time. Next, I reset the collection and destroy those models and related views.
Is this logic correct? Is there any better solutions?
This is a good article on preventing memory leaks in Backbone Views:
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
I don't think you need to explicitly remove the models and collections from memory. You just need to make sure that the view is no longer listening for changes in them. This a great overview of garbage collection in javascript and backbone specifically:
What does backbone.js do with models that are not used anymore
In any case, the use of model.destroy() seems a little weird/hacky to me because I think of it as a server operation (deleting the model from the server) and I also wonder if resetBoard will ever execute since you have already called stopListening() when the collection is reset.

BackboneJs - Should model or Collection have knowledge of view

In one of the example i picked from SO answers here and many BackBoneJs examples i see that the initialize function knows which view the model is going to be rendered with. I don't know i am kind of biased now, is this a good practice or it depends on type of application being developed.
Example
http://jsfiddle.net/thomas/Yqk5A/
Edited Fiddle
http://jsfiddle.net/Yqk5A/187/
Code Reference
FriendList = Backbone.Collection.extend({
initialize: function(){
this.bind("add", function( model ){
alert("hey");
view.render( model );
})
}
});
Is the above a good practice or below
var friendslist = new FriendList;
var view = new FriendView({el: 'body'});
friendslist.bind("add", function( model ){
alert("hey" + model.get("name"));
view.render( model );
})
in the edited fiddle collection is rendered by a view, and we also can use many more views to render the collection.
I am all for using events,
I myself don't want to move the bind's outside the model, I'd keep them there like the original example
var app = {};
app.evt = _.extend({}, Backbone.Events); // adding a global event aggregator
FriendList = Backbone.Collection.extend({
initialize: function(){
this.bind("add", function( model ){
alert("hey");
app.evt.trigger('addfriend', model);
})
}
});
//further in your code you can bind to that event
app.evt.bind("addfriend", function(model){
var view = new FriendView({el: 'body'});
view.render(model);
});
however, i find the example a bit weird, creating a new view, with body as element, and rendering it with giving a model to the render function. i'd find it more logic if the view is created with a model as attribute, and then rendering the content into the body. but thats another subject all together.
in short, i move creating the view outside, listening to an event being triggered, but the bind on the collection stays in the collection code. i find it more managable to keep all the collection code at the same place.
I don't think the collection should know about the view that is used to render it. I know in my projects, the same collection is render in multiple ways so that approach would deteriorate rapidly.
In general I pass the collection to the view that renders the collection and the view will listen to add/remove/update events of the collection to render the elements. The collection view will have knowledge of the child view.
Check out the following link (3rd blog in a series) and in particular the UpdatingCollectionView. This is the approach that I've found useful.
http://liquidmedia.ca/blog/2011/02/backbone-js-part-3/

A Backbone.js view that is a simple select list

I've built a Backbone-powered library that allows a user to add/remove items, much like the Todos example.
Every time an item is add or removed - or the entire collection is refreshed - I need two other select elements that are on other areas of the page to re-populate with the latest items as options. How would this be implemented, do I simply re-populate the select element in the render function of the view which holds a reference to the collection?
I'm tempted to create a view just for the select options but this seems like overkill, especially when considering the view doesn't need to re-act to any events. The select options are used by other views to populate form data.
You're correct: create a unique view for each select option. It's not overkill at all; that's what Views are for. They listen for events from their models, in this case the item list, and redraw themselves upon receiving an event. They have container designations, so once you've established those in the parameters for the View subclass, you never need to think about them again. You can style them independently.
That's the whole point of the Views being the way they are.
More importantly, you could also abstract out "view of a list of things," and then each of your specific views can inherit from that view, and add two features: the filter ("latest"), and the renderer. You have to write the renderer anyway; you may as well exploit a little syntatic sugar to make it clear what you're rendering where. It's better than writing comments.
Not to distract from Elf Sternberg's already excellent answer, but to add a little context:
Don't loop over collections in your templates
If you want to do this, you might as well just use HTML partials and
AJAX. Instead, use a Backbone View that renders its own views (the
granularity is what minimizes server syncs and page refreshes). This
is recursive, you can repeat this pattern until there is no more
associated data to loop over.
— Jonathan Otto in A Conceptual Understanding of Backbone.js For The Everyman
Of course, there are a few gotchas when you want to render subviews.
I did a code search to try and find some examples of how to do this. Turns out that the TodoMVC example is a good model for doing this. From the Strider-CD source (MIT License):
var UserView = Backbone.View.extend({
//... is a class. not sure how to put that here
tagName: "option",
// Cache the template function for a single item.
template: _.template($('#user-item-template').html()),
// The DOM events specific to an item.
// maybe could put links here? but then user couldn't see on mouse-over
// The UserView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between a **User** and a **UserView** in this
// app, we set a direct reference on the model for convenience.
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.model.bind('destroy', this.remove);
},
// Re-render the contents of the User item.
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
// Remove the item, destroy the model.
clear: function() {
this.model.clear();
}
});
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
var UsersView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: $("#user-form"),
// no events here either at this time
// At initialization we kick things off by
// loading list of Users from the db
initialize: function() {
_.bindAll(this, 'addAll', 'addOne','render');
Users.bind('add', this.addOne);
Users.bind('reset', this.addAll);
Users.bind('all', this.render);
console.log("fetching Users");
Users.fetch();
},
// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function() {
console.log("rendering User AppView");
// might want to put some total stats for the Users somewhere on the page
},
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne: function(User) {
console.log("adding one User: " + User.get("id") + "/" + User.get("email"));
var view = new UserView({model: User});
this.$("#user-list").append(view.render().el);
},
// Add all items in the **Users** collection at once.
addAll: function() {
console.log("adding all Users");
console.log(Users.length + " Users");
Users.each(this.addOne);
}
});
// Finally, we kick things off by creating the **App**.
console.log("starting userapp now");
var UsersApp = new UsersView();
});
Additional examples of a select list view with option sub-views can be found in:
Zipkin source
reviewboard source

backbone.js - collections and views

I understand how to get a collection together, or an individual model. And I can usually get a model's data to display. But I'm not clear at all how to take a collection and get the list of models within that collection to display.
Am I supposed to iterate over the collection and render each model individually?
Is that supposed to be part of the collection's render function?
Or does the collection just have it's own view and somehow I populate the entire collection data into a view?
Just speaking generally, what is the normal method to display a list of models?
From my experience, it's the best to keep in your collection view references to each model's view.
This snippet from the project I'm currently working on should help you get the idea better:
var MyElementsViewClass = Backbone.View.extend({
tagName: 'table',
events: {
// only whole collection events (like table sorting)
// each child view has it's own events
},
initialize: function() {
this._MyElementViews = {}; // view chache for further reuse
_(this).bindAll('add');
this.collection.bind('add', this.add);
},
render: function() {
// some collection rendering related stuff
// like appending <table> or <ul> elements
return this;
},
add: function(m) {
var MyElementView = new MyElementViewClass({
model: m
});
// cache the view
this._MyElementViews[m.get('id')] = MyElementView;
// single model rendering
// like appending <tr> or <li> elements
MyElementView.render();
}
});
Taking this approach allows you to update views more efficiently (re-rendering one row in the table instead of the whole table).
I think there are two ways to do it.
Use a view per item, and manipulate the DOM yourself. This is what the Todos example does. It's a nice way to do things because the event handling for a single model item is clearer. You also can do one template per item. On the downside, you don't have a single template for the collection view as a whole.
Use a view for the whole collection. The main advantage here is that you can do more manipulation in your templates. The downside is that you don't have a template per item, so if you've got a heterogeneous collection, you need to switch in the collection view template code -- bletcherous.
For the second strategy, I've done code that works something like this:
var Goose = Backbone.Model.extend({ });
var Gaggle = Backbone.Collection.extend({
model: Goose;
};
var GaggleView = Backbone.View.extend({
el: $('#gaggle'),
template: _.template($('#gaggle-template').html()),
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
}
};
var g = new Gaggle({id: 69);
g.fetch({success: function(g, response) {
gv = new GaggleView({model: g});
gv.render();
}});
The template code would look something like this:
<script type="text/template" id="gaggle-template">
<ul id="gaggle-list">
<% _.each(gaggle, function(goose) { %>
<li><%- goose.name %></li>
<% }); %>
</ul>
</script>
Note that I use the _ functions (useful!) in the template. I also use the "obj" element, which is captured in the template function. It's probably cheating a bit; passing in {gaggle: [...]} might be nicer, and less dependent on the implementation.
I think when it comes down to it the answer is "There are two ways to do it, and neither one is that great."
The idea of backbone is that view rendering is event driven.
Views attach to Model data change events so that when any data in the model changes the view updates itself for you.
What you're meant to do with collections is manipulate a collection of models at the same time.
I would recommend reading the annotated example.
I've also found this a confusing part of the Backbone framework.
The example Todos code is an example here. It uses 4 classes:
Todo (extends Backbone.Model). This represents a single item to be todone.
TodoList (extends Backbone.Collection). Its "model" property is the Todo class.
TodoView (extends Backbone.View). Its tagName is "li". It uses a template to render a single Todo.
AppView (extends Backbone.View). Its element is the "#todoapp" . Instead of having a "model" property, it uses a global TodoList named "Todos" (it's not clear why...). It binds to the important change events on Todos, and when there's a change, it either adds a single TodoView, or loops through all the Todo instances, adding one TodoView at a time. It doesn't have a single template for rendering; it lets each TodoView render itself, and it has a separate template for rendering the stats area.
It's not really the world's best example for first review. In particular, it doesn't use the Router class to route URLs, nor does it map the model classes to REST resources.
But it seems like the "best practice" might be to keep a view for each member of the collection, and manipulate the DOM elements created by those views directly.

Categories