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.
Related
Can one use differently defined models defined via require.js/AMD in a single central view? I mean separately defined models, not ones of a collection.
If yes, how they are referenced in the json part of the callback function of define() that defines that central view, in its vars, in functions of its attributes etc. There can only one this.model, right?
Is it possible to Render different templates, possibly populated by vars from those different models, conditionally, from within this single central view?
To extend my question:
Can one use differently defined collections defined via require.js/AMD in a single central view?
Can one use differently defined models defined via require.js/AMD in a single collection? (this alone could achieve first goal with referencing only this capable collection.)
Can one use differently defined models defined via require.js/AMD in a
single central view? I mean separately defined models, not ones of a
collection.
If yes, how they are referenced in the json part of the callback
function of define() that defines that central view, in its vars, in
functions of its attributes etc. There can only one this.model, right?
A View's this.model is just one way to provide a view with a model. You can also pass whatever other options you want to a view, like so:
var YourView = Backbone.View.exend({
initialize: function(options) {
this.foo = options.foo;
}
});
var modelA = new Backbone.Model();
var modelB = new Backbone.Model();
var yourView = new YourView({model: modelA, foo: modelB});
// yourView.model == modelA
// yourView.foo == modelB
Is it possible to Render different templates, possibly populated by
vars from those different models, conditionally, from within this
single central view?
Yup. For instance, here's an example if we add a render method to YourView:
render: function() {
if (this.model.get('bar')) {
this.$el.html(this.templateA(this.model.toJSON());
} else {
this.$el.html(this.templateB(this.foo.toJSON());
}
}
Can one use differently defined collections defined via require.js/AMD
in a single central view?
Yup, the same way as you pass in multiple models. It doesn't matter if you use Require; you can pass around Backbone objects/classes through it just fine.
Can one use differently defined models defined via require.js/AMD in a
single collection? (this alone could achieve first goal with
referencing only this capable collection.)
Yup, just throw all the models in to a single collection, like so:
var modelA = new ModelClass();
var modelB = new SomeOtherModelClass();
var collection = new Backbone.Collection([modelA, modelB]);
The only limit is that a Collection can only have a single model property, which means that whenever you create a new Model through the Collection (eg. via fetch or create) they will all be of that model.
(It's possible to get around even that with if you replace the model with a custom function, but you shouldn't need to do that.)
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.
Problem Space
I'm rendering some nested Ember views so I can make a splitter-pane style UI. I want to resize my views when they first render so that they'll have equal widths. I don't want my child views looking at each other, so I'm using a subclass of Ember.ContainerView to hold my content and draggable splitter handles.
I can't use Ember.View#didInsertElement on my container view, because I need to wait for my child views to be fully rendered.
My (attempted) Solution
I'm using the code presented in this answer: How to wait for a template to be fully rendered. This adds a property isRendered to all Ember.View instances that is set automatically when a template fires didInsertElement by re-opening Ember.View:
Ember.View.reopen
didInsertElement: ->
res = #_super();
#_setIsRendered();
res
_setIsRendered: ->
if (!! #$())
#set('isRendered', true)
else
Ember.run.next this, ->
#_setIsRendered()
I tried re-opening Ember.ContainerView to add a childViewsRendered property to all container views, but Ember objected and threw some very strange IndexOutOfBounds errors for container views with only one item in childViews.
I ended up putting my collection code in the following mixin:
App.ChildrenRendered = Ember.Mixin.create
childViewsRendered: (->
res = #get('childViews').everyProperty('isRendered')
console.log('childViewsRendered', res, this)
# Pointer to this most offensive object for debugging
window.wtf = this
res
).property('childViews.#each.isRendered')
_runChildViewsDidRender: (->
if #get('childViewsRendered')
console.log('trying to invoke childViewsDidRender')
Ember.tryInvoke(this, 'childViewsDidRender')
).observes('childViewsRendered')
And then I have a class like this:
App.SplitterView = Ember.ContainerView.extend App.ChildrenRendered,
# ...(some properties)...
init: ->
child_views = #get('childViews')
child_views.pushObjects([App.WindowView.create(), App.WindowView.create()])
What works:
App.SplitterView#childViewsRendered is computed once, before any views have rendered, and thus becomes false
Views are processed (inserted and rendered) by Ember, and set their own isRendered property fine and dandy.
Later running window.wtf.get('childViews').everyProperty('isRendered') returns true.
What doesn't work:
the computed property childViewsRendered never updates itself again.
Computed properties on dummy values on childView array element members also don't seem to work.
Without a jsFiddle to see I can only go by what you've said and the snippets you've provided but it seems like all you're trying to do is make sure all the childViews of a container are in the DOM before doing something, yes?
If that's the case... when views are added to childViews they are, as you know, automatically rendered and inserted into the DOM. Well this all happens within the same RunLoop. So to delay the execution of some function until all children are "inDOM" it's as easy as observing childViews.#each and using Ember.run.next to delay the execution until the next RunLoop. Here is an example
App.SomeContainerView = Em.ContainerView.extend
init: ->
#_super() # run default init
# add 2 of our 4 views
#get('childViews').pushObjects [#aView.create(), #bView.create()]
# a non-cached (volatile) computed property to check if children are inDOM
# just used for demo purposes
childrenAreInDom: (->
#get('childViews').every (v) ->
v.state is "inDOM"
).property().volatile()
# our observer
observeChildren: (->
# if the container isn't inDOM yet the afterRender function we add below
# will handle it, if views are added/removed after that then we handle it here
return unless #state is "inDOM"
# output to console if every childview are inDOM
console.log "observeChildren", #get('childrenAreInDom')
# the above will always be false since we're still in the same RunLoop
# next RunLoop our function will run
Ember.run.next #, 'doWhatever'
).observes('childViews.#each') # observe changes in the childViews array
afterRender: ->
console.log "afterRender", #get('childrenAreInDom')
Ember.run.next #, 'doWhatever'
# the function we want to run eventually after children are inDOM
doWhatever: ->
console.log "doWhatever"
# print out childrenAreInDom one more time.. it will be true
console.log "childrenAreInDom:", #get('childrenAreInDom')
# some views to insert
aView: Em.View.extend
template: Em.Handlebars.compile("A")
bView: Em.View.extend
template: Em.Handlebars.compile("B")
cView: Em.View.extend
template: Em.Handlebars.compile("C")
dView: Em.View.extend
template: Em.Handlebars.compile("D")
If you were to have {{view App.SomeContainerView}} in a template, in the console you'd see:
afterRender false
doWhatever
childrenAreInDom true
If you were to then programmatically add cView & dView via pushObjects after the container is already in the DOM, you'd see
observeChildren false
doWhatever
childrenAreInDom true
Even if this isn't exactly what you want hopefully it helps you get where you need to be without all that Mixin nonsense :D
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