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.
Related
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
I'm new to Backbone (and Marionette), and trying to write a pretty simple app using both. The app has a menu of "groups" on the left nav, and a list of "entries" on the main right div. Every time a Group menu item is clicked, I filter the entries with the group ID and show them, when hide all others.
Here is the Entry Item view (all scripts are in CoffeeScript btw):
class EntryItemView extends Backbone.Marionette.ItemView
tagName: 'tr'
template: _.template $('#entryItemTemplate').html()
render: ->
#$el.html #template(#model.toJSON())
show: ->
#$el.show()
hide: ->
#$el.hide()
Here is the Entry List view, extending Marionette's CollectionView:
class EntryListView extends Backbone.Marionette.CollectionView
itemView: EntryItemView
el: '#main tbody'
This is the AppRouter, pretty much straightforwad:
class AppRouter extends Backbone.Router
routes:
'group/:id' : 'showGroup'
router = new AppRouter()
router.on 'route:showGroup', (id) ->
_.each entryViews, (view) ->
if view.model.get('group_id') is parseInt(id)
view.show()
else
view.hide()
(The entryViews variable is a simple global array to store all EntryItemView instances).
With this approach, navigating the app to /group/:id indeed invokes the show() and hide() method of each EntryItemView object. The problem is, looks like the reference between this object and the actual HTML doesn't exist, so the actual element <tr> doesn't show or hide.
Can you guys point out what I'm doing wrong here? Thanks in advance.
Here are a couple pointers:
since your template is in the HTML, you just specify the jQuery selector with template: "#entryItemTemplate"
you can remove the render declaration, because Marionette does that on its own (i.e. you're implementing the default behavior)
unless you know what you're doing, you typically don't declare an el property in a collection view. Instead you declare a region (possibly within a layout), where you will call the show method to display a view instance
The reason your code probably doesn't work is that it looks like Backbone code with some Marionette stuff thrown in. Take a look at the free sample to my book on Marionette. It should get you started quickly with Marionette and will explain most of what you're trying to accomplish here.
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.
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.
How would I attach a constructor for every instance of a model using prototype? I have been trying to modify the constructor method when an instance of Backbone.Model is created. BuT I do not want to attach a constructor method to each model I create individually. Adding a prototype of initialize works; the only issue is if initialize is passed with the model it overrides it via the underscore extend method.
I tried this
Backbone.Model.prototype.constructor = function(attributes, opts) {
console.log('hi');
Backbone.Model.prototype.constructor.call(this, attributes);
};
only problem is, it doesn't work outside of the extend; it will only get called if constructor is inside the object you extend the model with when creating a new instance.
I just need to run some code when an instance of each model is created. I want to perform some auto binding work with node's socket.io framework.
It's not recommended to mess with the internals of backbone inheritance and use the provided methods and flow to achieve model manipulation on initialization. To do that - in a similar fashion as with collections, views or routers you have an initialization method that you should override that takes the role of a constructor.
If you need to alter all your models just create a new base class that you will later use as a super class for all your models and then if you need to overwrite the initialization method of subclasses remeber to call the super class initializer using the built in backbone class __super__ property.
initialize: function() {
this.constructor.__super__.initialize.call(this);
// whatever
},
Altering the core of backbone will never do good for you in the long run as it my prevent you from being able to easily update to new version if they would introduce conflicting changes.
I've been there and done that - and after doing it I'd recommend not following this path to anybody!