Warning: Code is in Coffeescript. I hope that's ok.
I have a model, Song, a collection Songs, and a view SongsView. Here it is:
SONG_TEMPLATE = '''
<table>
{{#if songs.length }}
{{#each songs}}
<tr><td>{{ this.name }}</td><td>{{ this.duration }}</td></tr>
{{/each}}
{{/if}}
</table>
'''
$ ->
class Song extends Backbone.Model
parse: (response) ->
console.log "model parsing #{response}"
#
class Songs extends Backbone.Collection
initialize: ->
#model = Song
#url = "/songs/data"
parse: (response) ->
console.log "collection parsing"
console.log response
# This works. The JSON here was grabbed right out of the XHR response I got from the server and pasted into my code.
songs = new Songs(
[
{"name":"Stray Cat Strut","rating":4,"duration":3},
{"name":"Chinatown","rating":2,"duration":4.2},
{"name":"Sultans of Swing","rating":3,"duration":5.4},
{"name":"Pride & Joy","rating":3,"duration":3}
]
)
# This fails. It should be exactly the same as the above code, and indeed, the collection parsing takes place.
# However, the view renders nothing.
# songs = new Songs
# songs.fetch()
class SongsView extends Backbone.View
initialize: ->
#model = Song
#render()
render: =>
console.log "render"
console.log #collection
template = Handlebars.compile(SONG_TEMPLATE)
#template = template(songs: #collection.toJSON())
console.log "template: #{#template}"
$('#song-list').html #template
songView = new SongsView(collection: songs)
The issue I'm having is that there is some subtle difference between initializing songs from the JSON string and allowing backbone to populate it using fetch(). The object looks ok in the script debug window, but no joy.
So, what's going on here and am I sort of on the right track?
Thanks
Fetch is an asynchronous method, this means that when you render your view, the data has not been retrieved, but when you write it manually, the data is there. The general way to do this is to bind the reset trigger that gets called by fetch to the render method.
class SongsView extends Backbone.View
initialize: ->
#model = Song
#collection.bind("reset", #render)
render: =>
console.log "render"
console.log #collection
template = Handlebars.compile(SONG_TEMPLATE)
#template = template(songs: #collection.toJSON())
console.log "template: #{#template}"
$('#song-list').html #template
You should probably more your songs.fetch below where you instantiate your view too.
As answered by Gazler, the problem is that fetch is asynchronous. If you want to use Gazler's solution, be aware that the collection's fetch method no longer triggers the reset event by default. Therefore, you'll need to have the collection explicitly trigger the reset event:
my_collection.fetch({reset: true})
Another solution to solving this is using jQuery deferreds to display the view once the results have been fetched. More one using deferreds to manage view display when fetching data asynchronously: http://davidsulc.com/blog/2013/04/01/using-jquery-promises-to-render-backbone-views-after-fetching-data/
And waiting for multiple asynchronous data sources to return: http://davidsulc.com/blog/2013/04/02/rendering-a-view-after-multiple-async-functions-return-using-promises/
Related
I am making an app using OMDB api. I have define a method 'top_movies' in 'movie_controller' which is rendering json data.
I have defined collection like this:
class Fanboy.Collections.Movies extends Backbone.Collection
url: '/movie/top_movies'
Click to see JSON response
I fetched the collection in console and getting the objects in this manner.
Click to see the Image view of console
I want to display the list on a page. But I am not able to show the list.
movies_router.js.coffee
class Fanboy.Routers.Movies extends Backbone.Router
routes:
"": "index"
initialize: ->
#collection = new Fanboy.Collections.Movies()
#collection.fetch()
index: ->
view = new Fanboy.Views.MoviesIndex(collection: #collection)
$('#movies-container').html(view.render().el)
/view/movies_index.js.coffee
class Fanboy.Views.MoviesIndex extends Backbone.View
template: JST['movies/index']
initialize: ->
#collection.on('reset', #render, this)
render: ->
$(#el).html(#template(movies: #collection))
this
/templates/movies/index.jst.eco
<h4>Top movies of all time </h4>
<ul>
<% for movie in #movies.models : %>
<li><%= movie.get('Title') %></li>
<% end %>
</ul>
Here I am able to see h4 tag but not the list of Titles. What I am doing wrong here? Please someone help.
Update:- I did some debugging. Since the url defined is coming through Omdb API and the view are loaded asynchronously in backbone. Hence the issue.
So I put setTimeout for few seconds and now I am able to display the list. But this makes the app slower. What can I do now?
Though #collection.on('reset', #render, this)should have handled the issue. But why it is not able to?
Instead of 'reset' use 'sync':
#collection.on('sync', #render, this)
"sync" (model_or_collection, response, options) — when a model or collection has been successfully synced with the server.
Backbone.js Catalog of Events
I've got an event in my view which on keyup does a fetch request to the TMDB api,
class Movieseat.Views.Moviesearch extends Backbone.View
template: JST['movieseats/moviesearch']
el: '#moviesearch'
initialize: (opts) ->
#collection.on('reset', #render, this)
{#collection} = opts
#render()
return
render: ->
$(#el).html(#template(collection: #collection))
return
events:
"keyup input": "doSearch"
doSearch: (e) ->
#collection.setQuery $(e.currentTarget).val()
#collection.fetch()
view = new Movieseat.Views.Movie()
$('#movies').append(view.render().el)
This is my collection,
class Movieseat.Collections.Moviesearch extends Backbone.Collection
url: -> "http://api.themoviedb.org/3/search/movie?api_key=a8f7039633f2065942cd8a28d7cadad4&query=#{#query}"
setQuery: (q) ->
#query = q
return
So if my input is inception this is the fetch request,
http://api.themoviedb.org/3/search/movie?api_key=a8f7039633f2065942cd8a28d7cadad4&query=inception
As you can see in my view I'm append a template called movies. I would like to show all of the original_title from the fetch request in that template. And update the template when it changes.
From what I understood, your collection holds elements of type movie or something like that. After you fetched the collection, it's models should contain the original_title and poster_path. Now you can use pluck method to read all those properties. The code should look like that (javascript):
collection.on("sync", function() {
myView.displayTitles(collection.pluck("original_title"));
})
Update
With this code update you posted, I'm wondering why don't you use those properties inside a collection's template. Given that your collection doesn't declare the type of models it contains, you cannot use the #model.get('prop') syntax, as #model is not a Backbone model. Try using
#model.prop
syntax instead.
I'm writing a small app using Backbone. I start creating a SongsView which creates a SongsCollection. I fetch this collection to retrieve the data from an external API I wrote. The next step is to render the fetched data using the toJSON method, however calling toJSON returns [undefined], despite the fact that the collection is an instance of Bakcbone.Collection.
Here is my code (in coffeescript):
App:
songs = new SongsView
songs.render()
SongsView:
SongsCollection = require 'scripts/collections/songs'
class SongsView extends Backbone.View
el: '#songs'
render: ->
songs = new SongsCollection
songs.fetch
success: ( res ) =>
console.log (res instanceof Backbone.Collection) # true
console.log (res instanceof SongsCollection) # true
console.log (res.toJSON()) # [undefined]
SongsCollection:
Song = require 'scripts/models/song'
class SongsCollection extends Backbone.Collection
model: Song
url: 'http://localhost:1337/songs'
Song:
class Song extends Backbone.Model
constructor: ({#name, #path}) ->
console.log 'new'
EDIT: If I look at the prototypes chain, I can find a toJSON() method though :
EDIT²: Same behavior for a single model :
console.log (res.models[0].toJSON()) # undefined
Which is actually interesting. It means that the toJSON method from the SongsCollection works but the toJSON from Song does not. I'll dig deeper there.
Problem solved. I was using constructor instead of initialize which leads to create a model without any attributes, thus, calling toJSON returned undefined as the attributes property was not defined.
class Song extends Backbone.Model
initialize: ({#name, #path}) ->
console.log 'new'
I have a router accessing its collection. My for loop wasn't iterating through the models so I tried logging the collection to see what it returned. Turns out when I log the collection directly I see all of the models as expected. But if I try to log the models attribute of the collection I get an empty array! It doesn't make sense. These lines are directly following each other. I tried changing the order and got the same outcome.
console.log(this.collection);
=> Shots
_byCid: Object
_byId: Object
length: 15
models: Array[15]
__proto__: Shots
...
console.log(this.collection.models);
=> []
console.log(this.collection.length);
=> 0
Why would this happen?
Here is the code as it is in the router to give a better context of where this code is firing:
# Routers
class Draft.Routers.Shots extends Backbone.Router
routes:
'' : 'index'
'shots/:id' : 'show'
initialize: ->
#collection = new Draft.Collections.Shots()
#collection.fetch()
index: ->
console.log #collection
console.log #collection.models
Jim,
This doesn't fix your problem - you've worked that out. But it explains why you're seeing the console output you see.
When you run console.log(this), you output the object itself and the console links references (pointers if you like) to the inner variables.
When you're looking at it in the console, at the time the console.log(this) runs the models area is empty, but at the time you look at the logs, the collection has finished loading the models and the inner array variable is updated, AND the reference to that variable in the object log shows the current content.
Basically in console.log(this),inner models variable continues its normal life and the console shows the current status at the time you're looking at it, not at the time you called it.
With console.log(this.models), the array is dumped as is, no reference is kept and all the inner values are dumped one by one..
That behaviour is quite simple to reproduce with a short timeout, see this fiddle.. http://jsfiddle.net/bendog/XVkHW/
I found that I needed to listen for the collection to reset. So instead of passing the model into the view I created another view expecting the collection and listened for the 'reset' event to fire 'render' for the view.
# Routers
class Draft.Routers.Shots extends Backbone.Router
routes:
'' : 'index'
'shots/:id' : 'show'
initialize: ->
#collection = new Draft.Collections.Shots()
#collection.fetch()
index: ->
view = new Draft.Views.Desktop(collection: #collection)
# Views
class Draft.Views.Desktop extends Backbone.View
el: $("body")
initialize: ->
#collection.on("reset",#render,this)
render: ->
console.log #collection
console.log #collection.length
You can use a promise. (.done will do fine)
#collection.fetch().done =>
for model in #collection.models
console.log model
this will give you #collection's models fetched and ready to go.
or if you don't need to force the app to wait,
#collection.on 'sync', =>
for model in #collection.models
console.log model
Both of these will let you do what you want.
I'm having some trouble deleting an item from a collection inside a model when in a view. Basically the model/collection structure is the following:
Basically when I try to remove an item from the sub item collection in the sub item view it actually removes the correct item from the collection. However when I come to persisting the main model the item seems to be still in the collection.
This is the how my views are structured:
The main view inserts the DOM nodes required by the main model, and them main model creates a new view for the item model etc. All views are getting the main model as model option like so:
new App.Views.MainModelView({
model : this.model,
el : $('#nodeID')
})
The only difference is in the creation of the Sub-item model view, where, due to re usability of the view and template, I still pass in the main model, however I also pass in the item in the item collection that is currently being modified. Which looks like this:
new App.Views.ItemView({
model : this.model,
item : this.selectedItem,
el : $('#nodeID')
});
In the sub-item's view init method I do the following:
this.item = (this.options.item) ? this.options.item : this.model;
To remove a sub-item from the sub-item collection I do:
removeSubItem : function(e) {
// get the id of the sub-item to be removed
var id = $(e.target).closest('tr').attr('data-id');
if (!id) throw "Could not retrieve data id";
// retrieve the sub-item from the collection
var subItem = this.item.subItems.get(id);
// remove the sub-item from the collection
this.item.subItems.remove(subItem);
},
As I said earlier when I remove the sub-item and I inspect the collection modified by the view I can see that the sub-item has been removed from the collection, however then I persist the main model the removed sub-item re-appears. The leads me to believe that somewhere along the line the sub-item collection might be cloned which could explain the sudden reappearance of the sub-item.
I know this is a fairly specific problem and I'm not sure if it is possible to get to the cause of the problem with what I have provided here, if you need any more information please let me know.
Thanks for all your help,
Vincent
========== EDIT ============
To answer some of the questions below let me outline the scope in which I am experiencing this issue:
If I console log the this.item.subItems collection in the SubItem view, after removeSubItem was called, I can see that the instance of the SubItem model has been removed successfully.
Before I call the save method on the main model I console log the return of the toJSON function. At this point I am experiencing the problem that the previously removed instance is 'back' in the collection. I have been monitoring the traffic between client and server with both Wireshark and Google chrome's developer console and there is no call to the server to refresh any of the models.
The toJSON method for the SubItem collection looks like this:
toJSON : function() {
App.log(["SubItem::collection::toJSON", this], "info");
var json = {};
// make sure the key for each SubItem is the primary key
this.each(function(subItem) {
json[subItem.get('id')] = subItem.toJSON();
});
return json;
}
Backbone.js support for nested collection/models is non-existent, and they provide no saving support (see http://documentcloud.github.com/backbone/#FAQ-nested). You have to override toJSON on any model with a subcollection. I've run into this scenario a million times. If you have something like (in coffeescript):
class MainModel extends Backbone.Model
itemCollection: ->
#_itemCollection ?= new ItemCollection(#get('itemCollection'))
class ItemCollection extends Backbone.Collection
model: ItemModel
class ItemModel extends Backbone.Model
subCollection: ->
#_subCollection ?= new SubCollection(#get('subCollection'))
class SubCollection extends Backbone.Collection
model: SubModel
class SubModel extends Backbone.Model
mainModel = new MainModel(json)
Then in order for mainModel.save() to work, you need to override toJSON on MainModel and ItemModel, like:
class MainModel extends Backbone.Model
itemCollection: ->
#_itemCollection ?= new ItemCollection(#get('itemCollection'))
toJSON: ->
return _.extend(#attributes, {itemCollection: #itemCollection().toJSON()})
class ItemModel extends Backbone.Model
subCollection: ->
#_subCollection ?= new SubCollection(#get('subCollection'))
toJSON: ->
return _.extend(#attributes, {subCollection: #subCollection().toJSON()})
I wrote the example in coffeescript because it's much more concise than javascript. If you need any help making sense of it, please just ask.
Hope this helps!
--- Note ---
Technically, in coffeescript, the toJSON methods could simply be:
toJSON: ->
_.extend #attributes, itemCollection: #itemCollection().toJSON()
But I wrote it the way I did to be more understandable to non-coffeescripters.
Without looking at your whole code base I think you might have your structure slightly wrong. Normally with backbone I hardly ever if ever pass the :el element directly into the view. The view is responsible for generating it's own el. After it has been rendered I then insert the new view.el into the DOM. Like below
var subView = new FooView({ model: fooModel });
mainView.$(".list").append(subView.el);
In the above case there is a backbone object with every subview. If you need to
remove the subview you don't need to do a selector query to find it, you just call
the remove method on the object and it knows how to remove itself from the dom.
Or to be more specific the subView handles a click event on itself which it
can then handle by destroying it's associated model and then calling remove on itself