Backbone "add" and "update" events confusion - javascript

From the Backbone.js (1.2.3) documentation:
add collection.add(models, [options])
Add a model (or an array of models) to the collection, firing an "add"
event for each model, and an "update" event afterwards.
In my code I want to add a new model (visually a card with an input) to the collection and then force focus on the input of created card. I'm not sure if it's the correct way, but I basically listen to the event fired when model is added to collection and trigger another event which facilitates focusing from the view of created model:
# ItemView
initialize: ->
App.vent.on "focus:field", =>
$("div.card:last").addClass("edit")
#$el.find("input:first").focus()
When I listen to the update event, it works as expected: :last card (the new model) is selected and input is focused.
But when I listen to the add event, initialize function fires on the penultimate model, instead of the new model. The new model is still created, but the edit class and focus is forced on the one before.
Why is that happens?
I would use an update event for this purpose, but unfortunately for me model.destroy method also fires an update event, so it results in ruined UI, and if I pass model.destroy with silient:true bad things happen overall. Is there a workaround?
Relevant code:
# CompositeView
class List.Models extends App.Views.CompositeView
template: "path/to/template"
childViewContainer: "div.destination"
childView: List.Model
events:
"click #add-model": "addModel"
initialize: ->
#listenTo #collection, "update", ->
App.vent.trigger "focus:field"
addModel: (e) ->
#$el.find("#add-model").prop "disabled", true
model = App.request "new:model:entity"
#collection.add(model)
# ItemView
class List.Model extends App.Views.ItemView
template: "path/to/template"
initialize: ->
App.vent.on "focus:field", =>
$("div.card:last").addClass("edit")
#$el.find("input:first").focus()
Edit:
Apparently the difference between add and update events is that the add event is fired immediately after #collection.add(model) is called, but before new model is inserted in the DOM, resulting in $("div.card:last") selector pointing to the penultimate view. I'm not sure however, maybe more experienced people can clarify whether this is true or not. I've come to this conclusion after adding timeout to the execution of the App.vent:
initialize: ->
delay = (ms, func) -> setTimeout func, ms
# Triggered via add event
App.vent.on "focus:field", =>
delay 100, =>
$("div.card:last").addClass("edit")
$("input:first").focus()

Related

Use Backbone.View events method through a mixin

I'm using Backbone with Coffeescript in an app. Now the example that I'll use is made as trivial as possible. I have a header in my app that all the views share it. This header has a logout link with #logout index. Now what I'm trying to do is to put the events method and the method that the event will use it when it is triggered in a mixin object and extend the prototype of my Backbone View. Here is the code:
Mixin.Header =
events: ->
'click #logout': 'logout'
logout: (e)->
e.preventDefault()
$.ajax(
type: 'DELETE'
url: '/logout'
).then(
->
document.location = ''
)
class View extends Backbone.View
initialize: (options)->
_.extend(View.prototype, Mixin.Header)
I've been looking through the Backbone source code and I simply can't find the problem why this is not working. The events get delegated to the View through the delegateEvents() method and when the View is initialized the initialize method is being called first.
From the fine manual:
events view.events or view.events()
[...]
Backbone will automatically attach the event listeners at instantiation time, right before invoking initialize.
You're trying to add events in initialize but the events are bound before initialize is called.
You could call delegateEvents yourself to rebind the events after you've updated the prototype:
initialize: (options)->
_.extend(View.prototype, Mixin.Header)
#delegateEvents() # <----------
but the structure would be a little weird because you'd be modifying the class inside an instance.
I think you'd be better off modifying the class before you have any instances:
class View extends Backbone.View
_.extend(View.prototype, Mixin.Header)
or you could use the CoffeeScript shortcut for prototype:
class View extends Backbone.View
_.extend(View::, Mixin.Header)
# -----------^^
or even:
class View extends Backbone.View
_.extend(#::, Mixin.Header)
You will still run into problems if View has its own events as _.extend blindly overwrites properties rather than merging them. You'd need something smarter than _.extend to properly handle this and it would need to be able to figure out how to merge events functions and objects.

collection add event firing on view initialisation

I have this code in my backbone application,
createNewProject: function(e) {
e.preventDefault();
this.createNewProject = new app.CreateNewProject({
collection: this.dropdownListProjectCollection
});
}
app.CreateNewProject = Backbone.View.extend({
el: '.modal',
template: _.template( $("#tpl-create-new-project").html() ),
events: {
'click input[type="submit"]' : 'saveBasicProject'
},
initialize: function() {
this.collection.on("add", console.log("added"));
this.$el.find("h4").text("Create new project");
this.$el.find(".modal-body").html( this.template() );
this.render();
return this;
},
I am trying to detect when a model is added to the collection, and then eventually fire a method to render that new record. However I am having problem in that the this.collection.on('add'...) seems to run when ever the view is initialised and again when a model is saved, how can I only run the method when the model is saved, and not the view being initialised.
I recommend you do this.listenTo(this.collection, ... in your view because when the view is removed it will remove the event and avoid memory leaks.
The add event will get fired on every .set on the collection. If you passed models on collection initialize this will happen ctor -> reset -> add -> set to add those models so you will get reset and add events.
What doesn't make any sense is that your collection should have initialize BEFORE you initialized the view so I don't know why you are getting that event unless you are subsequently adding more models.
To listen to a save event you can use these events on the collection:
request - when the request starts
sync - when the request has happened successfully
error - something went wrong
PS this is a shortcut for this.$el.find -> this.$('.someselector')

Click event handler only working once in Backbone.js

I'm new to Backbone and am using it with jQuery in Rails. In my view's render method, I use delegateEvents to bind an event handler to the "click" event of a button with id btn-go. This button is itself rendered by the view in question.
Clicking the button stores the form's values in an array and causes the view to re-render. This button is itself rendered by the view in question. This works the first time I click the button, but nothing happens the second time, even though the view does correctly re-render its template.
class MyApp.Views.ChainsNew extends Backbone.View
# #template is defined by a conditional inside render()
render: (step_number) ->
window.model = #model
#step_number = step_number
#template = if #step_number is 1 then JST['chains/new'] else JST['chains/step']
$(#el).html(#template())
#delegateEvents({
'click #btn-go': 'add_step'
})
#
add_step: ->
#divide array into arrays of steps before and after step being edited
steps = #model.get('steps')
array1 = steps.slice(0, #step_number - 1)
array2 = steps.slice(#step_number)
array1.push(#$el.find('textarea').val())
newArray = array2.concat(array1)
#model.set({
steps: newArray
})
The view's render method is called by the router. As you can see in the code below, the router is listening to the change event on the model. This causes it to update the URL, which in turn triggers the router's step method to be called, and it's within that method that the view's render method is finally called.
class MyApp.Routers.Chains extends Backbone.Router
routes:
'chains/new(/)': 'new'
'chains/new/step/:step_number(/)': 'step'
initialize: ->
# Model
#model = new MyApp.Models.Chain()
#listenTo(#model, "change", ->
#goto_step_number #model.get('steps').length + 1
)
# Views
#view_new = new MyApp.Views.ChainsNew({
model: #model
})
step: (url_step_number) ->
# Before rendering the view, make sure URL & number of steps in model are correctly related
url_step_number = parseInt url_step_number
steps_entered = #model.get('steps').length
if url_step_number > steps_entered + 1
#goto_step_number steps_entered + 1
else
$('#main-container').html(#view_new.render(url_step_number).el)
new: ->
#goto_step_number 1
goto_step_number: (step_number) ->
#.navigate('chains/new/step/' + step_number, trigger: true)
Why doesn't anything happen the second time I click the button? I'm guessing that the event handler hasn't been correctly bound to the button, but I have no idea why.
Your problem is right here:
$('#main-container').html(#view_new.render(url_step_number).el)
From the fine manual:
.html( htmlString )
[...]
When .html() is used to set an element's content, any content that was in that element is completely replaced by the new content. Additionally, jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.
Note the removes other constructs such as data and event handlers part. The sequence of events goes like this:
You call render.
render calls delegateEvents to attached a jQuery event delegator to the view's el.
You call $x.html(view.el) but view.el is already there so jQuery detaches all the event bindings (including the one you just added in 2), clears out $x, and then puts view.el back into $x.
But when view.el is put back on the page, the events are already gone. This is roughly equivalent to what you're doing:
# In the view...
add_step: ->
re_render(#step_number + 1)
and
v = new YourView
$('#main-container').append(v.render(1).el)
re_render = (step_number) ->
$('#main-container').html(v.render(step_number).el)
Demo: http://jsfiddle.net/ambiguous/4rJyB/
You need to stop calling .html all the time. Once the view is on the page, you can simply tell it to re-render itself and that's all you need to do. So, if the view has been rendered once to get its el into #main-container, you just need to:
#view_new.render(url_step_number)
and that's it. Then you can remove the #delegateEvents call from render and use the usual events map on the view:
class MyApp.Views.ChainsNew extends Backbone.View
events:
'click #btn-go': 'add_step'
render: (step_number) ->
window.model = #model
#step_number = step_number
#template = if #step_number is 1 then JST['chains/new'] else JST['chains/step']
#$el.html(#template())
#
#...

Backbonejs Events for all collection items or Events per ItemView

What approach would be more efficient?
I have a Backbone.Collection so i Create a Backbone.View to render this collection. The CollectionView render method:
render: ->
container = document.createDocumentFragment()
#collection.each (item) ->
view = new ItemView(item)
container.appendChild view.el
view.render()
$(el).append container
I can use the events in two forms.
1.- Set the events object in CollectionView, so i need to declare the action of select an item in the CollectionView and "rescue" the model that i selected.
CollectionView extends Backbone.View
events:
'click #itemView', 'onSelectItem'
onSelectItem: ->
##Get the model
##Show ItemDetailView
2.- Set the events object per itemView, so the select method don't need to retrieve the model.
ItemView extends Backbone.View
events:
'click #div','onSelect'
onSelect: ->
#Show ItemDetailView
Which of this options are better?
EDITED: I create a JSperf snippet http://jsperf.com/backbone-events-on-collectionview-or-per-itemview
JSperf show us that the ItemView approach is more faster, but is this the only metric of importance?
The second approach is much better. Performance aside, the code is much more straightforward. In 6 months, if you had to return to this code, would you think the event would be handled in the ItemView, or the CollectionView? It is a click event on the ItemView, so that's where I would go to look for how it is handled.
Is there a good reason to handle the event in the collection? If you needed the handle the event from the CollectionView, you could delegate the event to the collection. A little redirection, but, to me, this is far clearer. For example:
# The collection view
initialize: ->
#listenTo #, 'selected', #itemSelected
render: ->
container = document.createDocumentFragment()
#collection.each (item) ->
view = new ItemView(item, parent: #)
container.appendChild view.el
view.render()
$(el).append container
itemSelected: (model)->
# Do whatever you need to here, like
# show the ItemDetailView in the container
# The ItemView
ItemView extends Backbone.View
events:
'click #div','onSelect'
onSelect: ->
#options.parent.trigger('selected', #model, #)
#Show ItemDetailView
If I understand you correctly, option 2 seems the most sane. You will be referring to this.model as opposed to digging through the collection to find the model associated with the clicked view.

Backbone context issue with coffeescript

I have a simple calendar that re-renders whenever the date changes. After onClick the date is set triggering the change:date event and the render method is called.
However, the context is off as the html is not being replaced.
I don't know why this is so because I am using => to preserve the context and when I console.log #$el it always shows me the same class ie. WidgetView
It works when I do $(elementName).html but not with #$el.html. Ideas?
class WidgetView extends sandbox.mvc.View
className: 'sidebar-group'
events:
"click a" : "onClick"
template: sandbox.template.compile tmpl
initialize: (options) ->
#date = new DateModel {rid:1000}
#listenTo #date, "change:date", #render
onClick: (e) ->
e.preventDefault()
# Get value
value = $(e.currentTarget).text()
# Set date model
#date.set {date:value}
# Emit model change event
sandbox.emit "model.date.change", #date
return false
render: (model) =>
data = CalendarResource()
#$el.html #template {calendar: data}
#
I think the problem is pretty simple. Look at listenTo closely if you didn't read it.
object.listenTo(other, event, callback)
Unlike bind
object.on(event, callback, [context])
If you see listenTo doesn't set the context of your callback in any shape or form.
Try just doing
#listenTo #date, "change:date", _.bind(#render, this);
Bleh I'm not a coffee scripter but you get the point. Force the context in the constructor to always be the instance.
Problem
The issue for this particular problem was that I was using the JQuery Page Slide plugin which was copying over the element to another node.
As a result, I was updating the reference node and the plugin was not keeping the new node in sync.
Solution
Since there was no refresh method for the plugin, I will have to copy over the elements and rebind all events.

Categories

Resources