Chaplin no context after template rerender - backbone.js

I'm trying to have a view which can have interchangable templates. So clicking on one of the checkbox rerenders the view. this in fact is happening . but after the view has rerenderd and show the new template correctly im loosing the context and all click bound to this view don't work anymore. http://pastebin.com/bFJ5Yuer
View = require 'views/base/view'
template = require 'views/templates/list_view_a'
module.exports = class OfferListView extends View
autoRender: true
container: "[data-role='content']"
containerMethod: 'html'
initialize: ->
super
#template = template
#views
#delegate 'change', '#list_view_a', #change_list_view
#delegate 'change', '#list_view_b', #change_list_view
#delegate 'change', '#list_view_c', #change_list_view
#delegate 'click', #click_ev
change_list_view: (event) =>
console.log('change')
#template = require 'views/templates/' + event.target.id
#render()
click_ev: =>
console.log('click')
getTemplateData: =>
#collection.toJSON()
Any pointers ?

Yeah, the call to #render in change_list_view is wiping out the element that the events are bound to in the initialize method.
If you must re-render the view (like if you are changing the template) then you will just have to add a call to #delegateEvents below the #render call in change_list_view.
UPDATE
If you wanted to switch to a subview method, you could probably get rid of that second call to render. Something like this:
# snipped all the outer require js stuff
class InnerViewBase extends Chaplin.View # Or whatever your base view is
autoRender: true
container: "#innerViewContainer"
containerMethod: "html"
initialize: (templateName) ->
#template = require templateName
super
class ListViewA extends InnerViewBase
initialize: ->
super "views/templates/list_view_a"
# Do any event handlers in here, instead of the outer view
# #delegate 'click', #click_ev
class ListViewB extends InnerViewBase
initialize: ->
super "views/templates/list_view_b"
class ListViewC extends InnerViewBase
initialize: ->
super "views/templates/list_view_b"
class OfferListView extends View
autoRender: true
container: "[data-role='content']"
containerMethod: 'html'
initialize: ->
super
#template = template
#views
# Consider changing these three id subscriptions to a class
#delegate 'change', '#list_view_a', #change_list_view
#delegate 'change', '#list_view_b', #change_list_view
#delegate 'change', '#list_view_c', #change_list_view
#delegate 'click', #click_ev
afterRender: ->
# Add a default ListView here if you want
# #subview "ListView", new ListViewA
change_list_view: (event) =>
console.log('change')
# Make a hacky looking map to the subview constructors
viewNames =
"list_view_a": ListViewA
"list_view_b": ListViewB
"list_view_c": ListViewC
#subview "ListView", new viewNames[event.target.id]
click_ev: =>
console.log('click')
getTemplateData: =>
#collection.toJSON()

The call to #delegateEvents didn't do the trick. But thanks for pointing me in a right direction.
This was helpful as well. Backbone: event lost in re-render
mind my wrong indentation
View = require 'views/base/view'
template = require 'views/templates/list_view_a'
module.exports = class OfferListView extends View
autoRender: true
container: "[data-role='content']"
containerMethod: 'html'
template: template
initialize: (options) ->
super
#views
change_list_view: (event) =>
#template = require 'views/templates/' + event.target.id
#render()
initEvents: =>
#delegate 'change', '#list_view_a', #change_list_view
#delegate 'change', '#list_view_b', #change_list_view
#delegate 'change', '#list_view_c', #change_list_view
#delegate 'click', #click_ev
click_ev: ->
console.log('click')
render: =>
super
afterRender: =>
super
#initEvents()

Related

Backbone.js modify newly fetched collection before reset events occur

I'm trying to do the following:
Fetch data from the server
Add a zero-based index to the models before the views are notified
Finally run have the 'render' events fire for the views
I was trying to do this by using a success callback in the collection
View Before
initialize: () ->
#collection.on 'reset', #render, this
render: () -> ...render code...
Collection Before
search: () ->
#fetch {
success: #fetch_success
}
fetch_success: () ->
for i in [0...collection.models.length]
collection.models[i].set('go_index', i)
Doing things this way was causing the views to fire their render events before the collection was updated by the success callback. The solution I came up with was to have the views listen to a fetched event, then have the collection fire that after it successfully modifies the collection:
View After
initialize: () ->
#collection.on 'fetched', #render, this
render: () -> ...render code...
Collection After
initialize: () ->
#on 'reset', #add_index_and_notify, this
add_index_and_notify: () ->
for i in [0...#models.length]
#models[i].set('go_index', i)
#trigger('fetched')
This works fine, I'm just wondering if this is the most elegant way to accomplish this or if there is a built-in way that I'm missing.
UPDATE 3/15
I've come up with a cleaner solution that doesn't require the view to do any of the dirty work and I don't have to create a custom event. The trick is to listen to the sync event (which fires after reset)
View Final
initialize: () ->
#collection.on 'sync', #render, this
render: () -> ...render code...
Collection Final
initialize: () ->
#on 'reset', #add_index, this
add_index: () ->
for i in [0...#models.length]
#models[i].set('go_index', i)
Hopefully this pattern can help somebody searching in the future.
I've already posted the solution in the original question, but I figured I'd formally post as an answer:
The cleaner solution doesn't require the view to do any of the dirty work and doesn't require a custom event. The trick is to listen to the sync event (which fires after reset)
View Final
initialize: () ->
#collection.on 'sync', #render, this
render: () -> ...render code...
Collection Final
initialize: () ->
#on 'reset', #add_index, this
add_index: () ->
for i in [0...#models.length]
#models[i].set('go_index', i)
Hopefully this pattern can help somebody searching in the future.
Your view should get both the model and it's index separately from the collection because the index isn't really part of the model record itself. Try having your view use collection.each to loop over the models as the callback function there will get model, index, collection as parameters. Remember a view can pass more than just a single model to its template.
class CollectionView1 extends Backbone.View
render: =>
$el = #$el
$el.empty()
#collection.each (model, index) ->
modelView = new ModelView1 {model, index}
$el.append modelView.render().el
return this
Why don't you listen to the add event of the collection..
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
this.listenTo(this.collection , 'add' , this.add_index_and_notify);
this.index = 0;
},
add_index_and_notify: function(model){
model.set({go_index : this.index++}, {silent : true});
// Render the model here
},
render: function(){
this.$el.empty().append(Your template);
this.index= 0;
_.each(this.collection.models, function(model){
this.add_index_and_notify(model);
}
}

How to render item views for a collection including an add new view?

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.

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/

Backbone.js not updating HTML in View

I have the view:
class FoursquareSearch.Views.SearchNew extends Backbone.View
tagName: 'sidebar'
template: JST["templates/search/new_search"]
#events:
# 'submit': 'create'
initialize: ->
#render
render: ->
console.log('hello')
$this = $('#sidebar')
$this.html('<p>All new content. <em>You bet!</em></p>')
$this
with this Router
class FoursquareSearch.Routers.Maps extends Backbone.Router
routes:
'': 'index'
index: ->
FoursquareSearch.Views.maps = new FoursquareSearch.Views.Maps()
#model = new FoursquareSearch.Models.Map()
#originForm = new Traveltime.Views.ShapesOriginForm(model: model, map: Traveltime.Views.map)
newSearch = new FoursquareSearch.Views.SearchNew(model: model, map: FoursquareSearch.Views.maps)
And this HTML
<div id="top"></div>
<div id="sidebar">
</div>
Init code:
window.FoursquareSearch =
Models: {}
Collections: {}
Views: {}
Routers: {}
init: ->
new FoursquareSearch.Routers.Maps()
Backbone.history.start()
$(document).ready ->
FoursquareSearch.init()
I can see the console.log message however the HTML class / id does not get updated!
If I run:
$this = $('#sidebar')
$this.html('<p>All new content. <em>You bet!</em></p>')
in console I can see the HTML change on the page, it just seems that Backbone.js does not want to update the view for me?
Update #render to be #render() in your initialize method. You are simply returning the method, but never calling it.

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