capsule.js | |
---|---|
(function(){ | |
Module Setup | |
All public Capsule classes and modules will be attached to the | var Capsule,
server = false,
Backbone,
_,
uuid;
if (typeof exports !== 'undefined') {
Backbone = require('backbone');
_ = require('underscore')._;
uuid = require('node-uuid');
Capsule = exports;
server = true;
} else {
Backbone = this.Backbone;
_ = this._;
Capsule = this.Capsule || (this.Capsule = {});
}
|
Our model hash, this is where all instantiated models are stored by id | Capsule.models = {};
|
Capsule.Model | |
Extend the Backbone model with Capsule functionality | Capsule.Model = Backbone.Model.extend({ |
registerRegister ourselves. This means generate a uuid if we're on the server
and listen for changes to ID (which you shouldn't really change) this just handles the
case where our root model is initted on the client, before it has any data. Once it gets
its We also bind change so to our | register: function () {
var self = this;
if (server && !this.get('id')) {
this.set({id: uuid()});
}
if (this.id && !Capsule.models[this.id]) Capsule.models[this.id] = this;
this.bind('change:id', function (model) {
if (!Capsule.models[this.id]) Capsule.models[model.id] = self;
});
this.bind('change', _(this.publishChange).bind(this));
},
|
addChildCollectionWe use this to build our nested model structure. This will ensure
that | addChildCollection: function (label, constructor) {
this[label] = new constructor();
this[label].bind('publish', _(this.publishProxy).bind(this));
this[label].bind('remove', _(this.publishRemove).bind(this));
this[label].bind('add', _(this.publishAdd).bind(this));
this[label].bind('move', _(this.publishMove).bind(this));
this[label].parent = this;
},
|
addChildModelAdds a child model and ensures that various publish events will be proxied up and that we store a reference to the parent. | addChildModel: function (label, constructor) {
this[label] = new constructor();
this[label].bind('publish', _(this.publishProxy).bind(this));
this[label].parent = this;
},
|
modelGetterConvenience method for retrieving any model, no matter where, by id. | modelGetter: function (id) {
return Capsule.models[id];
},
|
togglechecks and toggles boolean properties. | toggle: function (attrName) {
var change = {};
change[attrName] = !(this.get(attrName));
this.set(change);
},
|
toggleServerchecks and toggles boolean properties on the server. | toggleServer: function (attrName) {
var change = {};
change[attrName] = !(this.get(attrName));
this.setServer(change);
},
|
deleteServerSends delete event for | deleteServer: function () {
socket.send({
event: 'delete',
id: this.id
});
},
|
callServerMethodSend a method call event. To trigger a model method on the server (if allowed). | callServerMethod: function (method) {
socket.send({
event: 'method',
id: this.id,
method: method
});
},
|
toTemplateThis is a replacement for simply sending Backbone's | toTemplate: function () {
var result = this.toJSON(),
self = this;
result.htmlId = this.cid;
if (this.templateHelpers) {
_.each(this.templateHelpers, function (val) {
result[val] = _.bind(self[val], self);
});
}
return result;
},
|
xportOur serializer. Builds and returns a simple object ready to be JSON stringified
By default it recurses through child models/collections unless you pass it | xport: function (opt) {
var result = {},
settings = _({
recurse: true
}).extend(opt || {});
function process(targetObj, source) {
targetObj.attrs = source.toJSON();
_.each(source, function (value, key) {
if (settings.recurse) {
if (key !== 'collection' && source[key] instanceof Backbone.Collection) {
targetObj.collections = targetObj.collections || {};
targetObj.collections[key] = {};
targetObj.collections[key].models = [];
targetObj.collections[key].id = source[key].id || null;
_.each(source[key].models, function (value, index) {
process(targetObj.collections[key].models[index] = {}, value);
});
} else if (key !== 'parent' && source[key] instanceof Backbone.Model) {
targetObj.models = targetObj.models || {};
process(targetObj.models[key] = {}, value);
}
}
});
}
process(result, this);
return result;
},
|
mportOur deserializer. Reinflates the model structure with data created by the | mport: function (data, silent) {
function process(targetObj, data) {
targetObj.set(data.attrs, {silent: silent});
if (data.collections) {
_.each(data.collections, function (collection, name) {
targetObj[name].id = collection.id;
Capsule.models[collection.id] = targetObj[name];
_.each(collection.models, function (modelData, index) {
var nextObject = targetObj[name].get(modelData.attrs.id) || targetObj[name]._add({}, {silent: silent});
process(nextObject, modelData);
});
});
}
if (data.models) {
_.each(data.models, function (modelData, name) {
process(targetObj[name], modelData);
});
}
}
process(this, data);
return this;
},
|
publishProxyPrimarily an internal method that just passes publish events up through the model structure so those events can bubble. | publishProxy: function (data) {
this.trigger('publish', data);
},
|
publishChangeCreates a publish event of type | publishChange: function (model) {
if (model instanceof Backbone.Model) {
this.trigger('publish', {
event: 'change',
id: model.id,
data: model.attributes
});
} else {
console.error('event was not a model', e);
}
},
|
publishAddConvert | publishAdd: function (model, collection) {
this.trigger('publish', {
event: 'add',
data: model.xport(),
collection: collection.id
});
},
|
publishRemoveConvert | publishRemove: function (model, collection) {
this.trigger('publish', {
event: 'remove',
id: model.id
});
},
|
publishMovePublishes a | publishMove: function (collection, id, newPosition) {
this.trigger('publish', {
event: 'move',
collection: collection.id,
id: id,
newPosition: newPosition
});
},
|
ensureRequiredConvenience for making sure a model has certain required attributes. | ensureRequired: function () {
var self = this;
if (this.required) {
_.each(this.required, function (type, key) {
self.checkType(type, self.get(key), key);
});
}
},
|
validateConvenient default for Backbone's | validate: function (attr) {
var self = this;
_.each(attr, function (value, key) {
if (self.required && self.required.hasOwnProperty(key)) {
var type = self.required[key];
self.checkType(type, value, key);
}
});
},
|
checkTypeOur simple typechecker, that just uses underscore's type checkers. | checkType: function (type, value, key) {
var validator;
type = type.toLowerCase();
switch (type) {
case 'string': validator = _.isString; break;
case 'boolean': validator = _.isBoolean; break;
case 'date': validator = _.isDate; break;
case 'array': validator = _.isArray; break;
case 'number': validator = _.isNumber; break;
}
if (!validator(value)) {
throw "The '" + key + "' property of a '" + this.type + "' must be a '" + type + "'. You gave me '" + value + "'.";
}
},
|
setServerOur server version of the normal | setServer: function(attrs) {
socket.send({
event: 'set',
id: this.id,
change: attrs
});
},
|
unsetServerUnsets a given property | unsetServer: function(property) {
socket.send({
event: 'unset',
id: this.id,
property: property
});
}
});
|
Capsule.Collection | |
Extend Backbone collection with Capsule functionality | Capsule.Collection = Backbone.Collection.extend({
|
registerGenerates an | register: function () {
if (server) this.id = uuid();
if (this.id && !Capsule.models[this.id]) Capsule.models[this.id] = this;
},
|
addServerThe server version of backbone's | addServer: function (data) {
socket.send({
event: 'add',
id: this.id,
data: data
});
},
|
moveServerSend the | moveServer: function (id, newPosition) {
socket.send({
event: 'move',
collection: this.id,
id: id,
newPosition: newPosition
});
},
|
registerRadioPropertiesA convenience for creating | registerRadioProperties: function () {
var collection = this;
if (this.radioProperties) {
_.each(this.radioProperties, function (property) {
collection.bind('change:' + property, function (changedModel) {
if (changedModel.get(property)) {
collection.each(function (model) {
var tempObj = {};
if (model.get(property) && model.cid !== changedModel.cid) {
tempObj[property] = false;
model.set(tempObj);
}
});
}
});
collection.bind('add', function (addedModel) {
var tempObj = {};
if (collection.select(function (model) {
return model.get(property);
}).length > 1) {
tempObj[property] = false;
addedModel.set(tempObj);
}
});
});
}
},
|
filterByPropertyShortcut for returning an array of models in the collection that have a certain | filterByProperty: function (prop, value) {
return this.filter(function (model) {
return model.get(prop) === value;
});
},
|
findByPropertyShortcut for finding first model in the collection with a certain | findByProperty: function (prop, value) {
return this.find(function (model) {
return model.get(prop) === value;
});
},
|
setAllConvenience for setting an attribute on all items in collection | setAll: function (obj) {
this.each(function (model) {
model.set(obj);
});
return this;
},
|
moveItemCalculate position and move to new position if not in right spot. | moveItem: function (id, newPosition) {
var model = this.get(id),
currPosition = _(this.models).indexOf(model);
if (currPosition !== newPosition) {
this.models.splice(currPosition, 1);
this.models.splice(newPosition, 0, model);
this.trigger('move', this, id, newPosition);
}
}
});
|
Capsule.ViewAdding some conveniences to the Backbone view. | Capsule.View = Backbone.View.extend({ |
handleBindingsThis makes it simple to bind model attributes to the view.
To use it, add a | handleBindings: function () {
var self = this;
if (this.contentBindings) {
_.each(this.contentBindings, function (selector, key) {
self.model.bind('change:' + key, function () {
var el = (selector.length > 0) ? self.$(selector) : $(self.el);
el.html(self.model.get(key));
});
});
}
if (this.classBindings) {
_.each(this.classBindings, function (selector, key) {
self.model.bind('change:' + key, function () {
var newValue = self.model.get(key),
el = (selector.length > 0) ? self.$(selector) : $(self.el);
if (_.isBoolean(newValue)) {
if (newValue) {
el.addClass(key);
} else {
el.removeClass(key);
}
} else {
el.removeClass(self.model.previous(key)).addClass(newValue);
}
});
});
}
return this;
},
|
desistThis is method we used to remove/unbind/destroy the view.
By default we fade it out this seemed like a reasonable default for realtime apps.
So things to just magically disappear and to give some visual indication that
it's going away. You can also pass an options hash | desist: function (opts) {
opts || (opts = {});
if (this.interval) {
clearInterval(this.interval);
delete this.interval;
}
if (opts.quick) {
$(this.el).unbind().remove();
} else {
$(this.el).animate({
height: 0,
opacity: 0
},
function () {
$(this).unbind().remove();
}
);
}
},
|
addReferencesThis is a shortcut for adding reference to specific elements within your view for
access later. This is avoids excessive DOM queries and gives makes it easier to update
your view if your template changes. You could argue whether this is worth doing or not,
but I like it.
In your
Then later you can access elements by reference like so: | addReferences: function (hash) {
for (var item in hash) {
this['$' + item] = $(hash[item], this.el);
}
},
|
autoSetInputsConvenience for automagically setting all input values on the server
as-you-type. This is letter-by-letter syncing. You have to be careful with this
but it's very cool for some use-cases.
To use, just add a
Then if you call | autoSetInputs: function () {
this.$(':input').bind('input', _(this.genericKeyUp).bind(this));
},
|
genericKeyUpThis is handy if you want to add any sort of as-you-type syncing this is obviously traffic heavy, use wth caution. | genericKeyUp: function (e) {
var res = {},
target = $(e.target),
type;
if (e.which === 13 && e.target.tagName.toLowerCase() === 'input') target.blur();
res[type = target.data('type')] = target.val();
this.model.setServer(res);
},
|
basicRenderAll the usual stuff when I render a view. It assumes that the view has a | basicRender: function (opts) {
opts || (opts = {});
_.defaults(opts, {
templateKey: this.template
});
var newEl = ich[opts.templateKey](this.model.toTemplate());
$(this.el).replaceWith(newEl);
this.el = newEl;
this.handleBindings();
this.delegateEvents();
},
|
subViewRenderThis is handy for views within collections when you use | subViewRender: function (opts) {
opts || (opts = {});
_.defaults(opts , {
placement: 'append',
templateKey: this.template
});
var newEl = ich[opts.templateKey](this.model.toTemplate())[0];
if (!this.el.parentNode) {
$(this.containerEl)[opts.placement](newEl);
} else {
$(this.el).replaceWith(newEl);
}
this.el = newEl;
this.handleBindings();
this.delegateEvents();
},
|
Binding Utilities (thanks to @natevw)bindomaticYou send it your model, an event (or array of events) and options. It will bind the event (or events) and set the proper context for the handler so you don't have to bind the handler to the instance. It also adds the function to an array of functions to unbind if the view is destroyed. | bindomatic: function (model, ev, handler, options) {
var boundHandler = _(handler).bind(this),
evs = (ev instanceof Array) ? ev : [ev];
_(evs).each(function (ev) {
model.bind(ev, boundHandler);
});
if (options && options.trigger) boundHandler();
(this.unbindomatic_list = this.unbindomatic_list || []).push(function () {
_(evs).each(function (ev) {
model.unbind(ev, boundHandler);
});
});
},
|
unbindomaticUnbinds all the handlers in the unbindomatic list from the model. | unbindomatic: function () {
_(this.unbindomatic_list || []).each(function (unbind) {
unbind();
});
},
|
collectomaticShorthand for rendering collections and their invividual views.
Just pass it the collection, and the view to use for the items in the
collection. (anything in the | collectomatic: function (collection, ViewClass, options) {
var views = {};
this.bindomatic(collection, 'add', function (model) {
views[model.cid] = new ViewClass(_({model: model}).extend(options));
});
this.bindomatic(collection, 'remove', function (model) {
views[model.cid].desist();
delete views[model.cid];
});
this.bindomatic(collection, 'refresh', function () {
_(views).each(function (view) {
view.desist();
});
views = {};
collection.each(function (model) {
views[model.cid] = new ViewClass(_({model: model}).extend(options));
});
}, {trigger: true});
this.bindomatic(collection, 'move', function () {
_(views).each(function (view) {
view.desist({quick: true});
});
views = {};
collection.each(function (model) {
views[model.cid] = new ViewClass(_({model: model}).extend(options));
});
});
}
});
})();
|