Backbone.js not iterating through collection - backbone.js

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/

Related

Backbone Marionette: CompositeView replacing items instead of appending them

I've got a Marionette CompositeView that I'm using to fill in a dropdown. The JSON response is clean when I call collection.fetch() from within the CompositeView, but instead of appending the new ItemViews, CompositeView seems to be replacing them in the DOM.
Here's my code (coffeescript):
class #PDCollectionItemView extends Backbone.Marionette.ItemView
el: 'li'
template: Handlebars.compile('{{ title }}')
class #PDCollectionsView extends Backbone.Marionette.CompositeView
id: 'pd_collections'
className: 'selection'
itemView: PDCollectionItemView
itemViewContainer: '.scroll ul'
template: HandlebarsTemplates['connections/collection_select'] #handlebars_assets gem
ui:
modalTrigger: '#pd_collection_selector'
modal : '#pd_selection_modal'
selectBtn : '#select_collection'
initialize: ->
#selectedCollection = undefined
Connectors.App.vent.on "connections:collectionStaged", #assignSelectedCollection
return #PDCollectionsView
And the parent layout where the fetch is called:
class #IndexLayout extends Backbone.Marionette.Layout
initialize: ->
#collections = new PDCollectionsCollection
#collectionsView = new PDCollectionsView
collection: #collections
onRender: ->
#collectionSelect.show #collectionsView
#collections.fetch
success: (collection, response, options) =>
Connectors.App.vent.trigger "connections:collectionsLoaded"
Connectors.App.vent.trigger "loadComplete"
error: (collection, response, options) =>
console.log response
I've tried manually appending the items with an appendHTML call, but I get the same behavior. I can log each itemView with a call to onAfterItemAdded on the #PDCollectionsView, and the item views are distinct; different cids, and the appropriate models.
I think the problem is in your use of Backbone's fetch operation. fetch "syncs" the collection with its state on the server. Without specifying any customization it will intelligently add new items, update changed items, and remove items no longer on the server. I'm guessing that if you examine the collection after you call fetch you'll see it's only got the items that are being rendered in the CompositeView.
You can modify fetch's behavior to sync to the server without removing anything by passing {remove: false}. This should yield the results you're looking for:
#collections.fetch
remove: false
success: (collection, response, options) =>
Connectors.App.vent.trigger "connections:collectionsLoaded"
Connectors.App.vent.trigger "loadComplete"
error: (collection, response, options) =>
console.log response

Backbone.js modify newly fetched collection before reset events occur

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);
}
}

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.

Correct Backbone.js collection event for when async data is retrieved

I want to render a view when a collection has been loaded asynchronously from the remote server. I have the following collection class
class BusinessUnits extends Backbone.Collection
model: BusinessUnit
parse: (units) ->
units
And then my view I was doing this:
load: (businessUnits) =>
#collection = businessUnits
#collection.fetch()
#render()
Obviously render() will be invoked before the fetch has been completed.
Is there a backbone.js event that is fired whenever the collection is fetched or would I be better firing my own?
This seems like a very common scenario. How do people handle this type of situation?
I think the "reset" event is what you are looking for.
"reset" (collection) — when the collection's entire contents have been replaced.
This will be triggered after the fetch completes.
load: (businessUnits) =>
#collection = businessUnits
#collection.bind 'reset', => #render()
#collection.fetch()

Representing existing HTML as Backbone.js data structures in CoffeeScript

I'm having a rough time wrapping my head around this.
I have an HTML list, and I want to use Backbone.js to handle events on those list items. Here's what I've got so far. This is a simplified scenario to help me better understand how to structure a larger application. For my example, I simply want to ingest an existing HTML list into the Backbone structure, and handle click events through the Backbone view.
I'm getting an error related to using #model in the view, but I'm fairly certain I'm misunderstanding things conceptually here.
CoffeeScript:
$ ->
class Item extends Backbone.Model
name: null
class ItemList extends Backbone.Collection
model: Item
class ItemView extends Backbone.View
tagName: 'li'
initialize: =>
#model.bind('change', this.render)
#model.view = this
events:
'click' : 'clicked'
clicked: ->
console.log 'clicked'
render: =>
this
class ItemListView extends Backbone.View
el: $('ul#test')
initialize: =>
$('li', #el).each(#addItem)
addItem: (item) ->
item = new ItemView({ el: item })
render: =>
this
Items = new ItemListView
HTML:
<ul id="test">
<li>Hi thar</li>
<li>Yeah</li>
<li>OK</li>
</ul>
Here's a jsfiddle I started earlier: http://jsfiddle.net/Saxx4/
I never really like CoffeeScript (Javascript is so nice, why replace it?), but it looks like there are a few issues here:
You're getting an error on #model because you never set it on the ItemView. This doesn't happen automatically - you have to either instantiate the view's model in initialize() or pass it into the constructor, e.g.:
addItem: (item) ->
model = new ItemView({
el: item,
model: new Item({
// assuming you might want the list item text
// in the model data
text: $(item).text()
})
})
You usually just want to specify a selector in el, not a jQuery object - otherwise the DOM might not be ready when you load your Backbone code: el: '#test'
You need to pass an options object to the ItemListView constructor, not just a single argument, no matter what you do in initialize():
class ItemListView extends Backbone.View
initialize: (opts) =>
opts.items.each(#addItem)
// ...
Items = new ItemListView({ items: $('ul#test li') })

Resources