Backbone: Fetch before rendering - javascript

I'm trying to add a new item to my backbone collection using:
window.bearList.create({ name: "Tina" } );
This correctly saves the new item to the server, because afterwards I can see this on the server, which is what I want. (I'm using MongoDB)
{"name":"Tina","_id":"53b41d92b7083d0b00000009","__v":0}
I have this binding in my bearList ListView:
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('add', this.render);
},
The problem is that the code above just adds the following to my collection view until I reload the page.
{"name":"Tina"}
I've tried using the model.save() callback, but I still have the same issue.
Like I said, everything looks fine on the server, and the collection has the correction version of 'Tina' once I reload the page.
But for some reason, it is not getting the full model for the ListView's 'render' event. I've tried fetching each model individually on the ListView render method, but this did not work and is bad practice anyway.
Can someone help me out?
Here is my full code for this:
window.ListView = Backbone.View.extend({
tagName: 'ul',
className: 'list-group',
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('add', this.render);
},
render: function(){
this.$el.html(" ");
this.collection.each(function(item){
var listItemView = new ListItemView({ model: item });
this.$el.append(listItemView.render().el);
}, this);
return this;
},
});
window.ListItemView = Backbone.View.extend({
tagName: 'li',
className: 'list-group-item',
initialize:function () {
this.model.bind("change", this.render);
},
render:function () {
console.log(JSON.stringify(this.model.toJSON()));
this.$el.html("<a href='#"+ this.model.hashType + "/"+this.model.get('_id')+"' >" + this.model.get('name') + "</a>");
return this;
}
});

Just pass wait:true.
window.bearList.create({ name: "Tina" }, {wait: true} );
http://backbonejs.org/#Collection-create
Pass {wait: true} if you'd like to wait for the server before adding
the new model to the collection.

Listen to the sync event since add will only add the values you are creating from within backbone. http://backbonejs.org/#Sync
And a tip: use listenTo to use more of Backbone's features.
instead of
initialize:function () {
this.model.bind("change", this.render);
},
use:
initialize:function () {
this.listenTo( this.model, "change", this.render );
},

Related

How to improve my Backbone code

I'm starting to develop in Backbone.js.
And I still do not know how to write code using best practices.
My app is working but I have some issues, for example, the comparator does not work.
I would really help to make him better.
View:
Todos.Views.TasksView = Backbone.View.extend({
template: JST['todos/index'],
tagName: 'div',
id: '#todos',
initialize: function () {
this.collection = new Todos.Collections.TasksCollection();
this.collection.fetch({
reset: true
});
this.render();
this.listenTo(this.collection, 'reset', this.render);
this.listenTo(this.collection, 'change', this.render);
this.listenTo(this.collection, 'sort', this.render);
},
render: function () {
$(this.el).html(this.template({
tasks: tasks[0].task
}));
return this;
}
});
Collection:
Todos.Collections.TasksCollection = Backbone.Collection.extend({
model: Todos.Models.TasksModel
url: "/api/tasks.json",
comparator: function (sort) {
return -(new Date(sort.get('eventTime'))).getTime();
},
parse: function (response) {
return response;
}
});
Model:
Todos.Models.TasksModel = Backbone.Model.extend({
parse: function (response, options) {
return response;
}
});
Router:
Todos.Routers.TasksRouter = Backbone.Router.extend({
routes: {
'': 'index'
},
initialize: function () {
var viewTasks = new Todo.Views.TasksView({
model: Todos.Models.TasksModel,
el: '.tasks-list'
});
}
});
App.js:
window.Todos = {
Models: {},
Collections: {},
Views: {},
Routers: {},
initialize: function () {
new Todos.Routers.TasksRouter();
Backbone.history.start({})
},
$(document).ready(function () {
Todos.initialize();
});
Inside your views try to not have model or collection function like fetch or save or set.This is not a view responsibility.
Example : Instead of have this.collection.fetch() inside your collection, do something like this.
this.collection = new Todos.Collections.TasksCollection();
this.collection.getSomething();
this.render();
this.listenTo(this.collection, 'reset', this.render);
this.listenTo(this.collection, 'change', this.render);
this.listenTo(this.collection, 'sort', this.render);
Inside your collection, add a method to perform "fetch()" request.
I don't understand why you're declaring the parse function.. you aren't doing anything..
I like to use parse function when I want change something that is in your JSON before backbone bind it to your model/collection.
I can't see where you're calling the comparator function.. maybe the 'sort' parameter is with an invalid value.
Inside your model you're using parse again.. I can't see a reason.
Your router is ok.
I like your code.. for a beginner as you said. it's really good.
I recommend you use lo-dash instead of underscore... It have more options to work with arrays.
Try to use require.js to load your js dependencies.. It will help you a lot.
www.requirejs.org
Hope it helps.

Collection add event listener in Backbone

I am trying to update my view whenever I add a new model to my collection. My first question is do I automatically add a model to my collection when I save that model, like:
PostsApp.Views.Form = Backbone.View.extend({
template: _.template($('#form-template').html()),
render: function(){
this.$el.html(this.template(this.model.toJSON()));
},
events:{
'click button' : 'save'
},
save: function(e){
console.log("is this working");
e.preventDefault();
var newname = this.$('input[name=name-input]').val();
var newadress = this.$('input[name=adress-input]').val();
this.model.save({name: newname, adress : newadress});
}
});
or do I still have to do collection.add()
Other than that to see the new model in my view I am trying to add an 'add' event listener like this:
PostsApp.Views.Posts = Backbone.View.extend({
initialize: function(){
this.collection.on('add', this.addOne, this);
},
render: function(){
this.collection.forEach(this.addOne, this);
},
addOne: function(post){
var postView = new PostsApp.Views.Post({model:post});
postView.render();
this.$el.append(postView.el);
}
});
This not only doesnt work, but when I add the initialize method, it just duplicates everything in my model when the page is first loaded.
Nope.. When you do a model.save , it will just create a zombie model ( If it not already a part of the collection .i.e If a New model is saved) which is not a part of any collection.
So your add event will not be triggered for the collection.
If you want the add event to be triggered , Use the create method of collection , which then will know on which collection the new model has to be added..
collection.create({model});
Then it would internally add the model to the collection and fire the add event
Also it is a better idea to use listenTo instead of attaching events using on
this.listenTo(this.collection, 'add', this.addOne);
Code
PostsApp.Views.Form = Backbone.View.extend({
template: _.template($('#form-template').html()),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
},
events: {
'click button': 'save'
},
save: function (e) {
console.log("is this working");
e.preventDefault();
var newname = this.$('input[name=name-input]').val();
var newadress = this.$('input[name=adress-input]').val();
this.collection.create({
name: newname,
adress: newadress
});
}
});
PostsApp.Views.Posts = Backbone.View.extend({
initialize: function () {
this.listenTo(this.collection, 'add', this.addOne);
},
render: function () {
this.collection.forEach(this.addOne, this);
},
addOne: function (post) {
var postView = new PostsApp.Views.Post({
model: post,
collection : this.collection
});
postView.render();
this.$el.append(postView.el);
}
});

How do I render a Backbone Collection in a List and Item View?

I am working on a contact bar which renders all contacts of a user in a html list.
What I have:
UserModel - This is a simple Backbone.Model with username and email
UserCollection - This is used as the contact list
ContactsView - This is the ul contact list
ContactView - This is a single contact model rendered as li
I am currently breaking my head about a solution how (and where) I can fetch my UserCollection and how I pass the single models down to a single ContactView item.
Specific hurdles are:
Where should I fetch, store the UserCollection
How do I render the contact list
How do I render the contact items
How do I prevent fetch({ success: callback }) from breaking my code structure
My current code is this:
entrance point:
// create a new instance of the contact list view
var view = new ContactsView();
// insert the rendered element of the contact list view in to the dom
$('div.contacts-body').html(view.render().el);
view.fetch({ success: view.loadContacts });
ContactsView:
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contacts.html', 'collections/users', 'views/conversations/contact'],
function($, _, Backbone, ContactsTemplate, UserCollection, ContactView) {
var ContactsView = Backbone.View.extend({
tagName: "ul",
className: "contacts unstyled",
attributes: "",
// I am feeling uneasy hardcoding the collection into the view
initialize: function() {
this.collection = new UserCollection();
},
// this renders our contact list
// we don't need any template because we just have <ul class="contacts"></ul>
render: function() {
this.$el.html();
return this;
},
// this should render the contact list
// really crappy and unflexible
loadContacts: function() {
this.collection.each(function(contact) {
// create a new contact item, insert the model
var view = new ContactView({ model: contact });
// append it to our list
this.$el.append(view.render().el);
});
}
});
return ContactsView;
});
ContactView
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contact.html'],
function($, _, Backbone, ContactTemplate) {
var ContactView = Backbone.View.extend({
tagName: "li",
className: "contact",
attributes: "",
template:_.template(ContactTemplate),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
return ContactView;
});
Could somebody help me about my four hurdles.
Good example links are welcome. I oriented my code style at the todos list unfortunatly the todos list isn't that advanced...
UPDATED CODE:
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contacts.html', 'collections/users', 'views/conversations/contact'],
function($, _, Backbone, ContactsTemplate, UserCollection, ContactView) {
var ContactsView = Backbone.View.extend({
tagName: "ul",
className: "contacts unstyled",
attributes: "",
events: {
},
initialize: function() {
this.collection = new UserCollection();
this.collection.on('reset', this.render);
this.collection.fetch();
},
render: function() {
// in chromium console
console.log(this.el); // first: html, second: undefined
console.log(this.$el); // first: html in array, second: undefined
this.$el.empty(); // error on the called that this.$el is undefined
this.collection.each(function(contact) {
var view = new ContactView({ model: contact });
this.$el.append(view.el);
}.bind(this));
return this;
}
});
return ContactsView;
Can it be that reset is triggering this.render twice?
First of all: why do you fetch the view? Backbone views do not have a fetch method..
1 The correct place to fetch your UserCollection would be inside the view's initialize method:
initialize: function() { // ContactsView
_.bindAll(this, 'render', 'otherMethodName', ...); // Bind this to all view functions
...
this.collection.on('reset', this.render); // bind the collection reset event to render this view
this.collection.fetch();
...
}
Now you fetch the contacts exactly when you need them. Next step is to render the collection.
2 Binding to the reset event makes your loadContacts method obsolete and we can do that in the render function:
render: function() {
this.$el.empty(); // clear the element to make sure you don't double your contact view
var self = this; // so you can use this inside the each function
this.collection.each(function(contact) { // iterate through the collection
var contactView = new ContactView({model: contact});
self.$el.append(contactView.el);
});
return this;
}
Now you render your contactlist inside the render method, where it should be done.
3 The ContactView actually looks good.
Just make the item to render itself in the initialize method, so you don't have to make useless calls in the ContactsView's render method and clutter up your code. Also bindAll here as well.
initialize: function() { // ContactView
_.bindAll(this, 'render', 'otherMethodName', ...);
...
this.render(); // Render in the end of initialize
}
I have no idea what you are asking in here, but I think the best way is not to use success callbacks. The collections and models trigger events whenever something is done to them, so tapping onto them is much more robust and reliable than success callbacks. Check out the catalog of events to learn more. The Wine Cellar tutorial by Christophe Coenraets is has an excellent example of this kind of listview-listitemview arrangement.
Hope this helps!
UPDATE: Added _.bindAlls to fix the problem with this in a event bound render call. Some info on binding this.
NOTE: all the code is simplified and no tested
When I have all the elements structure defined, as you have, with all the Models, Collections and Views implemented then I implement a Loader which is in charge of trigger the fetching and rendering actions.
First of all I need to expose the classes definition from the outside something like this:
// App.js
var App = {}
// ContactsCollection.js
$(function(){
var App.ContactsCollection = Backbone.Collection.extend({ ... });
});
// ContactsView.js
$(function(){
var App.ContactsView = Backbone.View.extend({ ... });
});
// and so on...
And then I implement what I call the Loader:
// AppLoad.js
$(function(){
// instantiate the collection
var App.contactsCollection = new App.ContactsCollection();
// instantiate the CollectionView and assign the collection to it
var App.contactsView = new App.ContactsView({
el: "div.contacts-body ul",
collection: App.contactsCollection
});
// fetch the collection the contactsView will
// render the content authomatically
App.contactsCollection.fetch();
});
Another changes you have to do is configure the ContactsView in a way that respond to the changes in the App.contactsCollection because as the fetch() is asynchronous you can call render() when the collection is still not loaded, so you have to tell to the CollectionView to render it self when the Collection is ready:
var ContactsView = Backbone.View.extend({
initialize: function( opts ){
this.collection.on( 'reset', this.addAll, this );
this.collection.on( 'add', this.addOne, this );
// ... same with 'remove'
},
addOne: function( model ){
var view = new App.ContactView({ model: contact });
this.$el.append( view.render().el );
},
addAll: function(){
this.collection.each( $.proxy( this.addOne, this ) );
}
});
You have to require your js files in the proper order:
App.js
Your Models, Collections, Views
AppLoad.js
With this system you obtain:
External access to your collection in case you need to access it from another place.
External control of the CollectionView.el with is better for decoupling and testing.
The CollectionView will respond to changes in the Collection authomatically
Note: If you use Router you can move the AppLoad.js logic to there.

Getting the attribute from a View's Model when the view is clicked (backbone.js)

When a user clicks on a div with class .photo_container which is part of the view PhotoListView, there is a function sendSelectedPhotoId that will be triggered. This function has to get the attribute photo_id from the Photo model that belongs to this view whose div .photo_container element has been clicked, and send it to the serverside via fetch().
Problem: So far I managed to get the function sendSelectedPhotoId to be triggered when the div is clicked, but I cant figure out how to get the photo_id attribute of the view's Photo model. How should I achieve this?
On a side note, I'm not sure whether the correct photo_id will be send.
Code
$('#button').click( function() {
// Retrieve photos
this.photoList = new PhotoCollection();
var self = this;
this.photoList.fetch({
success: function() {
self.photoListView = new PhotoListView({ model: self.photoList });
$('#photo_list').html(self.photoListView.render().el);
}
});
});
Model & Collection
// Models
Photo = Backbone.Model.extend({
defaults: {
photo_id: ''
}
});
// Collections
PhotoCollection = Backbone.Collection.extend({
model: Photo,
url: 'splash/process_profiling_img'
});
Views
// Views
PhotoListView = Backbone.View.extend({
tagName: 'div',
events: {
'click .photo_container': 'sendSelectedPhotoId'
},
initialize: function() {
this.model.bind('reset', this.render, this);
this.model.bind('add', function(photo) {
$(this.el).append(new PhotoListItemView({ model: photo }).render().el);
}, this);
},
render: function() {
_.each(this.model.models, function(photo) {
$(this.el).append(new PhotoListItemView({ model: photo }).render().el);
}, this);
return this;
},
sendSelectedPhotoId: function() {
var self = this;
console.log(self.model.get('photo_id'));
self.model.fetch({
data: { chosen_photo: self.model.get('photo_id')},
processData: true,
success: function() {
}});
}
});
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
template: _.template($('#tpl-PhotoListItemView').html()),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.close, this);
},
render: function() {
$(this.el).html(this.template( this.model.toJSON() ));
return this;
},
close: function() {
$(this.el).unbind();
$(this.el).remove();
}
});
SECOND ATTEMPT
I also tried placing the event handler and sendSelectedPhotoId in the PhotoListItemView where I managed to get the Model's attribute properly, but I can't figure out how to trigger the reset event when the PhotoList collection did a fetch().
View
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
events: {
'click .photo_container': 'sendSelectedPhotoId'
},
template: _.template($('#tpl-PhotoListItemView').html()),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.close, this);
},
render: function() {
$(this.el).html(this.template( this.model.toJSON() ));
return this;
},
close: function() {
$(this.el).unbind();
$(this.el).remove();
},
sendSelectedPhotoId: function() {
console.log('clicked!');
var self = this;
console.log(self.model.get('photo_id'));
self.model.fetch({
data: { chosen_photo: self.model.get('photo_id')},
processData: true,
success: function() {
$(this.el).html('');
}});
}
});
Problem: With this, I cant seem to fire the reset event of the model after doing the fetch() in function sendSelectedPhotoId, which means I cant get it to re-render using PhotoListView's render().
In the screenshot below from Chrome's javascript console, I printed out the collection after sendSelectedPhotoId did its fetch(), and it seems like the fetched added the new data to the existing model, instead of creating 2 new models and removing all existing model!
You already have child views for each model, so I would put the click event handler in the child view. In the handler in the child, trigger an event passing this.model, and listen for that event in your parent.
Update based on update:
Try changing
this.model.bind('reset', this.render, this); to
this.model.bind('remove', this.render, this); // model is a collection right?
and then remove the model from the collection after the view is clicked. Also, I don't think using Model.fetch is what you really want to do. Maybe a .save or a custom method on the model?
Update based on author's comment showing sample base from blog
I would not follow that blog's advice. If you are using backbone professionally I can't recommend the Thoughtbot ebook enough.
It's $50 for a work in progress, and it's worth every penny
It has a simple sample application that lays out how to organize a backbone app. This is why I bought the book.
It uses Rails in the examples for the backend, but I have used Rails, Node, and C# MVC and all work no problem.

no existant $(this.el) in backbone view while rendering child view

full code here... http://pastebin.com/EEnm8vi3
line 378 is not inserting the sections into the current view. the section model is correctly being passed into the method. everything else works as expected except for the insertion of the child rendered views.
I am wanting to know why $(this.el) is empty and therefore not allowing an append. trying to use a direct selector like $('#sections') also does not work.
relevent code repeated from pastbin link above: (addOne method)
SCC.Views.SectionView = Backbone.View.extend({
tagName: "div",
className: "section",
initialize: function(){
_.bindAll(this, 'render');
this.template = _.template($('#section-tmpl').html());
},
render: function() {
console.log($(this.el));
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
SCC.Views.SectionsView = Backbone.View.extend({
tagName: "div",
id: "sections",
className: "sections",
initialize: function(){
_.bindAll(this, 'render');
//SCC.Sections.bind('add', this.addOne, this);
SCC.Sections.bind('reset', this.addAll, this);
SCC.Sections.bind('all', this.render, this);
},
render: function() {
$(this.el).html("<p>rendered</p>");
return this;
},
addOne: function(section) {
var view = new SCC.Views.SectionView({model: section});
$(this.el).append(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne);
}
});
SCC.Sections = new SCC.Collections.Sections();
SCC.SectionsView = new SCC.Views.SectionsView({collection:SCC.Sections});
SCC.Sections.reset(window.SectionData);
$('#main').append(SCC.SectionsView.render().el);
I ran into this problem myself and so I'll leave this answer for anyone else out there:
When you bind this.render to 'all' as #lukemh did:
SCC.Sections.bind('all', this.render, this);
You're effectively saying everytime an event is triggered in the model/collection re-render the view. When you use .html() in that render method you're also going to override any child views that may have been .append()'ed to it throught the addOne function.
If you move the $(this.el).html() call to the initialize view the problem is solved. You can still bind render to 'all' but make sure you're only re-rendering a portion of it or else you'll override the child views again.

Categories