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') })
Related
I want to perform an action, clearing parent element, after a collection has fetched his models but prior to the models rendering.
I've stumbled upon before and after render methods yet they are model specific, which will cause my parent element to clear before every model rendering.
I'm able of course to perform the action pre-fetching yet I want it to occur when fetch is done and before models are rendered.
I tried using reset and change events listening on the collection yet both resulted unwanted end result.
Reset event seamed to go in that direction yet the passed argument was the entire collection and not a single model from the collection, therefore using the add event callback wasn't possible due to difference in argument type (collection and not a model as required)
Any ideas how to invoke a callback when fetch a collection fetch is successful yet models are yet to be rendered?
The model contains the returned attributes while collection contains url for fetching and parse method to return argument wrapped object.
Below is the code I use to render the collection view, which is basically rendering each model's view within the collection.
Collection View
---------------
var FoosView = Backbone.View.extend({
el: '#plans',
events: {
//'click tr': 'rowClick'
},
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
_.bindAll(this, "render");
this.render();
},
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
...
});
The model view
--------
var FooView = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.options = options || {};
this.tpl = _.template(fooTpl);
},
render: function() {
var data = this.model.toJSON();
this.$el.html(this.tpl(data));
return this;
}
});
Thanks in advance.
OK, I think I understand your question and here is a proposed solution. You are now listening to the reset event on your collection and calling this.renderAll. this.renderAll will take the list of models from the collection and render them to the page, but only AFTER the list element has been emptied. Hope this helps :).
var FoosView = Backbone.View.extend({
el: '#plans',
collection: yourCollection, // A reference to the collection.
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
this.listenTo(this.collection, 'reset', this.renderAll);
},
renderAll: function() {
// Empty your list.
this.$el.empty();
var _views = []; // Create a list for you subviews
// Create your subviews with the models in this.collection.
this.collection.each(function(model) {
_views.push(new FooView({model: model});
});
// Now create a document fragment so as to not reflow the page for every subview.
var container = document.createDocumentFragment();
// Append your subviews to the container.
_.each(_views, function(subview) {
container.appendChild(subview.render().el);
});
// Append the container to your list.
this.$el.append(container);
},
// renderNew will only run on the collections 'add' event.
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
});
I am forced to assume a few things about you html, but I think the above code should be enough to get you up and running. Let me know if it works.
I'm not totally sure about what you are asking but have you tried:
MyCollection.fetch({
success: function(models,response) {
//do stuff here
}
});
Also you may be interested taking a look at http://backbonejs.org/#Model-parse
Hope it helps!
Edit: there is no direct link between fetching and rendering my bet is that you binded rendering to model change.
USE===============>>>> http://backbonejs.org/#Model-parse
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
I don't understand why the DOM is not updated in the collection view render:
class FastTodo.Views.TodoItemsIndex extends Backbone.View
template: JST['todo_items/index']
el: '#main'
render: ->
$(#el).html #form_view.render()
#collection.each #renderOne
renderOne: (item) ->
console.log(#)
console.log(#el)
$(#el).append "model data"
initialize: ->
#collection = new FastTodo.Collections.TodoItems()
#form_view = new FastTodo.Views.AddTodoItem collection: #collection
#collection.bind 'reset', =>
#render()
#collection.on 'add', (item) =>
#renderOne(item)
#collection.fetch()
The idea is that #main first get a view with add new form, and then the collection is appended to #main.
How would I do this?
The output of the view in the console looks like:
1) For #collection.each #renderOne to work correctly you need to bind your renderOne method to the view instance like this: renderOne: (item) => (notice the fat arrow), because otherwise it is invoked in the global context (that's why you see these Window objects in your console.
2) DOM element, not the view itself, should be inserted into DOM, so $(#el).html #form_view.render() should be written as #$el.html #form_view.render().el (the render method should return the view instance according to the backbone community convention).
Other looks fine and it should work this way.
You may wish to refer to some posting about context in js to deeper understand the subject (this one for example).
btw you can write less code for some things. i.e. this
#collection.bind 'reset', =>
#render()
#collection.on 'add', (item) =>
#renderOne(item)
can become this
#collection.on 'reset', #render
#collection.on 'add', #renderOne
but you should remember to bind your render method with the fat arrow in this case.
I am new to Backbone.js. I am using CoffeScript on a v0.9.2 app. The app works "fine" but the initialize() method of the views is not being called. Events are not being properly binded either. I am trying to figure out why this is not the case. I am using other (manual) ways to bind events to elements but that should not be the case.
The app is instantiated with this:
window.Site =
Models: {}
Collections: {}
Views: {}
Routers: {}
init: ->
new Site.Routers.MyRouter()
Backbone.history.start()
$(document).ready ->
Site.init()
The router:
class Site.Routers.MyRouter extends Backbone.Router
routes:
'': 'index'
initialize: ->
# some code here (this IS being called)
index: =>
# this is also being called since I am trying mysite.com/
view = new Site.Views.MyView()
$('#someId').html(view.render().el)
The view:
class Site.Views.MyView extends Backbone.View
template: JST['views/index']
events:
'click .someElement': 'someMethod'
inititalize: ->
console.log "hello" # NOT CALLED
_.bindAll #
#
render: =>
# draw stuff (this works)
#
The view gets drawn fine. Why is initialize not being called?
Thanks!
You have to spell initialize correctly =p
inititalize: -> # should be `initialize: ->`
console.log "hello" # NOT CALLED
For future readers, also check you don't have two initialize functions.
Backbone.View.extend({
initialize: function () {
// not called
},
// stuff
initialize: function () {
// overwrites previous
}
})
I don't write CoffeeScript, but the only place I see an instance of your view initailized is in the router:
index: =>
view = new Site.Views.MyView()
I suspect the router's index is not being called and as a result your view's initialize isn't being called. Extending a view doesn't create an instance of the view, rather it creates a customized definition of a view.
HTH.
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/