Backbone Marionette: CompositeView replacing items instead of appending them - backbone.js

I've got a Marionette CompositeView that I'm using to fill in a dropdown. The JSON response is clean when I call collection.fetch() from within the CompositeView, but instead of appending the new ItemViews, CompositeView seems to be replacing them in the DOM.
Here's my code (coffeescript):
class #PDCollectionItemView extends Backbone.Marionette.ItemView
el: 'li'
template: Handlebars.compile('{{ title }}')
class #PDCollectionsView extends Backbone.Marionette.CompositeView
id: 'pd_collections'
className: 'selection'
itemView: PDCollectionItemView
itemViewContainer: '.scroll ul'
template: HandlebarsTemplates['connections/collection_select'] #handlebars_assets gem
ui:
modalTrigger: '#pd_collection_selector'
modal : '#pd_selection_modal'
selectBtn : '#select_collection'
initialize: ->
#selectedCollection = undefined
Connectors.App.vent.on "connections:collectionStaged", #assignSelectedCollection
return #PDCollectionsView
And the parent layout where the fetch is called:
class #IndexLayout extends Backbone.Marionette.Layout
initialize: ->
#collections = new PDCollectionsCollection
#collectionsView = new PDCollectionsView
collection: #collections
onRender: ->
#collectionSelect.show #collectionsView
#collections.fetch
success: (collection, response, options) =>
Connectors.App.vent.trigger "connections:collectionsLoaded"
Connectors.App.vent.trigger "loadComplete"
error: (collection, response, options) =>
console.log response
I've tried manually appending the items with an appendHTML call, but I get the same behavior. I can log each itemView with a call to onAfterItemAdded on the #PDCollectionsView, and the item views are distinct; different cids, and the appropriate models.

I think the problem is in your use of Backbone's fetch operation. fetch "syncs" the collection with its state on the server. Without specifying any customization it will intelligently add new items, update changed items, and remove items no longer on the server. I'm guessing that if you examine the collection after you call fetch you'll see it's only got the items that are being rendered in the CompositeView.
You can modify fetch's behavior to sync to the server without removing anything by passing {remove: false}. This should yield the results you're looking for:
#collections.fetch
remove: false
success: (collection, response, options) =>
Connectors.App.vent.trigger "connections:collectionsLoaded"
Connectors.App.vent.trigger "loadComplete"
error: (collection, response, options) =>
console.log response

Related

Backbone Marionette: Rendering Collection in ItemView

I was unable to find any posts relevant to this error. I am attempting to render a Backbone Collection in a Marionette ItemView. The template is rendered, however, the data related to the collection is not rendered in the template. I am getting no errors or other indicators. For reasons I do not understand, using setTimeout() on App.mainRegion.show(overView). However, I know that that is not an acceptable solution. Could someone give me some insight on how to make an ItemView for a Collection properly render in this case? Here is my simplified code:
My Collection to be rendered:
About.Collection = Backbone.Collection.extend({
url: '/api/about',
idAttribute: '_id',
});
Involved View definitions:
About.ListView = Marionette.CollectionView.extend({
tagName: 'ul',
itemView: App.About.ListItemView,
});
About.OverView = Marionette.ItemView.extend({
tagName: 'div',
className: 'inner',
template: _.template('<h2>About Overview</h2><p><%= items %></p>'),
});
My relevant execution code:
var API = {
getAbouts: function() {
var abouts = new App.About.Collection();
abouts.fetch();
return abouts;
},
...
}
var abouts = API.getAbouts();
var aboutsListView = new App.About.ListView({collection: abouts }),
aboutsOverView = new App.About.OverView({collection: abouts});
// Correctly renders collection data
App.listRegion.show(aboutsListView);
// Does not render collection data
App.mainRegion.show(aboutsOverView);
// unless
setTimeout(function() {App.mainRegion.show(aboutsOverView)}, 50);
For those who are interested, I am using an ItemView with the eventual intent to display aggregate data of About.Collection. I will be happy to provide additional information, if needed.
It's an issue with the asynchronous nature of the fetch call on your collection. The data for the collection has not returned when you show the two views. If you update the execution part of your code something like the following (untested), you should be on the right tracks:
var API = {
getAbouts: function() {
// Just return the new collection here
return new App.About.Collection();
},
...
}
// Fetch the collection here and show views on success
var abouts = API.getAbouts().fetch({
success: function() {
var aboutsListView = new App.About.ListView({collection: abouts }),
aboutsOverView = new App.About.OverView({collection: abouts});
// Should render collection data now
App.listRegion.show(aboutsListView);
// Should render collection data now
App.mainRegion.show(aboutsOverView);
}
});
The abouts.fetch call is asynchronous, and a significant amount of time elapses before the collection receives data from the server. This is the order in which things are happening:
You call getAbouts, which itself calls abouts.fetch to make GET call to server for collection.
The listRegion.show and mainRegion.show calls are made, rendering the 2 views with the empty collection (the collection hasn't received a response from the server yet).
The GET call eventually returns, and the collection is populated with data.
Only the aboutsListView re-renders to show the data (see below for the reason).
The reason that only the aboutsListView re-renders is that the Marionette CollectionView automatically listens for the collection's reset event, which is fired when the collection's contents are replaced.
You can fix this by simply adding an initialize function to your OverView, so that view also re-renders in response to the same event:
// add to About.OverView:
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
}
That will take care of it.

BackboneJS + RequireJS allow view to act on a collection using global event dispatcher

Update: Problems solved, case closed.
I'm still having problems getting one part of my code to work.
My view now listens to the collection for updates, and what should happen is:
ListView listens to Results Collection
Results are synced
ListView creates an ItemView for each Result
ListView (ul) appends each ItemView (li)
Everything seems to work fine, up until the final step.
The function in ListView that is supposed to add the results to a list does not have access to the ListView's element.
I can create an ItemView, and retrieve it's element "<li>", but the ListView's "<ul>" cannot be referred to within the function.
Sample code bits from ListView:
el: $('.result-list'),
initialize: function() {
this.listenTo(this.collection, 'add', this.addOne);
},
addOne: function(result) {
view = new ItemView({ model: result });
this.$el.append(view.render().el);
},
In the above code, the variable view exists, as does it's element, but "this" doesn't refer to the ListView anymore.
Problem below solved
What I'm trying to accomplish is having a View module (search) able to trigger an event in a Collection (results).
When the Search View is submitted, it should pass the input field to the Collection's fetch method to retrieve results from the server. Currently, I can trigger a function from the View, but the function does not have access to any of the Collection's methods.
Previously, I had the View/Collection refer to each other directly by their variable names.
Since I have separated the code into modules, the View/Collection cannot access each other directly anymore.
Here is some of the code: (written in Coffeescript)
app.coffee - global_dispatcher is applied to Backbone
define [
'jquery'
'underscore'
'backbone'
'cs!router'
], ($, _, Backbone, Router) ->
# global_dispatcher added to all Backbone Collection, Model, View and Router classes
dispatcher = _.extend {}, Backbone.Events, cid: 'dispatcher'
_.each [ Backbone.Collection::, Backbone.Model::, Backbone.View::, Backbone.Router:: ], (proto) ->
_.extend proto, global_dispatcher: dispatcher
new Router()
router.coffee - This is where I'm having trouble. The function for 'getResults' is triggered, but the collection 'results' is not accessible from here.
define [
'backbone'
'cs!views/resuls/list'
'cs!views/results/search'
'cs!collections/results'
], (Backbone, ListView, SearchView, Results) ->
Backbone.Router.extend
routes:
# URL routes
'': 'index'
index: ->
results = new Results
new ListView { model: results }
new SearchView
#global_dispatcher.bind 'getResults', (data) ->
console.log results
search.coffee - View which triggers the event, it will successfully trigger the event and pass the correct arguments.
define [
'jquery'
'backbone'
], ($, Backbone) ->
Backbone.View.extend
events:
'submit #search-form': 'submit'
submit: (evt) ->
evt.preventDefault()
phrase = #.$('input').val()
#.$('input').val('')
args = name: phrase
#global_dispatcher.trigger 'getResults', args
If I'm understanding your problem correctly it's not hard to solve. Here's some dummy code to illustrate:
var Results = Backbone.Collection.extend();
var Search = Backbone.View.extend({
someEventHandler: function() {
// The next line accesses the collection from the view
this.collection.fetch();
}
});
var results = new Results();
var search = new Search({collection: results});
If you want your view to do something after the results come back, just bind an event handler on the collection:
var Search = Backbone.View.extend({
fetchResposne: function() { /* do something*/},
someEventHandler: function() {
// The next line accesses the collection from the view
this.collection.on('sync', this.fetchResponse);
this.collection.fetch();
}
});

Backbone.js not iterating through collection

Bit of an odd one...
I have the collection:
class Store.Collections.Product extends Backbone.Collection
url: '/api/products'
model: Store.Models.Product
With the view:
class Store.Views.Origin extends Backbone.View
initialize: ->
#collection = new Store.Collections.Product()
#collection.fetch()
#model.bind('change:formatted', #render, this);
#render()
events:
'change [name=origin]': 'setOrigin'
el: =>
#options.parent.$('.origin-input')[0]
template: JST["backbone/templates/shapes/product"]
render: ->
$this = $(this.el)
$this.html(#template(model: #model.toJSON(), errors: #model.errors))
console.log(#collection)
#collection.each(#appdenDropdown)
#delegateEvents()
this
appdenDropdown: (product) ->
console.log("append trigger")
#view = new Store.Views.Products(model: product)
#$('#history').append(view.render().el)
with the template:
<div id="history"></div>
The collection works... the
console.log(#collection)
shows the data! however
#collection.each(#appdenDropdown)
Does not do anything, doesn't error, or through anything. It just doesn't do anything. I am trying to extract the data out of the collection! But it wont...
It's because there's nothing in the collection yet.
#collection.fetch() in the initializer is an asynchronous method. You have to wait until the fetch is complete before you iterate through the collection items.
The fetch() function takes an optional success callback that is fired when the fetch is complete.
So you can update your initializer code to wait until the collection is fetched before calling render. Here is the code.
initialize: ->
#collection = new Store.Collections.Product()
#collection.fetch
success: =>
#render()
The problem as others have mentioned is that fetch is asynchronous, but the solution is simpler: jQuery's deferred object:
initialize: ->
#collection = new Store.Collections.Product()
#collection.deferred = #collection.fetch()
#model.bind('change:formatted', #render, this);
#collection.deferred.done ->
#render()
What's happening here is that when you call #collection.deferred.done, you're telling jQuery to wait until the collection is loaded before executing render on it, which is what you want. I think that should work.
A couple good references on deferred:
http://lostechies.com/derickbailey/2012/02/07/rewriting-my-guaranteed-callbacks-code-with-jquery-deferred/
http://www.erichynds.com/jquery/using-deferreds-in-jquery/

Correct Backbone.js collection event for when async data is retrieved

I want to render a view when a collection has been loaded asynchronously from the remote server. I have the following collection class
class BusinessUnits extends Backbone.Collection
model: BusinessUnit
parse: (units) ->
units
And then my view I was doing this:
load: (businessUnits) =>
#collection = businessUnits
#collection.fetch()
#render()
Obviously render() will be invoked before the fetch has been completed.
Is there a backbone.js event that is fired whenever the collection is fetched or would I be better firing my own?
This seems like a very common scenario. How do people handle this type of situation?
I think the "reset" event is what you are looking for.
"reset" (collection) — when the collection's entire contents have been replaced.
This will be triggered after the fetch completes.
load: (businessUnits) =>
#collection = businessUnits
#collection.bind 'reset', => #render()
#collection.fetch()

Representing existing HTML as Backbone.js data structures in CoffeeScript

I'm having a rough time wrapping my head around this.
I have an HTML list, and I want to use Backbone.js to handle events on those list items. Here's what I've got so far. This is a simplified scenario to help me better understand how to structure a larger application. For my example, I simply want to ingest an existing HTML list into the Backbone structure, and handle click events through the Backbone view.
I'm getting an error related to using #model in the view, but I'm fairly certain I'm misunderstanding things conceptually here.
CoffeeScript:
$ ->
class Item extends Backbone.Model
name: null
class ItemList extends Backbone.Collection
model: Item
class ItemView extends Backbone.View
tagName: 'li'
initialize: =>
#model.bind('change', this.render)
#model.view = this
events:
'click' : 'clicked'
clicked: ->
console.log 'clicked'
render: =>
this
class ItemListView extends Backbone.View
el: $('ul#test')
initialize: =>
$('li', #el).each(#addItem)
addItem: (item) ->
item = new ItemView({ el: item })
render: =>
this
Items = new ItemListView
HTML:
<ul id="test">
<li>Hi thar</li>
<li>Yeah</li>
<li>OK</li>
</ul>
Here's a jsfiddle I started earlier: http://jsfiddle.net/Saxx4/
I never really like CoffeeScript (Javascript is so nice, why replace it?), but it looks like there are a few issues here:
You're getting an error on #model because you never set it on the ItemView. This doesn't happen automatically - you have to either instantiate the view's model in initialize() or pass it into the constructor, e.g.:
addItem: (item) ->
model = new ItemView({
el: item,
model: new Item({
// assuming you might want the list item text
// in the model data
text: $(item).text()
})
})
You usually just want to specify a selector in el, not a jQuery object - otherwise the DOM might not be ready when you load your Backbone code: el: '#test'
You need to pass an options object to the ItemListView constructor, not just a single argument, no matter what you do in initialize():
class ItemListView extends Backbone.View
initialize: (opts) =>
opts.items.each(#addItem)
// ...
Items = new ItemListView({ items: $('ul#test li') })

Resources