Backbone context issue with coffeescript - javascript

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.

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.

Backbone "add" and "update" events confusion

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()

Backbone model objects.... where are they stored so that the DOM can interact with them?

I just realized that I have no idea what the heck I am doing when it comes to backbone. I came to this realization when trying to figure out a cogent strategy for removing the view's event listeners on the model. Then I asked "well, where is the model anyways now that the view has been rendered to the DOM?" and then I asked "how is this model object that I created inside a function body, and is therefore out of scope now that I have rendered the view to the DOM, maintaining state?"
AHHHHHHHH!!!!!!!!!
Ex.
View Constructor
Timeclock.Views.JobNewView = Backbone.View.extend({
template: JST['jobs/_form'],
events:{
'blur #job_form :input':'assignValue'
},
initialize: function(options){
this.listenTo(this.model, 'failed-request', this.failedLocationRequest);
this.listenTo(this.model, 'updated-location', this.updatedLocation);
this.listenTo(this.model, 'sync', this.renderJobView);
this.listenTo(this.model, 'invalid', this.displayModelErrors);
this.listenTo($(window), 'hashchange', this.clearListeners);
},
render: function(){
this.$el.html(this.template({attributes: this.model.attributes}));
this.$el.find('#address_fields').listenForAutoFill();
return this;
},
assignValue: function(e){
var $field = $(e.currentTarget)
var attr_name = $field.attr('name');
var value = $field.val();
this.model.set(attr_name, value);
}...
});
Function rendering view to the DOM
renderCollaboratingView: function(e){
var job = this.model;
var $row = $(e.currentTarget);
job.set({customer_id: $row.data('id')});
var model_view = new this.ViewConstructor({model: job});
$container.html(model_view.render().el);
}
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
I understand that backbone views are just a wrapper to declaratively write DOM listeners but how are DOM events acting on the underlying model object in the example above? As soon as the renderCollaboratingView() function has exited how is the model that I passed to the view still being interacted with?
I can think of two ways:
1) The model object is bound to the DOM through a jquery object. All the event listeners that I declare in my view all know where the underlying model object lives on the jquery object(the 'model' attribute?).
2) Backbone is creating some object namespace that the view knows about where it stores models and collections that back the DOM. I have a feeling it's #1 but who knows.
Once again, I got here because I was trying to understand why I need to remove the listeners on the model that I passed into view in the first place. If backbone views are really just jquery objects then aren't jquery listeners removed from DOM elements when the element backing the jquery object is removed from the DOM? Do I only need to remove the listeners if I am going to not destroy the view entirely and save it for later use?
Any help that can be given would be greatly apprecaited. Having an existential crisis.
Thanks
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
Backbone Models and Views are simply Javascript objects that live in-memory in the scope of the page (like any other Javascript). If you were to do ...
var name = 'Peter';
var person = new Backbone.Model({ name: 'Peter' });
var view = new Backbone.View({ model: person } );
... then name, person, and view are all just objects in memory. They have no relation to jQuery; they have no relation to the DOM. The View happens to be able to create DOM elements if you implement render(), but even then those elements don't ever have to ever be attached to the page's live DOM at all.
... how are DOM events acting on the underlying model object in the example above? As soon as the renderCollaboratingView() function has exited how is the model that I passed to the view still being interacted with?
Based on the code you've shown, the model isn't being interacted with directly. Your events hash ...
events:{
'blur #job_form :input':'assignValue'
},
... does say that any time a blur event happens in the job_form element, it will call a method on the view called assignValue. That method may interact with the model (it probably does, right?), but DOM events don't directly cause interaction with the model at all.
If backbone views are really just jquery objects then aren't jquery listeners removed from DOM elements when the element backing the jquery object is removed from the DOM?
Backbone's listeners are wholly different than jQuery listeners. They listen for Backbone-centric events. See here for the list of built-in events that Backbone components fire. A View's events hash is a nice convention that is used to listen for DOM events; it's basically a nice wrapper around the jQuery concept of event delegation.
Do I only need to remove the listeners if I am going to not destroy the view entirely and save it for later use?
If you don't remove listeners, they will continue to run whenever the related event happens, regardless of whether the listening component is changing the page. Suppose you had a Backbone.View that did something like this:
var MyView = Backbone.View.extend({
// ...
events: {
// Don't do this!
'click': '_onClick'
},
// ...
_onClick: function() {
this.$el.append('Clicked!');
}
});
Any time any click DOM event happens on the page, this view will append the text Clicked! to its internal DOM element. When the view is attached to the page's DOM, Clicked! would appear on every click. When the view was removed from the DOM, the function would still run on every click... but since the View's internal root element wasn't attached to anything the function would have no effect.
It's a type of a memory leak, as any instance of MyView will ever be cleared up by the garbage collector. But the particularly nefarious side effect is it also uses CPU time to do something that is completely worthless. Now imagine if the event listener did anything of consequence. Performance of the page will suffer.
JavaScript has garbage collection. Objects do not get destroyed then they go out of scope. An Object X get garbage collected by the runtime system, when it sees that nobody is having a reference (or is pointing) to X.
A Backbone View is also an object. An object can store reference to another object.
In your renderCollaboratingView, you wrote :
var model_view = new this.ViewConstructor({model: job});
this model_view is your view's object. You passed your job which is your model you got from :
renderCollaboratingView: function(e){
var job = this.model;
....
}
You can look at this line in backbone annotated code : BackBone View Options. (I would suggest to look at the link after you have read the answer)
The line is :
var viewOptions = ['model', 'collection', 'el', 'id',
'attributes', 'className', 'tagName', 'events'];
and then Backbone View is defined as : BackBone View
It is :
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
options || (options = {});
_.extend(this, _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
};
Look at line :
_.extend(this, _.pick(options, viewOptions));
and your code :
var model_view = new this.ViewConstructor({model: job});
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
If you merge the dots : You are passing a model to your view. You can also pass other like 'collection', 'el', 'id', ... in viewOptions.
They get pick from your passed object {model: job} and extended in the view object.
This is how your view object has reference to the model that it was given.
Once again, I got here because I was trying to understand why I need to remove the listeners on the model that I passed into view in the first place.
As i said, just removing a view-object from DOM is not going to destroy it. You would have to remove all references of view-object that other objects (here model) have.
When you said :
initialize: function(options){
this.listenTo(this.model, 'failed-request', this.failedLocationRequest);
....
in your view. You told the model to call your view-object's failedLocationRequest on model's event failed-request. This is possible only when your model's object would store reference to view's object. So, your view is not destroyed.
view-object(s) not in dom would continue receiving such events from models and all other places where they registered (except dom) and would do things in the background, that you just never wanted. Definitely not what you wanted..
simple advice, call remove on your view. BackBone View remove
and read stopListening

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.

Categories

Resources