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'
Related
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.
Here's the entirety of my simple Backbone app. When I instantiate a new view, I'm passing it a model, but when the view calls attributes on the model, it says model is undefined:
class App.Recipe extends Backbone.Model
class App.RecipeList extends Backbone.Collection
url: '/users/4/recipes'
model: App.Recipe
#recipeList = new App.RecipeList
#recipeList.fetch()
class App.RecipeView extends Backbone.View
template: _.template(#model.attributes)
render: ->
#$el.html(#template)
#
#recipeView = new App.RecipeView(model: recipeList.models[0])
$ ->
$('#featured_recipes').html(window.recipeView.render())
In the console, recipeList.models[0] returns a JSON object that does indeed have an attributes property. So when I instantiate the view and pass it a model, why do I get Uncaught TypeError: Cannot read property 'attributes' of undefined?
My guess is that #model is getting evaluated at runtime, before I pass in the model. Is there a way to defer that? Or am I wrong?
I've also tried the _.bindAll # in the initialize function of the view. No dice.
PS — I'm obviously a noob, so if you see other antipatterns or things I'm going to run into here, feel free to mention them.
Edit
I've tried the answer outlined here of adding the template inline in my initialize method like this:
class #App.RecipeView extends Backbone.View
tagName: 'li'
initialize: ->
_.bindAll #, 'render'
template = _.template $('#featured_recipes').html()
render: ->
#$el.html #model.toJSON()
#
But then I get Cannot call method 'replace' of undefined , which I know is from underscore calling template on something that isn't in the DOM yet, and that's happening because this gets called before my DOM renders. Any thoughts?
Edit 2
Now I've moved the entire app into the footer of my haml layout, so that the $(#featured-_recipes') div is in the DOM. Then I changed the view to this:
class #App.RecipeView extends Backbone.View
tagName: 'li'
initialize: ->
_.bindAll #, 'render'
#template: _.template $('#featured_recipes').html()
render: ->
#$el.html #template(#model.toJSON())
#
But I'm still getting Cannot call method 'replace' of undefined .
Try the following. I added an initialize method to your View, and I used Backbone's Collection get method to get your recipe.
class App.RecipeView extends Backbone.View
initialize: (options) ->
#model = options.recipe
template: _.template(#model.attributes)
render: ->
#$el.html(#template)
#
#view = new App.RecipeView
recipe: recipeList.get(0)
I'm using coffeescript. My code is pretty simple:
class SomeCollection extends Backbone.Collection
constructor: (#options) ->
url: ->
"#{$SCRIPT_ROOT}/some/data/#{#options.someId}"
model: SomeModel
class SomeView extends Backbone.View
initialize: ->
myCollection = new SomeCollection()
myCollection.fetch
success: (coll, resp) ->
console.log coll
The JSON that's being returned from my collection's url is exactly:
[{"id": 1, "comments": "", "name": "images/exceptions/59.png"}]
However, before anything is printed to the console, I receive a backbone.js error on line 768: Cannot read property 1 of undefined. The undefined object is this._byId within the collection's get function. How can I solve this problem?
You are extending Backbone.Collection and providing your own constructor, so you need to make sure to call the parent constructor.
constructor: (#options) ->
super null, #options
Also, the standard arguments for a Collection are (models, options), so I would stick with that.
constructor: (models, #options) ->
super models, #options
Or better yet, use initialize instead of constructor to avoid that entirely
initialize: (models, #options) ->
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.
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/