Skip to content

Instantly share code, notes, and snippets.

@szimek
Created June 15, 2011 10:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save szimek/1026843 to your computer and use it in GitHub Desktop.
Save szimek/1026843 to your computer and use it in GitHub Desktop.
Extensions for Backbone.
// TODO:
// - TemplateView
// - change the way templates are compiled (store compiled version inside view "class"?)
//
// - CollectionView
// - cleanup layout rendering if it depends on collection (remove collection container from DOM, rerender the layout and attach it again?)
// - store model views together with models (?)
//
// - ModelView
// - extend Model#toJSON to convert nested objects as well (http://rcos.rpi.edu/projects/concert/commit/got-requests-displaying-properly-again/)
// Provides computed and dependent attributes
Backbone.Spine = Backbone.Model.extend({
get: function (attr) {
var value = Backbone.Model.prototype.get.apply(this, arguments),
computed,
attrs;
if (value) return value;
if (_(this._computed).indexOf(attr) !== -1) {
computed = this[attr]();
attrs = {};
attrs[attr] = computed;
this.set(attrs, {silent: true});
}
return computed;
},
computed: function (fnNames) {
this._computed = (this._computed || []).concat(fnNames);
},
dependent: function (dependencies) {
var self = this;
_(dependencies).each(function (deps, attr) {
_(deps).each(function (dependency) {
self.bind('change:' + dependency, function () {
var prev = self.attributes[attr],
curr = self[attr](),
attrs = {};
attrs[attr] = curr;
if (!_.isEqual(prev, curr)) self.set(attrs);
});
});
});
},
toJSON: function (attrs) {
attrs = attrs || [];
attrs = attrs.length ? attrs : this._computed;
// Reload all attributes
_(attrs).each(_.bind(function (attr) {
this.get(attr);
}, this));
return Backbone.Model.prototype.toJSON.apply(this);
}
});
// Reduce collection using provided conditions
// e.g. FriendList.where({age: 30, gender: 'male'})
// Returns object of the same "class", so it's possible to chain methods
Backbone.Collection.prototype.where = function(conditions) {
return new this.constructor(_(conditions).reduce(function(memo, value, key) {
memo = _(memo).filter(function(model) {
return model.get(key) === value;
});
return memo;
}, this.models));
};
// Handlebars helper for Backbone.CollectionView layout
Handlebars.registerHelper('yield', function () {
return new Handlebars.SafeString('<div class="yield"></div>');
});
// Cache for compiled templates
Backbone.View.CompiledTemplates = {};
// Template view
// Uses Handlebars templates and provides some generic helper methods
Backbone.TemplateView = Backbone.View.extend({
templateName: null,
templateSource: null,
context: {},
initialize: function (options) {
this._setup(options);
this._setTemplateSource();
this._precompileTemplate();
},
_setup: function (options) {
if (options) {
this.templateSource = options.templateSource || this.templateSource;
this.templateName = options.templateName || this.templateName;
this.context = options.context || this.context;
}
_.bindAll(this, 'render');
},
// Sets template source to the compiled version,
// if only template name is provided and such template is already compiled
_setTemplateSource: function () {
if (this.templateSource) return;
if (this.templateName && Backbone.View.CompiledTemplates[this.templateName]) {
this.templateSource = Backbone.View.CompiledTemplates[this.templateName];
}
},
_precompileTemplate: function () {
if (this.templateName && !Backbone.View.CompiledTemplates[this.templateName]) {
Backbone.View.CompiledTemplates[this.templateName] = this.compileTemplate();
}
},
// Returns precompiled template (if name is provided ) or compiles it on every call
template: function (context) {
var template;
if (this.templateName) {
template = Backbone.View.CompiledTemplates[this.templateName];
} else {
template = this.compileTemplate();
}
return template(context || {});
},
compileTemplate: function () {
return Handlebars.compile(this.templateSource);
},
render: function () {
var el = $(this.el),
html = this.template(this.context);
el.html(html);
el.triggerHandler("render");
return this;
},
reset: function () {
this.empty();
return this;
},
empty: function () {
$(this.el).empty();
return this;
},
show: function () {
$(this.el).show();
return this;
},
hide: function () {
$(this.el).hide();
return this;
},
find: function (query) {
return this.$(query);
},
appendTo: function (parent) {
$(this.el).appendTo(parent);
return this;
}
});
// Model view
// Automtically rerenders itself when associated model changes
Backbone.ModelView = Backbone.TemplateView.extend({
attrs: [],
initialize: function (options) {
if (!this.model) throw new Error('model option is missing');
Backbone.TemplateView.prototype.initialize.call(this, options);
$(this.el).attr('data-model-cid', this.model.cid);
if (this.attrs.length > 0) {
_.each(this.attrs, _.bind(function (attr) {
this.model.bind('change:' + attr, this.render);
}, this));
} else {
this.model.bind('change', this.render);
}
},
render: function () {
this.context = this.model.toJSON(this.attrs);
Backbone.TemplateView.prototype.render.call(this);
return this;
}
});
// Collection view
// Automatically updates itself when items are added/removed from the associated collection
Backbone.CollectionView = Backbone.TemplateView.extend({
collection: null,
container: null,
useLayout: true,
insertItemsWith: 'appendTo',
initialize: function (options) {
if (!this.collection) throw new Error('collection option is missing');
Backbone.TemplateView.prototype.initialize.call(this, options);
// Update view context whenever underlying collection is modified
// This has to go before other render bindings,
// so that the context will be already updated when rendering the layout
_.bindAll(this, 'updateContext');
this.collection.bind('add', this.updateContext);
this.collection.bind('remove', this.updateContext);
this.collection.bind('reset', this.updateContext);
this.collection.bind('change', this.updateContext);
this.updateContext();
// Bindings for rendering stuff
_.bindAll(this, 'addItem', 'removeItem');
this.collection.bind('add', this.addItem);
this.collection.bind('remove', this.removeItem);
this.collection.bind('reset', this.render);
},
render: function () {
this.empty();
if (this.useLayout) {
this._renderLayout();
this.container = this.find('.yield');
} else {
this.container = $(this.el);
}
this._renderCollection();
return this;
},
// Adding item does not update layout
addItem: function (model) {
var view = new this.modelView({model: model}).render();
$(view.el)[this.insertItemsWith](this.container);
},
// Removing item does not update layout
removeItem: function (model) {
var view = this.container.find('[data-model-cid="' + model.cid + '"]');
if (view.length) view.remove();
},
// Override this to update layout context before rendering
// if it depends on associated collection
// e.g. if collection size is displayed in the layout
updateContext: function () {},
_renderLayout: function () {
var html = this.template(this.context);
$(this.el).html(html);
},
_renderCollection: function () {
this.collection.each(_.bind(this.addItem, this));
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment