I am learning JavaScript MVC application development using Backbone.js, and having issues rendering model collection in the view. Here's what I want to do:
After the page finishes loading, retrieves data from the server as model collection
Render them in the view
That's all I want to do and here is what I have so far:
$(function(){
"use strict";
var PostModel = Backbone.Model.extend({});
var PostCollection = Backbone.Collection.extend({
model: PostModel,
url: 'post_action.php'
});
var PostView = Backbone.View.extend({
el: "#posts-editor",
initialize: function(){
this.template = _.template($("#ptpl").html());
this.collection.fetch({data:{fetch:true, type:"post", page:1}});
this.collection.bind('reset', this.render, this);
},
render: function(){
var renderedContent = this.collection.toJSON();
console.log(renderedContent);
$(this.el).html(renderedContent);
return this;
}
});
var postList = new PostCollection();
postList.reset();
var postView = new PostView({
collection: postList
});
});
Problem
As far as I know, Chrome is logging the response from the server and it's in JSON format like I want it. But it does not render in my view. There are no apparent errors in the console.
The server has a handler that accepts GET parameters and echos some JSON:
http://localhost/blog/post_action.php?fetch=true&type=post&page=1
[
{
"username":"admin",
"id":"2",
"title":"Second",
"commentable":"0",
"body":"This is the second post."
},
{
"username":"admin",
"id":"1",
"title":"Welcome!",
"commentable":"1",
"body":"Hello there! welcome to my blog."
}
]
There are 2 potential problems with your code.
The event listener callback should be registered before calling the collection.fetch(). Otherwise, you might miss the first reset event as it might be triggered before the listener is registered.
The reset event is not enough to ensure that the view will re-render every time the collection gets updated.
Also, note that it is a good practice to use the object.listenTo() form to bind events as it will ensure proper unregistration when the view is closed. Otherwise, you may end up with what is known as Backbone zombies. Here is a solution.
this.listenTo( this.collection, 'reset add change remove', this.render, this );
this.collection.fetch({ data: { fetch:true, type:"post", page:1 } });
Note how you can register multiple events from the same object by separating them with whitespace.
change
this.collection.bind('reset', this.render, this);
to
this.collection.bind('sync', this.render, this);
The problem is you perform reset only once, in the beginning. And at that time you don't have anything to render. The next time, when you fetch your collection, reset event doesn't fire, because you fetch collection without option {reset: true}.
Change this line
this.collection.bind('reset', this.render, this);
to
this.listenTo(this.collection, 'reset', this.render);
When fetching your collection, the reset event is not fired by default anymore. (I believe since version 1.0)
In order to have Backbone fire the reset event when the collection has been fetched, you now have to call the fetch method like so:
this.collection.fetch({reset: true});
Related
Here is the general structure I have right now:
Collection 'A' which has view 'A'
to add a model, you can open a modal and choose from a list. The modal is it's own view, 'B', tied to its own collection, 'B'. the view 'B' is instantiated from view 'A' on a click event.
I was going to initialize view 'B' with a 'myParent' attribute so when a model is selected from collection b, I can say this from within view B:
this.myParent.collection.add(newModel).
I know this will work, but is this coupling things too tight that don't need to be? is there a good pattern for this? I was thinking about having several different events and triggers spread about different places but that seems to just complicate things without much added value.
I have come across a similar problem in an application which lists a collection of selectable models in a table. Based on the models which are selected, another view needs to determine which buttons to show to the user.
In this case, I went for an event driven approach. Every time a model from view A is selected, I fire an event which View B listens to and adds that model to Collection B.
For your situation, that could look something like this:
// Your collection view.
var ViewA = Backbone.View.extend({
initialize: function(){
Backbone.Events.on('modelSelected', this.onModelSelected, this);
this.listenTo(this.collection, 'add', this.renderList);
},
onModelSelected: function(model){
this.collection.add(model);
}
renderList: function(){
// Draw your collection
}
// Do some clean up of events
remove: function(){
Backbone.Events.off('modelSelected', this.onModelSelected, this);
Backbone.View.prototype.remove.call(this);
}
});
// Modal View
var ViewB = Backbone.View.extend({
initialize: function(){
this.collection = new YourCollection();
this.listenTo(this.collection, 'sync', this.renderCollection);
this.collection.fetch();
},
render: function(){
// Render your modal
return this;
},
renderCollection: function(){
// Render your collection
},
//
modalSubmit: function(){
var model = this.getSelectedModel();
Backbone.Events.trigger('modelSelected', modal);
this.close();
}
});
I should note that this comes with a few caveats. Firstly, I would recommend picking a robust and namespace event naming scheme. This is just an example. Secondly, fetching from the initialize function is often not ideal, but I'm leaving out the concept of controllers for brevity.
I have this code in my backbone application,
createNewProject: function(e) {
e.preventDefault();
this.createNewProject = new app.CreateNewProject({
collection: this.dropdownListProjectCollection
});
}
app.CreateNewProject = Backbone.View.extend({
el: '.modal',
template: _.template( $("#tpl-create-new-project").html() ),
events: {
'click input[type="submit"]' : 'saveBasicProject'
},
initialize: function() {
this.collection.on("add", console.log("added"));
this.$el.find("h4").text("Create new project");
this.$el.find(".modal-body").html( this.template() );
this.render();
return this;
},
I am trying to detect when a model is added to the collection, and then eventually fire a method to render that new record. However I am having problem in that the this.collection.on('add'...) seems to run when ever the view is initialised and again when a model is saved, how can I only run the method when the model is saved, and not the view being initialised.
I recommend you do this.listenTo(this.collection, ... in your view because when the view is removed it will remove the event and avoid memory leaks.
The add event will get fired on every .set on the collection. If you passed models on collection initialize this will happen ctor -> reset -> add -> set to add those models so you will get reset and add events.
What doesn't make any sense is that your collection should have initialize BEFORE you initialized the view so I don't know why you are getting that event unless you are subsequently adding more models.
To listen to a save event you can use these events on the collection:
request - when the request starts
sync - when the request has happened successfully
error - something went wrong
PS this is a shortcut for this.$el.find -> this.$('.someselector')
I am wondering if there are any pointers on the best way of "fetching" and then binding a collection of data to a view within Backbone.js.
I'm populating my collection with the async fetch operation and on success binding the results to a template to display on the page. As the async fetch operation executes off the main thread, I one loses reference to the backbone view object (SectionsView in this case). As this is the case I cannot reference the $el to append results. I am forced to create another DOM reference on the page to inject results. This works but I'm not happy with the fact that
I've lost reference to my view when async fetch is executed, is there a cleaner way of implementing this ? I feel that I'm missing something...Any pointers would be appreciated.
SectionItem = Backbone.Model.extend({ Title: '' });
SectionList = Backbone.Collection.extend({
model: SectionItem,
url: 'http://xxx/api/Sections',
parse: function (response) {
_(response).each(function (dataItem) {
var section = new SectionItem();
section.set('Title', dataItem);
this.push(section);
}, this);
return this.models;
}
});
//Views---
var SectionsView = Backbone.View.extend(
{
tagName : 'ul',
initialize: function () {
_.bindAll(this, 'fetchSuccess');
},
template: _.template($('#sections-template').html()),
render: function () {
var sections = new SectionList();
sections.fetch({ success: function () { this.SectionsView.prototype.fetchSuccess(sections); } }); //<----NOT SURE IF THIS IS THE BEST WAY OF CALLING fetchSuccess?
return this;
},
fetchSuccess: function (sections) {
console.log('sections ' + JSON.stringify(sections));
var data = this.template({
sections: sections.toJSON()
});
console.log(this.$el); //<-- this returns undefined ???
$('#section-links').append(data); //<--reference independent DOM div element to append results
}
}
);
darthal, why did you re-implement parse? It seems to do exactly what Backbone does by default (receive an array of models from the AJAX call and create the models + add them to the collection).
Now on to your question... you are supposed to use the reset event of the Collection to do the rendering. You also have an add and remove when single instances are added or deleted, but a fetch will reset the collection (remove all then add all) and will only trigger one event, reset, not many delete/add.
So in your initialize:
this.collection.on("reset", this.fetchSuccess, this);
If you are wondering where the this.collection is coming from, it's a param you need to give to your view when you create it, you can pass either a model or a collection or both and they will automatically be added to the object (the view)'s context. The value of this param should be an instance of SectionList.
You'll also have to update fetchSuccess to rely on this.collection instead of some parameters. Backbone collections provide the .each method if you need to iterate over all the models to do stuff like appending HTML to the DOM.
In most cases you don't need a fetchSuccess, you should just use your render: when the collection is ready (on 'reset'), render the DOM based on the collection.
So to summarize the most general pattern:
The collection should be independent from the view: you give the collection as a param to the view creation, the collection shouldn't be created from a specific view.
You bind the View to the collection's reset event (+add, remove if you need) to run a render()
this.collection.on("reset", this.render, this);
You do a fetch on the collection, anytime (probably when you init your app).
A typical code to start the app would look something like this:
var sections = new SectionList();
var sectionsView = new SectionsView({collection: sections});
sections.fetch();
Because you bound the reset event in the view's initialize, you don't need to worry about anything, the view's render() will run after the fetch.
In my backbone model, I call save when there is a change event.
myModel = Backbone.View.extend({
initialize: function() {
var self = this;
self.model.on("change", function() { self.model.save(); });
}
});
From the Backbone docs, I understand that Backbone expects to get a json object back from the server.
So I send the model back to the client. And backbone then updates the model, which triggers the change event again, which causes it to resave again.
What is the recommended way to prevent this behaviour?
In general in Backbone when you don't want side effects from your action you just pass a silent: true option. For instance:
self.model.on("change", function() { self.model.save({silent: true}); });
I haven't tested to ensure this solves your case, but I suspect it will.
A cleaner way to write it would be:
//inside you model
initialize: function () {
this.on('change',function(){ this.save(null,{silent: true}); });
}
As in the docs backbonejs.org/#Model-save.
The 1st arg is the attributes and the 2nd is the options.
I'm starting out with backbone and I'm trying to create a simple view that alerts whenever my model changes. Right now the initialize function in the view is being called, but the render function is not being called when my model changes (my model is being changed).
I've attempted two ways of binding to the change event (in the initialize function and the events property). I feel like I'm missing something obvious.
The #jsonPreview id exists in the html.
// Create the view
var JSONView = Backbone.View.extend({
initialize: function(){
this.bind("change", this.render);
},
render: function() {
alert("change");
},
events:
{
"change":"render"
}
});
// Create the view, and attach it to the model:
var json_view = new JSONView({ el: $("#jsonPreview"), model: documentModel });
Thanks in advance.
It looks like you are binding to the change event on the view rather than the view's model. think you need to bind to the model event something like this:
initialize: function(){
this.model.bind("change", this.render);
},