Backbone.js modify newly fetched collection before reset events occur - backbone.js

I'm trying to do the following:
Fetch data from the server
Add a zero-based index to the models before the views are notified
Finally run have the 'render' events fire for the views
I was trying to do this by using a success callback in the collection
View Before
initialize: () ->
#collection.on 'reset', #render, this
render: () -> ...render code...
Collection Before
search: () ->
#fetch {
success: #fetch_success
}
fetch_success: () ->
for i in [0...collection.models.length]
collection.models[i].set('go_index', i)
Doing things this way was causing the views to fire their render events before the collection was updated by the success callback. The solution I came up with was to have the views listen to a fetched event, then have the collection fire that after it successfully modifies the collection:
View After
initialize: () ->
#collection.on 'fetched', #render, this
render: () -> ...render code...
Collection After
initialize: () ->
#on 'reset', #add_index_and_notify, this
add_index_and_notify: () ->
for i in [0...#models.length]
#models[i].set('go_index', i)
#trigger('fetched')
This works fine, I'm just wondering if this is the most elegant way to accomplish this or if there is a built-in way that I'm missing.
UPDATE 3/15
I've come up with a cleaner solution that doesn't require the view to do any of the dirty work and I don't have to create a custom event. The trick is to listen to the sync event (which fires after reset)
View Final
initialize: () ->
#collection.on 'sync', #render, this
render: () -> ...render code...
Collection Final
initialize: () ->
#on 'reset', #add_index, this
add_index: () ->
for i in [0...#models.length]
#models[i].set('go_index', i)
Hopefully this pattern can help somebody searching in the future.

I've already posted the solution in the original question, but I figured I'd formally post as an answer:
The cleaner solution doesn't require the view to do any of the dirty work and doesn't require a custom event. The trick is to listen to the sync event (which fires after reset)
View Final
initialize: () ->
#collection.on 'sync', #render, this
render: () -> ...render code...
Collection Final
initialize: () ->
#on 'reset', #add_index, this
add_index: () ->
for i in [0...#models.length]
#models[i].set('go_index', i)
Hopefully this pattern can help somebody searching in the future.

Your view should get both the model and it's index separately from the collection because the index isn't really part of the model record itself. Try having your view use collection.each to loop over the models as the callback function there will get model, index, collection as parameters. Remember a view can pass more than just a single model to its template.
class CollectionView1 extends Backbone.View
render: =>
$el = #$el
$el.empty()
#collection.each (model, index) ->
modelView = new ModelView1 {model, index}
$el.append modelView.render().el
return this

Why don't you listen to the add event of the collection..
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
this.listenTo(this.collection , 'add' , this.add_index_and_notify);
this.index = 0;
},
add_index_and_notify: function(model){
model.set({go_index : this.index++}, {silent : true});
// Render the model here
},
render: function(){
this.$el.empty().append(Your template);
this.index= 0;
_.each(this.collection.models, function(model){
this.add_index_and_notify(model);
}
}

Related

Backbone pre rendering of collection models

I want to perform an action, clearing parent element, after a collection has fetched his models but prior to the models rendering.
I've stumbled upon before and after render methods yet they are model specific, which will cause my parent element to clear before every model rendering.
I'm able of course to perform the action pre-fetching yet I want it to occur when fetch is done and before models are rendered.
I tried using reset and change events listening on the collection yet both resulted unwanted end result.
Reset event seamed to go in that direction yet the passed argument was the entire collection and not a single model from the collection, therefore using the add event callback wasn't possible due to difference in argument type (collection and not a model as required)
Any ideas how to invoke a callback when fetch a collection fetch is successful yet models are yet to be rendered?
The model contains the returned attributes while collection contains url for fetching and parse method to return argument wrapped object.
Below is the code I use to render the collection view, which is basically rendering each model's view within the collection.
Collection View
---------------
var FoosView = Backbone.View.extend({
el: '#plans',
events: {
//'click tr': 'rowClick'
},
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
_.bindAll(this, "render");
this.render();
},
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
...
});
The model view
--------
var FooView = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.options = options || {};
this.tpl = _.template(fooTpl);
},
render: function() {
var data = this.model.toJSON();
this.$el.html(this.tpl(data));
return this;
}
});
Thanks in advance.
OK, I think I understand your question and here is a proposed solution. You are now listening to the reset event on your collection and calling this.renderAll. this.renderAll will take the list of models from the collection and render them to the page, but only AFTER the list element has been emptied. Hope this helps :).
var FoosView = Backbone.View.extend({
el: '#plans',
collection: yourCollection, // A reference to the collection.
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
this.listenTo(this.collection, 'reset', this.renderAll);
},
renderAll: function() {
// Empty your list.
this.$el.empty();
var _views = []; // Create a list for you subviews
// Create your subviews with the models in this.collection.
this.collection.each(function(model) {
_views.push(new FooView({model: model});
});
// Now create a document fragment so as to not reflow the page for every subview.
var container = document.createDocumentFragment();
// Append your subviews to the container.
_.each(_views, function(subview) {
container.appendChild(subview.render().el);
});
// Append the container to your list.
this.$el.append(container);
},
// renderNew will only run on the collections 'add' event.
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
});
I am forced to assume a few things about you html, but I think the above code should be enough to get you up and running. Let me know if it works.
I'm not totally sure about what you are asking but have you tried:
MyCollection.fetch({
success: function(models,response) {
//do stuff here
}
});
Also you may be interested taking a look at http://backbonejs.org/#Model-parse
Hope it helps!
Edit: there is no direct link between fetching and rendering my bet is that you binded rendering to model change.
USE===============>>>> http://backbonejs.org/#Model-parse

Backbone.js - delete views

This is my second day trying to use backbone and im completely lost. I am following this tutorial - http://net.tutsplus.com/tutorials/javascript-ajax/build-a-contacts-manager-using-backbone-js-part-3/
What I have done is loaded a contacts list and rendered it to the screen, but if you look at my render1 function - this takes a form input and appends it to my template. The problem is that I can't delete these items after they are created - the others can be deleted. help please?
var ContactView = Backbone.View.extend({
tagName: "contacts",
className: "contact-container",
template: $("#contactTemplate").html(),
initialize: function(){
this.model.on('change', this.render, this);
this.model.on('add', this.render1, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .deleteUser': 'delete'
},
test: function () {
alert("here");
},
delete: function () {
this.model.destroy();
},
render: function () {
console.log(this);
var tmpl = _.template(this.template);
$(this.el).html(tmpl(this.model.toJSON()));
temp = tmpl(this.model.toJSON());
console.log(temp);
return this;
},
render1: function () {
console.log(this);
var tmpl = _.template(this.template);
temp = tmpl(this.model.toJSON());
temp='<contacts class="contact-container">'+temp+'</contacts>';
console.log(temp);
$("#contacts").append(temp);
$(this.el).html(tmpl(this.model.toJSON()));
return this;
}
});
var AddPerson = Backbone.View.extend({
el: $("#addPerson"),
// el: $("form/"),
events: {
'click': 'submit',
'submit': 'submit'
},
submit: function(e) {
// alert("here");
e.preventDefault();
this.collection = new Directory();
// var data = (contacts[0]);
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
}
});
seasick, there are quite a few issues in this code.
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
Contact is a Backbone.Model but ContactView is a Backbone.View. Yet, you are adding both to the this.collection (which I assume is a Backbone.Collection of Contact?). See the problem here? In Backbone, there is no such concept of a 'collection of views'. You just get one concept: views, that are tied to a model.
So, here, you create a Contact and you add it to the Collection. That is all! It takes care of the Model part. The rendering part needs to be handled with events and renders.
When you add a model to a collection (this.collection.add(contact)), the collection will trigger a 'add' event, that you can hook to with a .on to create a new ContactView and append it to the DOM somewhere.
So when you write this...
this.model.on('add', this.render1, this);
You are actually saying 'When the Contact model triggers an add event, run render1', which isn't what you want, what you probably want is a collection.on('add', ...). The model will never trigger an add event (well, you could make it trigger one, but it wouldn't be an expected behavior!), the add/remove events are at the collection level.
In other words, you are missing some binding on the collection in your AddPerson view to 'react' to adding a new Contact to the collection. The code of the function bound to the add event should probably look a bit like:
onAdd: function(newContact){
var newContactView = new ContactView({model: newContact});
$("#contacts").append(newContactView.render().el);
}
There are other issues in your code, but I guess an outline of the steps to take would be like:
Remove the binding to add in ContactView: ContactView is only concerned with one contact, not how to manage multiple contacts. This is probably why you are having issues with only some (the first one?) of the contacts 'working'
Move that logic to the AddContact view which seems to be more concerned with the Collection of contacts. Use the collection 'add' event to create new ContactView and append them to the DOM
Hope this helps!

How to render item views for a collection including an add new view?

I don't understand why the DOM is not updated in the collection view render:
class FastTodo.Views.TodoItemsIndex extends Backbone.View
template: JST['todo_items/index']
el: '#main'
render: ->
$(#el).html #form_view.render()
#collection.each #renderOne
renderOne: (item) ->
console.log(#)
console.log(#el)
$(#el).append "model data"
initialize: ->
#collection = new FastTodo.Collections.TodoItems()
#form_view = new FastTodo.Views.AddTodoItem collection: #collection
#collection.bind 'reset', =>
#render()
#collection.on 'add', (item) =>
#renderOne(item)
#collection.fetch()
The idea is that #main first get a view with add new form, and then the collection is appended to #main.
How would I do this?
The output of the view in the console looks like:
1) For #collection.each #renderOne to work correctly you need to bind your renderOne method to the view instance like this: renderOne: (item) => (notice the fat arrow), because otherwise it is invoked in the global context (that's why you see these Window objects in your console.
2) DOM element, not the view itself, should be inserted into DOM, so $(#el).html #form_view.render() should be written as #$el.html #form_view.render().el (the render method should return the view instance according to the backbone community convention).
Other looks fine and it should work this way.
You may wish to refer to some posting about context in js to deeper understand the subject (this one for example).
btw you can write less code for some things. i.e. this
#collection.bind 'reset', =>
#render()
#collection.on 'add', (item) =>
#renderOne(item)
can become this
#collection.on 'reset', #render
#collection.on 'add', #renderOne
but you should remember to bind your render method with the fat arrow in this case.

Backbone: Update not calling callback function

I'm trying to add the functionality to my app so I can update my database and then update the DOM. The database gets updated fine, but the DOM doesn't. Here is part of my view:
App.Views.Table = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
this.model.on('change', this.render, this);
this.model.on('update', this.remove, this);
this.template = this.options.template;
this.url = this.options.url;
},
events: {
'click .verify': 'verify',
'click .spam': 'spam',
'click .duplicate': 'duplicate'
},
verify: function(e) {
id = e.currentTarget.id;
table = new App.Models.Table({ id: id });
table.urlRoot = this.url;
table.fetch();
table.toJSON();
table.set('verified', 1);
table.save();
},
spam: function(e) {
...
},
duplicate: function(e) {
...
},
remove: function() {
this.$el.remove();
console.log('hello');
},
retrieveTemplate: function(model) {
return _.template($('#' + this.template).html(), model);
},
render: function() {
//console.log(this);
this.$el.html(this.retrieveTemplate(this.model.toJSON()));
return this;
}
});
As I understand it, this.model.on('update', this.remove, this); should call my remove function when save completes. But the callback isn't firing because I'm not getting the console.log and my DOM isn't being updated. What am I doing wrong? I followed a tutorial, but everything works fine in the tutorial.
There is no update event. I think you mean sync
http://backbonejs.org/#Events-catalog
"sync" (model, resp, options) — when a model has been successfully synced with the server.
Also a helpful hit for debugging that I found was using the all event to see what events are getting triggered.
Edit:
After some debugging, the goal of the verify() function was to save the verified attribute to the model. To do that we needed to change verify() to
this.model.set('verified', 1);
this.model.save();
instead of creating a new App.Model.Table and setting it to the table variable. Doing table .save() was saving the new table model, instead of the old model, this.model. That's why the event handlers attached to this.model were net getting triggered.
There are no "create" or "update" events in Backbone.js. That's why your remove() callback isn't firing.
See http://backbonejs.org/#Events-catalog for a catalog of the possible events in Backbone.
UPDATE:
After looking at your code more closely, the answer is clear. They are different models:
initialize: function(options) {
this.model.on('change', this.render, this);
this.model.on('sync', this.remove, this);
and
table = new App.Models.Table({ id: id });
...
table.save();
The events that occur on your table object are not going to trigger event handlers that are bound to a totally different model (this.model).
Why create another model (table) when you already had a model? (this.model) ?
* UPDATE *
I don't really understand what you're trying to do, but perhaps try this:
table = new App.Models.Table({ id: id });
table.on('sync', this.remove, this);

Backbone.js not iterating through collection

Bit of an odd one...
I have the collection:
class Store.Collections.Product extends Backbone.Collection
url: '/api/products'
model: Store.Models.Product
With the view:
class Store.Views.Origin extends Backbone.View
initialize: ->
#collection = new Store.Collections.Product()
#collection.fetch()
#model.bind('change:formatted', #render, this);
#render()
events:
'change [name=origin]': 'setOrigin'
el: =>
#options.parent.$('.origin-input')[0]
template: JST["backbone/templates/shapes/product"]
render: ->
$this = $(this.el)
$this.html(#template(model: #model.toJSON(), errors: #model.errors))
console.log(#collection)
#collection.each(#appdenDropdown)
#delegateEvents()
this
appdenDropdown: (product) ->
console.log("append trigger")
#view = new Store.Views.Products(model: product)
#$('#history').append(view.render().el)
with the template:
<div id="history"></div>
The collection works... the
console.log(#collection)
shows the data! however
#collection.each(#appdenDropdown)
Does not do anything, doesn't error, or through anything. It just doesn't do anything. I am trying to extract the data out of the collection! But it wont...
It's because there's nothing in the collection yet.
#collection.fetch() in the initializer is an asynchronous method. You have to wait until the fetch is complete before you iterate through the collection items.
The fetch() function takes an optional success callback that is fired when the fetch is complete.
So you can update your initializer code to wait until the collection is fetched before calling render. Here is the code.
initialize: ->
#collection = new Store.Collections.Product()
#collection.fetch
success: =>
#render()
The problem as others have mentioned is that fetch is asynchronous, but the solution is simpler: jQuery's deferred object:
initialize: ->
#collection = new Store.Collections.Product()
#collection.deferred = #collection.fetch()
#model.bind('change:formatted', #render, this);
#collection.deferred.done ->
#render()
What's happening here is that when you call #collection.deferred.done, you're telling jQuery to wait until the collection is loaded before executing render on it, which is what you want. I think that should work.
A couple good references on deferred:
http://lostechies.com/derickbailey/2012/02/07/rewriting-my-guaranteed-callbacks-code-with-jquery-deferred/
http://www.erichynds.com/jquery/using-deferreds-in-jquery/

Resources