Generally when design view for Collection(s), I would bind the collection to the view, and register related events to the collection like this:
var Book = Backbone.Model.extend({});
var BookList = Backbone.Collection.extend({
model: Book,
url: "/books"
});
var BookListItemView = Backbone.View.extend({
mtemplate: _.template($('#tpl_book_item').html()),
render: function () {
this.$el = $(this.mtemplate(this.model.toJSON()));
return this;
}
});
var BookListView = Backbone.View.extend({
el: '#content',
initialize: function () {
this.listenTo(this.collection, 'add', this.render);
this.listenTo(this.collection, 'remove', this.render);
},
render: function () {
this.$el.empty();
this.collection.each(function (item) {
this.$el.append(new BookListItemView({model: item}).render().$el);
}, this);
return this;
}
});
Use:
var books = new BookList();
var bookListView = new BookListView({
collection: books
});
books.fetch();
It worked as expected: render every book as defined in the template. However I found that there is a slight stuck in the page.
I am not sure if this is caused by the re-rendering the view? As shown, when the books.fetch complete, it will add books to the collection of books, for each book item, an add event will be triggered, then I will re-render the page by removing the exist item and iterate the collection.
Which means once there are 10 books, there will be 1+2+3+4...+10 loops for the BookListView.
I my opinion, once the add event triggered, I should not refresh the whole list but just add a new view to the BookListView, but how about the remove event, it seems that Backbone does not provide any internal method to get the view from the model, so once a model to be removed, I can not get the related view.
How do you handle this kind of suitation?
Do not bind your add to the render function. Instead create a dedicated add method for that.
var Book, BookList, BookListItemView, BookListView;
Book = Backbone.Model.extend({});
BookList = Backbone.Collection.extend({
model: Book,
url: "/books"
});
BookListItemView = Backbone.View.extend({
mtemplate: _.template($("#tpl_book_item").html()),
initialize: function() {
this.model.on("remove", this.remove);
},
render: function() {
this.$el = $(this.mtemplate(this.model.toJSON()));
return this;
}
});
BookListView = Backbone.View.extend({
el: "#content",
initialize: function() {
this.listenTo(this.collection, "add", this.addItem);
},
render: function() {
this.$el.empty();
this.collection.each((function(item) {
this.addItem(item);
}), this);
return this;
},
addItem: function(item) {
this.$el.append(new BookListItemView({
model: item
}).render().$el);
}
});
Let the models own View handle its own remove event.
Related
I have backbone.js collection and collectionview. collection view listening to its collection add event. But when I add new models to it's collection it renders mutiple times for each model.
Please Check the JSFiddle
var ImageCollectioView = Backbone.View.extend({
initialize: function() {
this.collection.bind('add', this.render, this);
},
collection: imgColection,
el: '#cont',
render: function() {
var els = [], self = this;
this.collection.each(function(image){
var imageView = new ImageView({model: image});
self.$el.append(imageView.render().el);
});
return this;
}
});
Your render method renders the entire collection. So after adding a model you should clear the existing item views:
render: function() {
var els = [], self = this;
this.$el.empty();
//------^---- clear existing
this.collection.each(function(image){
var imageView = new ImageView({model: image});
self.$el.append(imageView.render().el);
});
return this;
}
That being said, it's better to add a separate method that just appends single item view rather than rendering the entire collection:
var ImageCollectioView = Backbone.View.extend({
initialize: function() {
this.render();
this.listenTo(this.collection, 'add', this.renderItem);
},
el: '#cont',
render: function() {
this.collection.each(this.renderItem, this);
return this;
},
renderItem: function(image) {
var imageView = new ImageView({
model: image
});
this.$el.append(imageView.el);
}
});
Updated Fiddle
I have 2 views, one is a list of "timetracks" and the other is a form to create a timetrack/s
The first one has a collection attached.
The second one, the timetraks form, it defines a "create" function that makes reference to the first one to rerender timetraks view once a new timetrack is created.
timetracks code:
define(['backbone','collections/timetracks', 'views/timetracks/item'], function(Backbone, instanceTimeTracksCollection, TimeTrackView){
var TimeTrackGrid = Backbone.View.extend({
//......
});
return TimeTrackGrid;
});
The form code:
define(['backbone', 'collections/timetracks'], function(Backbone, instanceTimeTracksCollection){
//...............
//next comes my issue:
create: function(){
instanceTimeTracksCollection.create(indexed_array,{
success: function(model, response) {
console.info('model created, response = ',response);
// timeTracksGrid is out of scope, timeTracksGrid would be an instance of timetracks.
timeTracksGrid.render();
},
error: function(error){
console.info('error=',error);
},
wait:true
});
}
});
... and finally I have app.js where the instances of both views are defined:
requirejs(['backbone','views/timetracks/new','views/timetracks/list'],
function(Backbone, newTimeTrackForm, timeTracksGrid) {
var grid = new timeTracksGrid();
var formView = new newTimeTrackForm();
});
How could I render the timetracks view once a new timetrack is created?
**************************** UPDATE *************************************
This is my new version of the code. The issue now is that "this.listenTo(this.collection, "add", this.render);" is overlapping with "this.collection.fetch". As a result the timetracks records are rendered multiple times.
// timetracks view
define(['backbone','collections/timetracks', 'views/timetracks/item'], function(Backbone, timeTracksCollection, TimeTrackView){
var TimeTrackGrid = Backbone.View.extend({
//....
initialize: function(){
_.bindAll(this, 'render', 'generateTimeTracks', 'appendTimeTrack');
this.listenTo(this.collection, "add", this.render);
this.render();
}
render: function(){
$(this.el).html("<table border='1'></table>");
this.collection.fetch({
success: this.generateTimeTracks
});
},
generateTimeTracks : function(){
var self = this;
_(this.collection.models).each(function(item){
self.appendTimeTrack(item);
}, this);
},
appendTimeTrack: function(item){
var timeTrackView = new TimeTrackView({
model: item
});
$('table', this.el).append(timeTrackView.render().el);
}
}
Some other changes:
on app.js instead doing {model:myCollection} as you suggested I'm doing {collection: myCollection}
my form code creates a new model by calling this.collection.create
Thanks again !
A different solution would be to create the views and your collection seperately.
Then in your app.js you could pass the collection to both views. In the initialize function of the TimeTrackGrid you should listen to the "add" event of models on the collections. When such an event is fired you should render the view.
In the create method of your form view you could add the data to your collection. This way your views don't have to know anything about each other which better conforms the Model and View separation.
Thus:
//the grid view
define(['backbone', 'collections/timetracks', 'views/timetracks/item'], function (Backbone, instanceTimeTracksCollection, TimeTrackView) {
var TimeTrackGrid = Backbone.View.extend({
initialize: function () {
//start listening to models being added
this.listenTo(instanceTimeTracksCollection, "add", this.render)
},
render: function () {
//render your view
return this;
}
});
return TimeTrackGrid;
});
//and the form view
define(['backbone', 'collections/timetracks'], function (Backbone, instanceTimeTracksCollection) {
//...............
//next comes my issue:
create: function () {
var data = //get the data from the form
instanceTimeTracksCollection.add(data) //if you have defined a model on your collection, backbone will automatically instantiate the model
}
});
//and you app -> notice the reference to the collection definition
requirejs(['backbone','views/timetracks/new','views/timetracks/list', 'collections/timetrackcollection'],
function(Backbone, newTimeTrackForm, timeTracksGrid) {
var instanceTimeTracksCollection = new TimeTracksCollection();
var grid = new timeTracksGrid({model : instanceTimeTracksCollection});
var formView = new newTimeTrackForm(model : instanceTimeTracksCollection);
});
EDIT=========================================================
fetch the config here
requirejs(['backbone','views/timetracks/new','views/timetracks/list'],
function(Backbone, newTimeTrackForm, timeTracksGrid) {
var grid = new timeTracksGrid();
var formView = new newTimeTrackForm();
var collection = new Collection();
collection.fetch()
});
change your view to:
define(['backbone','collections/timetracks', 'views/timetracks/item'], function(Backbone, timeTracksCollection, TimeTrackView){
var TimeTrackGrid = Backbone.View.extend({
//....
initialize: function(){
_.bindAll(this, 'render', 'generateTimeTracks', 'appendTimeTrack');
// maybe backbone does not fire the add event after fetch
// I believe it does, but I might be mistaken. You will have to look that up
this.listenTo(this.collection, "add", this.render);
this.render();
}
//model is passed to the render method by backbone
render: function(model){
$(this.el).html("<table border='1'></table>");
$('table', this.el).append(new TimeTrackView({model : model}).render().el);
},
//unused now
generateTimeTracks : function(){
var self = this;
// backbone has underscore build in
// so use this instead
this.collection.each(function(item){
//do something with item
}
_(this.collection.models).each(function(item){
self.appendTimeTrack(item);
}, this);
},
//unused now
appendTimeTrack: function(item){
var timeTrackView = new TimeTrackView({
model: item
});
$('table', this.el).append(timeTrackView.render().el);
}
}
My collection is not rendering for some reason. Cannot find out why.
TreeItem = Backbone.Model.extend({
});
TreeList = Backbone.Collection.extend({
model: TreeItem,
url: "/get_tree_list"
});
window.tree_list = new TreeList();
// VIEW
window.TreeItemView = Backbone.View.extend({
tagName: 'li',
initialize: function(){
_.bindAll(this, 'render');
},
render: function(){
$(this.el).html('<span>'+this.model.get('title')+'</span>');
return this;
}
});
window.TreeListView = Backbone.View.extend({
el: "#tree-structure",
events: {
},
initialize: function() {
_.bindAll(this, 'appendItem', 'render');
tree_list.bind('add', this.appendItem);
tree_list.fetch();
this.render();
},
render: function() {
tree_list.each(this.appendItem);
return this;
},
appendItem: function(item){
var tree_item_view = new TreeItemView({
model: item
});
$(this.el).append(tree_item_view.render().el);
}
});
var tree_list_view = new TreeListView;
Backbone.js provides a lot to be interpreted that's where people new go wrong. Your mistake is fundamental in nature. You tie the View directly to the model
see initialize function where a instance of collection is rendered!!
Always and anywhere you create model, collection pass then as parameters to the constructor of views. Check my fiddle
Never call render inside model, view or collection. They must be inside application file
JsFiddle
http://jsfiddle.net/35QGM/
I have a Backbone collection and when I add a new model to it the "add" event doesn't seem to work as I'd expect. I've bound 2 views to listen for add events on the collection, but only one seems to get notified of the event, and when this happens, no PUT request is sent to my server. When I remove the second bind, the other one works and the PUT request is sent. Here's the code snippets:
var FlagList = Backbone.Collection.extend({
model: Flag // model not shown here... let me know if it would help to see
});
var FlagCollectionView = Backbone.View.extend({
el: $('ul.#flags'),
initialize: function() {
flags.bind('add', this.addFlag, this); // this one doesn't fire!!
},
addFlag: function(flag) {
alert("got it 1"); // I never see this popup
}
});
var AddFlagView = Backbone.View.extend({
el: $("#addFlagPopup"),
events: {
"click #addFlag": "addFlag"
},
initialize: function() {
flags.bind('add', this.closePopup, this); // this one fires!!
}
addFlag: function() {
flags.create(new Flag);
},
closePopup: function() {
alert("got it 2"); // I see this popup
}
});
var flags = new FlagList;
var addFlagView = new AddFlagView;
var flagCollectionView = new FlagCollectionView;
A few suggestions:
ID's vs Classes
you've over qualified your selector by combining a class and an id. jQuery allows this, but the ID selector should be unique on the page anyway so change el: $('ul.#flags') to el: $('ul#flags').
Leveraging Backbone
I like to explicitly pass my collections and/or models to my views and use the magic collection and model attributes on views.
var flags = new FlagList;
var addFlagView = new AddFlagView({collection: flags});
var flagCollectionView = new FlagCollectionView({collection: flags});
which now means that in your view, you will automagically have access to this.collection
unbinding events to avoid ghost views
var FlagCollectionView = Backbone.View.extend(
{
initialize: function (options)
{
this.collection.bind('add', this.addFlag, this);
},
addFlag: function (flag)
{
alert("got it 1");
},
destroyMethod: function()
{
// you need some logic to call this function, this is not a default Backbone implementation
this.collection.unbind('add', this.addFlag);
}
});
var AddFlagView = Backbone.View.extend(
{
initialize: function ()
{
this.collection.bind('add', this.closePopup, this);
},
closePopup: function ()
{
alert("got it 2");
},
destroyMethod: function()
{
// you need some logic to call this function, this is not a default Backbone implementation
this.collection.unbind('add', this.closePopup);
}
});
It looks like I have to agree with #fguillen, that your problem must be somewhere in how you initialize the view, as in my comment I mention that it's most likely related to timing, ie: binding your event to the collection after the 'add' event has already fired.
This code works for me:
var FlagList = Backbone.Collection.extend({});
var FlagCollectionView = Backbone.View.extend({
initialize: function() {
flags.bind('add', this.addFlag, this);
},
addFlag: function(flag) {
alert("got it 1");
}
});
var AddFlagView = Backbone.View.extend({
initialize: function() {
flags.bind('add', this.closePopup, this);
},
closePopup: function() {
alert("got it 2");
}
});
var flags = new FlagList;
var addFlagView = new AddFlagView;
var flagCollectionView = new FlagCollectionView;
flags.add({key:"value"});
check the jsFiddle
Your problem is somewhere else.
If you ended up here after making the same stupid mistake I did, make sure you've got:
this.collection.bind( 'add', this.render )
and NOT:
this.collection.bind( 'add', this.render() )
I have implemented a simple close() method for all the Backbone views which disposes of a view when its not needed/needs to be reset.
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
this.remove();
this.unbind();
};
NewView = Backbone.View.extend({
el: '#List ul',
initialize: function() {},
render: function() {
_(this.collection.models).each(function(item) {
this.renderChildren(item);
}, this);
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
},
onClose: function() {
this.collection.reset();
// I want to remove the child views as well
}
});
NewChildView = Backbone.View.extend({
tagName: 'li',
render: function() {
}
});
Now, when I remove the parent view, I also want to remove all the child views here. Any ideas how can I can do this without looping through the models like this....
_(this.collection.models).each(function(item) {
item.close();
}, this);
I think in most of the cases you should keep the view removal in the view layer, without affecting your models.
For example, if you remove a view with comments, maybe another view in your app shows a selection of comments, or some statistics, and resetting the collection would affect those views too.
So I think you should keep it all in the view (only relevant methods included):
NewView = Backbone.View.extend({
initialize: function() {
this.childViews = [];
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
this.childViews.push(itemView);
},
onClose: function() {
_(this.childViews).each(function(view) {
view.close();
});
}
});