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.
Related
I'm trying to understand what the best practice is for communicating between the different components of a Backbone project. I've been re-implementing the Backbone version of TodoMVC and my question is around removing models from the collection object.
I understand that Backbone model instances trigger a change event whenever any of its model properties are modified via .set() and that the view associated with the model instance should .listenTo() changes from the model instance and then re-render. However, what's the best practice for communication between models and the collection object that houses it? For example, how should the communication work when a model is removed from the collection object?
Here's what I think: when a model is removed, I think the model instance should emit a custom event that the collection object is listening for and pass itself along. When the collection object hears this event, it should remove the model instance from the list (along with any event listeners attached to the model) and then the entire collection object should re-render itself. This re-rendering process will then create a new set of models and model-views.
Is this the best approach? I would love to hear your input! To me, this process of re-rendering sounds really expensive since you'll have to destroy the existing DOM elements, remove their event listeners, and then re-create them again.
UPDATE - 3/26/2015
To make this more concrete, I'll include the code that I have so far and point out where I feel my understanding is off.
File Structure
collections
a. todoList.coffee
models
a. todo.coffee
views
a. todoView.coffee
b. todoListView.coffee
app.coffee
app.coffee
window.app = app = window.app || {}
data = [
{
title: 'Eat dinner',
completed: false
}
{
title: 'Go to gym',
completed: true
}
]
app.todos = data.map (todo) -> new app.Todo todo
app.todoList = new app.TodoList app.todos
app.todoListView = new app.TodoListView
collection: app.todoList
app.$app = $('#todo-app')
$('#todo-app').append app.todoListView.render().el
todo.coffee
window.app = app = window.app || {}
app.Todo = Backbone.Model.extend
defaults:
title: ''
completed: false
toggleComplete: ->
this.set 'completed', !this.get 'completed'
todoList.coffee
window.app = app = window.app || {}
app.TodoList = Backbone.Collection.extend
model: app.Todo
initialize: () ->
# This is what I don't like - creating 'remove-todo' event
this.on 'remove-todo', this.removeTodoFromList
removeTodoFromList: (model) ->
this.remove model
getCompleted: ->
this.filter (model) -> model.completed
getNotCompleted: ->
this.filter (model) -> !model.completed
todoView.coffee
window.app = app = window.app || {}
app.TodoView = Backbone.View.extend
tagName: 'li'
events:
'click input' : 'checkComplete'
'click .delete' : 'removeTodo'
checkComplete: (e) ->
this.model.toggleComplete()
removeTodo: (e) ->
# I don't like how the collection is listening for this custom event 'remove-todo'
this.model.trigger 'remove-todo', this.model
initialize: ->
this.listenTo this.model, 'change:completed', () ->
this.render()
render: ->
template = _.template $('#todo-view').html()
this.$el.html template this.model.toJSON()
return this
todoListView.coffee
window.app = app = window.app || {}
app.TodoListView = Backbone.View.extend
tagName: 'ul'
className: 'todo-list'
initialize: ->
this.collection.on 'remove', (() ->
this.resetListView()
this.render()
), this
addOne: (model) ->
todoView = new app.TodoView
model: model
this.$el.append todoView.render().el
resetListView: ->
this.$el.html('')
render: ->
_.each this.collection.models, ((model) -> this.addOne model), this
return this
Explanation of Code
As you can see in my comments above, whenever a click happens on the remove button, my todoView triggers a custom event 'remove-todo'. The todoList collection listens to this event and removes the specific model from the collection. Since a 'remove' event is triggered whenever a collection removes a model, the todoListView listens for this 'remove' event and then re-renders. I feel like I'm off somewhere. Any advice?
It seems you are talking about views when you talk about models. When a model is removed from a collection you don't need a custom event, a "remove" event is triggered.
http://backbonejs.org/#Collection-remove
If you want to remove the corresponding view, use http://backbonejs.org/#View-remove this will manage the DOM and listener.
The re-rendering of view (I don't understand what you are talking about when you talk about rerendering collection) can be triggered if you listen to "remove" models from the collection, otherwise listen to http://backbonejs.org/#Collection-add, or http://backbonejs.org/#Model-hasChanged is you want to rerender your view when a model has changed.
I hope it helps.
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 have a Marionette.CompositeView which needs to render a collection.
I would like to filter this collection on fetch and add action.
I tried with the following code (1) but I get the following error (2).
Any ideas, thanks.
(1)
var myCompositeView = Marionette.CompositeView.extend({
initialize: function () {
this.collection = app.taskCollection.where({type: 'todo'});
}
});
(2)
// Uncaught TypeError: Object has no method 'on'
Marionette's CompositeView and CollectionView both expect the collection setting to be a valid Backbone.Collection. The where method on Backbone's collection does not return a Backbone.Collection, it return an array. So you have to wrap a collection around the results:
initialize: function(){
var filtered = app.taskCollection.where({type: 'todo'});
this.collection = new Backbone.Collection(filtered);
}
Of course you can use any type that extends from Backbone.Collection. I just wanted to illustrate the point of it being a collection with this example.
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/
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