Subclassing Backbone.View - backbone.js

I have several views that have common code I'd like to abstract into a custom Backbone.View class. Is there any best practices for doing this?
is a good pattern to do something like this? :
// Base Grid view
var GridView = Backbone.View.extend({
initialize : function(){
//common view init code ..
//do the plug in overrides
if (options.addHandler)
this.addHandler = options.addHandler;
if (options.events)
//?? extend default events or override?
this.events = $.extend(this.events, options.events);
},
addHandler : function() {
//defaulthandler this code can be overridden
});
});
// in another object create some views from the GridView base
....
var overrides = { events:"xxx yyy", el: ulElement addHandler: myAddFunction }
var UserList = GridView.extend(overrides)
var userList = new UserList(users, options);
....
var coursesOverrides : {addHandler: ...}
var coursesOptions: {el: courseElement, ...}
var CourseList = GridView.extend(coursesOverrides)
var courseList= new CourseList (courses, coursesOptions)
// along the same lines maybe there's an abstraction for toolbar views
var ClassToolbarView = ToolbarBase.extend(toolOverrides)
var classtoolbar = new ClassToolbarView(actions, toolbaropts)
Any pointers to good examples of extending a View for refactoring common view code is appreciated.

First, I don't see the options being passed in your initializer(), so that's a bug.
Secondly, the .extend() method is inherited:
var GridView = Backbone.View.extend({ ... })
var GridViewWithNewFunctionsAndEvents = GridView.extend({ ... })
And you can replace or extend GridView's functionality, and call new GridViewWithNewFunctionsAndEvents() and get the extra functionality in a new object you need, just like you extend the Backbone stock View class.
If you need to extend the initializer, you can do this to call the initializer on the superclass:
var GridViewWithNewFunctionsAndEvents = GridView.extend({
initializer: function(options) {
GridView.prototype.initializer.call(this, options);
/* Your stuff goes here */
}
});

Related

Backbone model level class method

I'm getting some model names dynamically and I want to access a method from each of the models. Is there a way that I can declare and access model level class method or constant in BB like Namespace.models["MyModel"].classMethod()/MY_CONSTANT?
Here's how implement instance and class methods in Backbone.
var instance_properties = {
myInstanceMethod: function() {console.log('instance method');}
};
var class_properties = {
myClassMethod: function() {console.log('class method');}
};
var Model = Backbone.Model.extend(instance_properties ,class_properties);
Model.myClassMethod(); // class method
var model = new Model();
model.myInstanceMethod(); // instance method

Initializing nested collections with Backbone

I am trying to nest a Collection View into a Model View.
In order to do so, I used Backbone's Marionnette Composite View and followed that tutorial
At the end he initializes the nested collection view like this:
MyApp.addInitializer(function(options){
var heroes = new Heroes(options.heroes);
// each hero's villains must be a backbone collection
// we initialize them here
heroes.each(function(hero){
var villains = hero.get('villains');
var villainCollection = new Villains(villains);
hero.set('villains', villainCollection);
});
// edited for brevity
});
How would you go doing the same without using the addInitalizer from Marionette?
In my project I am fectching data from the server. And when I try doing something like:
App.candidatures = new App.Collections.Candidatures;
App.candidatures.fetch({reset: true}).done(function() {
App.candidatures.each(function(candidature) {
var contacts = candidature.get('contacts');
var contactCollection = new App.Collections.Contacts(contacts);
candidature.set('contacts', contactCollection);
});
new App.Views.App({collection: App.candidatures});
});
I get an "indefined options" coming from the collection:
App.Collections.Contacts = Backbone.Collection.extend({
model: App.Models.Contact,
initialize:function(models, options) {
this.candidature = options.candidature;
},
url:function() {
return this.candidature.url() + "/contacts";
}
)};
That's because when you're creating the contactCollection, you're not providing a candidatures collections in an options object. You do need to modify your contact collection initialization code to something like:
initialize:function(models, options) {
this.candidature = options && options.candidature;
}
That way the candidature attribute will be set to the provided value (and if not provided, it will be undefined).
Then, you still need to provide the info when you're instanciating the collection:
App.candidatures.each(function(candidature) {
var contacts = candidature.get('contacts');
var contactCollection = new App.Collections.Contacts(contacts, {
candidature: candidature
});
candidature.set('contacts', contactCollection);
});
P.S.: I hope you found my blog post useful!

Find a Backbone.js View if you know the Model?

Given a page that uses Backbone.js to have a Collection tied to a View (RowsView, creates a <ul>) which creates sub Views (RowView, creates <li>) for each Model in the collection, I've got an issue setting up inline editing for those models in the collection.
I created an edit() method on the RowView view that replaces the li contents with a text box, and if the user presses tab while in that text box, I'd like to trigger the edit() method of the next View in the list.
I can get the model of the next model in the collection:
// within a RowView 'keydown' event handler
var myIndex = this.model.collection.indexOf(this.model);
var nextModel = this.model.collection.at(myIndex+1);
But the question is, how to find the View that is attached to that Model. The parent RowsView View doesn't keep a reference to all the children Views; it's render() method is just:
this.$el.html(''); // Clear
this.model.each(function (model) {
this.$el.append(new RowView({ model:model} ).render().el);
}, this);
Do I need to rewrite it to keep a separate array of pointers to all the RowViews it has under it? Or is there a clever way to find the View that's got a known Model attached to it?
Here's a jsFiddle of the whole problem: http://jsfiddle.net/midnightlightning/G4NeJ/
It is not elegant to store a reference to the View in your model, however you could link a View with a Model with events, do this:
// within a RowView 'keydown' event handler
var myIndex = this.model.collection.indexOf(this.model);
var nextModel = this.model.collection.at(myIndex+1);
nextModel.trigger('prepareEdit');
In RowView listen to the event prepareEdit and in that listener call edit(), something like this:
this.model.on('prepareEdit', this.edit);
I'd say that your RowsView should keep track of its component RowViews. The individual RowViews really are parts of the RowsView and it makes sense that a view should keep track of its parts.
So, your RowsView would have a render method sort of like this:
render: function() {
this.child_views = this.collection.map(function(m) {
var v = new RowView({ model: m });
this.$el.append(v.render().el);
return v;
}, this);
return this;
}
Then you just need a way to convert a Tab to an index in this.child_views.
One way is to use events, Backbone views have Backbone.Events mixed in so views can trigger events on themselves and other things can listen to those events. In your RowView you could have this:
events: {
'keydown input': 'tab_next'
},
tab_next: function(e) {
if(e.keyCode != 9)
return true;
this.trigger('tab-next', this);
return false;
}
and your RowsView would v.on('tab-next', this.edit_next); in the this.collection.map and you could have an edit_next sort like this:
edit_next: function(v) {
var i = this.collection.indexOf(v.model) + 1;
if(i >= this.collection.length)
i = 0;
this.child_views[i].enter_edit_mode(); // This method enables the <input>
}
Demo: http://jsfiddle.net/ambiguous/WeCRW/
A variant on this would be to add a reference to the RowsView to the RowViews and then tab_next could directly call this.parent_view.edit_next().
Another option is to put the keydown handler inside RowsView. This adds a bit of coupling between the RowView and RowsView but that's probably not a big problem in this case but it is a bit uglier than the event solution:
var RowsView = Backbone.View.extend({
//...
events: {
'keydown input': 'tab_next'
},
render: function() {
this.child_views = this.collection.map(function(m, i) {
var v = new RowView({ model: m });
this.$el.append(v.render().el);
v.$el.data('model-index', i); // You could look at the siblings instead...
return v;
}, this);
return this;
},
tab_next: function(e) {
if(e.keyCode != 9)
return true;
var i = $(e.target).closest('li').data('model-index') + 1;
if(i >= this.collection.length)
i = 0;
this.child_views[i].enter_edit_mode();
return false;
}
});
Demo: http://jsfiddle.net/ambiguous/ZnxZv/

cached view losing its events

I render a collection of models, which is associated with a collectionView where when rendered each element in the collection has its own 'itemview' which is rendered.
When a collection is sorted and the listView re-rendered based on the new order, I had been creating a totally new view for each item, and as I was not clearing up any previous instances of views associated with that model, I believe zombies being left around.
So initially rendering my collection I would do...
render : function() {
$(this.el).empty();
var content = this.template.tmpl({});
$(this.el).html(content);
sortingView.el ='#sorting-container';
var els = [];
_.each(this.collection.models, function(model){
var view = new TB_BB.RequestItemView({model : model});
els.push(view.render().el);
});
$('#request-list').append(els);
sortingView.render();
return this;
}
So whenever the render function was called a second/third etc time, I had not cleared up the TB_BB.RequestItemView (hence the zombies)
To overcome this I tried to add some simple caching in the collections view, so that instead of creating a new itemview if it had already been created use that instead. My code
initialize : function(){
_.bindAll(this,"render");
this.collection.bind("add", this.render);
this.collection.bind("remove", this.render);
this.template = $("#request-list-template");
this.views = {};
},
events : {
"change #sort" : "changesort",
"click #add-offer" : "addoffer",
"click #alert-button" : "addalert"
},
render : function() {
$(this.el).empty();
outerthis = this;
var content = this.template.tmpl({});
$(this.el).html(content);
sortingView.el ='#sorting-container';
var els = [];
_.each(this.collection.models, function(model){
var view;
if(outerthis.views[model.get('id')]) {
view = outerthis.views[model.get('id')];
} else {
view = new TB_BB.RequestItemView({model : model});
outerthis.views[model.get('id')] = view;
}
});
$('#request-list').append(els);
sortingView.render();
return this;
}
So this works in so much as the views are re-used - however what I have noticed is that if I use a cached view (e.g. the collection has been sorted and the render function finds a cached view) that all of the events on the sub itemview stop working? why is that?
Also could anyone suggest a better way of doing this?
You can use delegateEvents ( http://documentcloud.github.com/backbone/#View-delegateEvents ) to bind the events again.
As OlliM mentioned the reason is because the events are bound to the dom element, but instead of rebinding the element you can also just detach them instead of removing them (detach keeps the event bindings http://api.jquery.com/detach/)
something like
var $sortContainer = $('#sorting-container');
$('li', $sortContainer).detach();
And then just reattach the element
$cnt.append(view.el);
I would also consider using a document fragment while rebuilding/sorting the list and then attaching appending that instead.

How to add more than one model/collection to the Backbone View?

When I add collection to the view like this:
var View = new MyCollectionView({ collection: new MyCollection() });
everything is okey. I can use this collection in initialize method (for binding events, for example). But how can I add another one?
I can't do this way:
var View = new MyCollectionView({
collection: new MyCollection(),
secondCollection: new MySecondCollection()
});
From the fine manual:
constructor / initialize new View([options])
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events.
So, if you create a view like this:
new View({collection: c})
then Backbone will automatically assign c to the view's this.collection. But if you create the view like this:
new View({collection: c, secondCollection: c2})
then inside the View's constructor:
initialize: function(options) {
// this.collection will be 'c' from above
// options.secondCollection will be 'c2'
}
So you can do this:
var View = new MyCollectionView({
collection: new MyCollection(),
secondCollection: new MySecondCollection()
});
provided that your MyCollectionView has an initialize method that knows to pull the secondCollection out of its options argument.
Open your JavaScript console and have a look at what this does:
var V = Backbone.View.extend({
initialize: function(options) {
var c1 = options.collection;
var c2 = options.secondCollection;
console.log(this.collection);
console.log(c1);
console.log(c2);
}
});
var view = new V({collection: 1, secondCollection: 2});
Demo: http://jsfiddle.net/ambiguous/XyeSD/

Resources