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
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 encountered a problem I don't understand. I'm playing with Backbone and one of my initializer is called twice, one on purpose (when I instantiate my object) and it seems like it's called a second time from the constructor itself.
Here is my code :
class Views extends Backbone.Collection
model: View
initialize: ->
_.bindAll #
class View extends Backbone.View
initialize: ->
_.bindAll #
console.error 'Inner'
views = new Views
console.log 'Outer'
views.add new View
When I run this code, Outer is displayed once while Inner is displayed 2 times. Here is the stack trace :
Any idea about this ?
When you initialize a collection, the first argument is the list of models to pre-populate it with.
class Models extends Backbone.Collection
model: Model
initialize: (#rawModels) ->
# CoffeeScript has the fat arrow that renders this unnecessary.
# But it's something you should use as sparingly as possible.
# Whatever. Not the time to get into that argument.
_.bindAll #
# At this point in time, all the models have been added to the
# collection. Here, you add them again. IF the models have a
# primary key attribute, this will detect that they already
# exist, and not actually add them twice, but this is still
# unnecessary.
_.each #rawModels, #addItem
# assuming this was a typo
addItem: ( place ) -> #add new Model model
models = new Models json
Not directly related to your question, but hopefully helpful.
More directly related: don't create a collection of views. Collections are for storing Models. Backbone.View is not a type of Backbone.Model; they're separate. It doesn't really make sense -- you can just create an array of views -- and a lot of operations won't work right on that view collection.
Which is what's happening here.
When you call Backbone.Collection::add, it tries to see if what you're adding is a Backbone.Model. Since it's not, it assumes you're trying to add a JSON blob that it wants to turn into a Model. So it tries to do that...using its this.model class as a guide. But since that's View, it creates another one and adds that instead (not checking after the fact that it actually produced a Backbone.Model).
You can follow the call stack from add to set to _prepareModel, where the second View is instantiated.
I'm new to Backbone (and Marionette), and trying to write a pretty simple app using both. The app has a menu of "groups" on the left nav, and a list of "entries" on the main right div. Every time a Group menu item is clicked, I filter the entries with the group ID and show them, when hide all others.
Here is the Entry Item view (all scripts are in CoffeeScript btw):
class EntryItemView extends Backbone.Marionette.ItemView
tagName: 'tr'
template: _.template $('#entryItemTemplate').html()
render: ->
#$el.html #template(#model.toJSON())
show: ->
#$el.show()
hide: ->
#$el.hide()
Here is the Entry List view, extending Marionette's CollectionView:
class EntryListView extends Backbone.Marionette.CollectionView
itemView: EntryItemView
el: '#main tbody'
This is the AppRouter, pretty much straightforwad:
class AppRouter extends Backbone.Router
routes:
'group/:id' : 'showGroup'
router = new AppRouter()
router.on 'route:showGroup', (id) ->
_.each entryViews, (view) ->
if view.model.get('group_id') is parseInt(id)
view.show()
else
view.hide()
(The entryViews variable is a simple global array to store all EntryItemView instances).
With this approach, navigating the app to /group/:id indeed invokes the show() and hide() method of each EntryItemView object. The problem is, looks like the reference between this object and the actual HTML doesn't exist, so the actual element <tr> doesn't show or hide.
Can you guys point out what I'm doing wrong here? Thanks in advance.
Here are a couple pointers:
since your template is in the HTML, you just specify the jQuery selector with template: "#entryItemTemplate"
you can remove the render declaration, because Marionette does that on its own (i.e. you're implementing the default behavior)
unless you know what you're doing, you typically don't declare an el property in a collection view. Instead you declare a region (possibly within a layout), where you will call the show method to display a view instance
The reason your code probably doesn't work is that it looks like Backbone code with some Marionette stuff thrown in. Take a look at the free sample to my book on Marionette. It should get you started quickly with Marionette and will explain most of what you're trying to accomplish here.
I'm trying to render an item at the start of a collection (imagine if you were posting a new record on facebook)
When I come to add(response, {at: 0}); into the collection, the record is correctly inserted at 0 into the collection, but is rendered at the bottom of the list of items. I'm confused as I had this working before, but I think what I was doing in a hacky style, was just reset and re-rendering the collection.
I'm wondering what the tidy way to handle this, and where should I bind the logic.
Is it on the add method of the collection? Currently this is empty (but I am using Marionette) and I feel that this overrides the default rendering of backbone. How do I take control of it again, so I can correctly get my new item to be added to the list, without destroying it all and re-creating it.
In Marionette, the default way to add a new item to a collection in the views, is to use jQuery's append method. The CollectionView type has a method called appendHtml which is used to do the actual appending. (see http://derickbailey.github.com/backbone.marionette/docs/backbone.marionette.html#section-24 )
You can easily override this method in your specific collection view, though, and have the new model appended wherever it needs to go.
In your case, if you are always wanting to prepend the new model to the top of the list, it's very trivial to change your collection view to do this:
Backbone.Marionette.CollectionView.extend({
appendHtml: function(cv, iv){
cv.$el.prepend(iv.el);
}
});
Note that cv is the collection view instance and iv is the item view instance for the model in the collection.
If you need to do more complicated things like find an exact position in the existing collection of HTML nodes, you can do that within the appendHtml function as well. Of course this gets more complicated than just doing a prepend instead of an append, but it's still possible.
Hope that helps.
This might be a very old question, but I'll post my solution anyway...
It prepends/appends the itemViewContainer with new models depending on how they were added to the collection (unshift/add), by overriding the appendHtml function.
appendHtml: function(collectionView, itemView, index){
if(index > 0) {
collectionView.$el.find(this.itemViewContainer).append(itemView.el);
} else {
collectionView.$el.find(this.itemViewContainer).prepend(itemView.el);
}
},
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.