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
Related
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/
I'll start off with I'm new to these two frameworks but I'm really excited with what I've learned so far with them. Big improvement with the way I've been doing things.
I want to display a collection of items (trip itineraries) in say a table. I only want to display a couple of the itinerary fields in the table because there are many fields. When you edit/add an item, I would like to display a form to edit all the fields in a different region or in a modal and obviously when you save the list/table is updated. I'm unsure of the best way to structure this and am hoping someone could point me in the right direction or even better an example of something similar. My searches so far have came up short. It seems I should use a composite view for the list but how to best pass the selected item off to be edited is where I'm kinda stuck at. Any pointers would be much appreciated.
I'd use a CompositeView for the table, and an ItemView for the form. Clearly this is incomplete, but it should get you started. Actually... I kind of got carried away.
What follows are some ideas for the basic structure & flow. The implementation details, including templates, I'll leave to you.
The table/row views:
// each row in the table
var RowView = Backbone.Marionette.ItemView.extend({
tagName: "tr"
});
// This could be a table tag itself, or a container which holds the table.
// You want to use a CompositeView rather than a CollectionView so you can
// render the containing template (the table tag, headers), and also, it works
// if you have an actual collection model (ItineraryList, etc).
var TableView = Backbone.Marionette.CompositeView.extend({
itemView: RowView,
collectionEvents: {
"change": "render"
}
});
The form view:
// This would be a simple form wrapper that pulls default values from
// its model. There are some plugins in this space to help with
// forms in backbone (e.g. backbone-forms), but they may or may not
// be worth the bloat, or might be tricky to work with Marionette.
var FormView = Backbone.Marionette.ItemView.extend({
events: {
"submit form": "onFormSubmit"
},
data: function () {
// here you'd need to extract your form data, using `serializeArray`
// or some such, or look into a form plugin.
return {};
},
// Clearly this is incomplete. You'd need to handle errors,
// perhaps validation, etc. You probably also want to bind to
// collection:change or something to close the formview on success.
//
// Re-rendering the table should be handled by the collection change
// event handler on the table view
onFormSubmit: function (e) {
e.preventDefault();
if (this.model.isNew()) {
this.collection.create(this.data());
} else {
this.model.save(this.data());
}
}
});
Somewhere in your load process you'd instantiate a collection and show it:
// first off, somewhere on page load you'd instantiate and probably fetch
// a collection, and kick off the tableview
var itineraries = new Itineraries();
itineraries.fetch();
// For simplicity I'm assuming your app has two regions, form and table,
// you'll probably want to change this.
app.table.show(new TableView({collection: itineraries}));
Making routes for the edit and new itinerary links makes sense, but if your form is in a modal you might want to bind to button clicks instead. Either way, you'd open the form something like this:
// in the router (/itineraries/edit/:id) or a click handler...
function editItinerary(id) {
var itinerary = itineraries.get(id);
// then show a view, however you do that. If you're using a typical
// marionette region pattern it might look like
app.form.show(new FormView({
collection: itineraries,
model: itinerary
});
}
// once again, a route (/itineraries/new), or a click handler...
function newItinerary() {
app.form.show(new FormView({
collection: itineraries,
model: new Itinerary()
}));
}
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/
I have a collection of models. When a model changes it triggers a change event on the collection. I watch for the collection change event and then I update the UI.
How should I go about updating the UI? Don't I need to know what models are new, so I can append, and what already exist, so I can update?
One of the reason I feel I need this granularity is because there's an animation transition, so I need to relate every model to it's previous state. Does backbone help with this or should I just build this on my own?
to know which models are new, listen to the collection's "add" event. then you can render the individual item.
MyView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "renderItem");
this.collection.bind("add", this.renderItem);
},
renderItem: function(item){
// render the new item here
},
render: function(){
this.collection.each(this.renderItem);
}
});
in this example, rendering the collection works the same as rendering an individual item - it calls the same renderItem method that the "add" event calls.
to handle the scenario where you have a sorted list... you'll have to do some logic in your renderItem method to figure out the location of the item. something like this maybe (untested code... just an idea):
MyView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "renderItem");
this.collection.bind("add", this.renderItem);
},
renderItem: function(item){
var itemView = new ItemView({model: item});
itemView.render();
return itemView;
},
render: function(){
this.collection.each(function(item){
var itemView = renderItem(item);
var itemIndex = item.get("index");
var previousItem = this.$(".myList")[itemIndex];
$(itemView.el).insertAfter($(previousItem));
});
}
});
this code assumes you have an index attribute on your models to know the order that the models are supposed to be in.
also note that there's a high likelihood that this code won't execute as-is, since i haven't tested it out. but it should give you an idea of the code you'll want to write.
You don't need to go through the collection if the model changes. Simple render a sub view for each model and in which you can handle the change event.
Imagine you have the following structure:
Collection: Tasks
Model: Task
And two views:
Tasks view (main view)
Task view (sub view)
In the #render method of the Tasks view you render e.g. a <ul> element which will hold your tasks (sub views). Then you iterate through all your collection's models and create a Task view for each, passing the model and appending it's el to your main views <ul>.
Within your Task view you bind the #render method to the models change event and each Task view will take care of itself. — Am I making sense?
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.