Created
June 15, 2011 10:20
-
-
Save szimek/1026843 to your computer and use it in GitHub Desktop.
Extensions for Backbone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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