smackbone
Version:
an object oriented model framework
593 lines (540 loc) • 18.3 kB
JavaScript
(function() {
var Smackbone, _, root,
slice = [].slice,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
if (typeof exports !== "undefined" && exports !== null) {
Smackbone = exports;
_ = require('underscore');
Smackbone.$ = {
done: function(func) {
return func({});
},
ajax: function(options) {
return this;
}
};
} else {
root = this;
_ = root._;
Smackbone = root.Smackbone = {};
Smackbone.$ = root.$;
}
Smackbone.Event = (function() {
function Event() {}
Event.prototype.trigger = function() {
var allEvents, args, events, name, ref, ref1;
name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
events = (ref = this._events) != null ? ref[name] : void 0;
if (events != null) {
this._triggerEvents.apply(this, [events].concat(slice.call(args)));
}
allEvents = (ref1 = this._events) != null ? ref1.all : void 0;
if (allEvents != null) {
this._triggerEvents.apply(this, [allEvents, name].concat(slice.call(args)));
}
return this;
};
Event.prototype.on = function(names, callback) {
var events, j, len, name, nameArray;
if (this._events == null) {
this._events = {};
}
if (!_.isFunction(callback)) {
throw new Error('Must have a valid function callback');
}
if (/\s/g.test(name)) {
throw new Error('Illegal event name');
}
nameArray = names.split(' ');
for (j = 0, len = nameArray.length; j < len; j++) {
name = nameArray[j];
events = this._events[name] || (this._events[name] = []);
events.push({
callback: callback,
self: this
});
}
return this;
};
Event.prototype.off = function(name, callback) {
var event, events, j, k, key, l, len, len1, len2, names, newEvents, ref, ref1;
if (this._events == null) {
this._events = {};
}
if (callback == null) {
this._events = {};
return this;
}
ref = name.split(' ');
for (j = 0, len = ref.length; j < len; j++) {
name = ref[j];
events = (ref1 = this._events[name]) != null ? ref1 : [];
names = name ? [name] : (function() {
var k, len1, ref2, results;
ref2 = this._events;
results = [];
for (k = 0, len1 = ref2.length; k < len1; k++) {
key = ref2[k];
results.push(key);
}
return results;
}).call(this);
for (k = 0, len1 = names.length; k < len1; k++) {
name = names[k];
newEvents = [];
this._events[name] = newEvents;
for (l = 0, len2 = events.length; l < len2; l++) {
event = events[l];
if (callback !== event.callback) {
newEvents.push(event);
}
}
if (newEvents.length === 0) {
delete this._events[name];
}
}
}
return this;
};
Event.prototype._triggerEvents = function() {
var args, event, events, j, len, results;
events = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
results = [];
for (j = 0, len = events.length; j < len; j++) {
event = events[j];
results.push(event.callback.apply(event, args));
}
return results;
};
return Event;
})();
Smackbone.Model = (function(superClass) {
extend(Model, superClass);
function Model(attributes, options) {
var key, modelClass, ref;
this._properties = {};
this.cid = _.uniqueId('m');
this.length = 0;
this.idAttribute = 'id';
this.changed = {};
this._indexToModel = [];
if (attributes != null) {
this.set(attributes);
}
if (this.models != null) {
ref = this.models;
for (key in ref) {
modelClass = ref[key];
if (!this.contains(key)) {
this.set(key, new modelClass({}));
}
}
}
if (typeof this.initialize === "function") {
this.initialize(attributes);
}
}
Model.prototype.toJSON = function() {
var key, properties;
properties = _.clone(this._properties);
for (key in this.transients) {
delete properties[key];
}
return properties;
};
Model.prototype.isNew = function() {
return this[this.idAttribute] == null;
};
Model.prototype.clone = function() {
return new this.constructor(this._properties);
};
Model.prototype._createModelFromName = function(name, value, backupClass) {
var modelClass, ref, ref1, ref2, ref3, ref4;
modelClass = (ref = (ref1 = (ref2 = (ref3 = this.modelClasses) != null ? ref3[value[this.classField]] : void 0) != null ? ref2 : (ref4 = this.models) != null ? ref4[name] : void 0) != null ? ref1 : this.model) != null ? ref : backupClass;
if (modelClass != null) {
return new modelClass(value);
} else {
return value;
}
};
Model.prototype.move = function(currentId, nextId) {
var o;
o = this.get(currentId);
if (o == null) {
throw new Error("Id '" + currentId + "' didn't exist.");
}
this.unset(currentId);
return this.set(nextId, o);
};
Model.prototype.set = function(key, value, options) {
var addedAttributes, attributes, changeName, changedPropertyNames, current, existingObject, j, k, l, len, len1, len2, n, name, oldId, previous, ref, ref1, ref2, removedAttributes, results, v;
if (key == null) {
throw new Error('can not set with undefined');
}
if (typeof key === 'object') {
attributes = key;
options = value;
value = void 0;
} else {
(attributes = {})[key] = value;
}
if (attributes[this.idAttribute] != null) {
oldId = this[this.idAttribute] || this.cid;
this[this.idAttribute] = attributes[this.idAttribute];
if ((ref = this._parent) != null) {
ref.move(oldId, this[this.idAttribute]);
}
}
this._previousProperties = _.clone(this._properties);
current = this._properties;
previous = this._previousProperties;
changedPropertyNames = [];
addedAttributes = [];
removedAttributes = [];
this.changed = {};
ref1 = this._properties;
for (name in ref1) {
value = ref1[name];
if (attributes[name] == null) {
removedAttributes.push(name);
}
}
for (name in attributes) {
value = attributes[name];
if (!_.isEqual(current[name], value)) {
changedPropertyNames.push(name);
}
if (!_.isEqual(previous[name], value)) {
this.changed[name] = value;
}
if ((((ref2 = current[name]) != null ? ref2.set : void 0) != null) && !(value instanceof Smackbone.Model) && (value != null)) {
existingObject = current[name];
existingObject.set(value, options);
} else {
if (!(value instanceof Smackbone.Model)) {
value = this._createModelFromName(name, value);
}
current[name] = value;
this.length = _.keys(current).length;
if (value instanceof Smackbone.Model && (value._parent == null)) {
value._parent = this;
if (value[this.idAttribute] == null) {
value[this.idAttribute] = name;
}
}
addedAttributes.push(value);
}
}
this._indexToModel = (function() {
var ref3, results;
ref3 = this._properties;
results = [];
for (n in ref3) {
v = ref3[n];
results.push(v);
}
return results;
}).call(this);
if (!(options != null ? options.silent : void 0)) {
for (j = 0, len = addedAttributes.length; j < len; j++) {
value = addedAttributes[j];
this.trigger('add', value, this, options);
}
for (k = 0, len1 = changedPropertyNames.length; k < len1; k++) {
changeName = changedPropertyNames[k];
this.trigger("change:" + changeName, current[changeName], this, options);
}
if (changedPropertyNames.length > 0) {
this.trigger('change', this, options);
}
}
if (options != null ? options.triggerRemove : void 0) {
results = [];
for (l = 0, len2 = removedAttributes.length; l < len2; l++) {
value = removedAttributes[l];
results.push(this.unset(value));
}
return results;
}
};
Model.prototype.contains = function(key) {
return this.get(key) != null;
};
Model.prototype.add = function(object) {
return this.set(object);
};
Model.prototype.remove = function(object) {
return this.unset(object);
};
Model.prototype.each = function(func) {
var key, ref, results, value;
ref = this._properties;
results = [];
for (key in ref) {
value = ref[key];
results.push(func(value, key));
}
return results;
};
Model.prototype.get = function(key) {
var id, j, len, model, parts, ref, ref1;
if (key == null) {
throw new Error('Must have a valid object for get()');
}
if (typeof key === 'string') {
if (key[key.length - 1] === '/') {
key = key.slice(0, -1);
}
parts = key.split('/');
model = this;
for (j = 0, len = parts.length; j < len; j++) {
id = parts[j];
if (model == null) {
throw new Error("Couldn't lookup '" + id + "' in '" + key + "'");
}
if (model instanceof Smackbone.Model) {
model = model._properties[id];
} else {
model = model[id];
}
}
return model;
} else {
return this._properties[(ref = (ref1 = key[this.idAttribute]) != null ? ref1 : key.cid) != null ? ref : key];
}
};
Model.prototype.at = function(index) {
return this._indexToModel[index];
};
Model.prototype.first = function() {
return this.at(0);
};
Model.prototype.last = function() {
return this.at(this._indexToModel.length - 1);
};
Model.prototype.unset = function(key, options) {
var model, n, ref, ref1, v;
key = (ref = (ref1 = key[this.idAttribute]) != null ? ref1 : key.cid) != null ? ref : key;
model = this._properties[key];
delete this._properties[key];
this._indexToModel = (function() {
var ref2, results;
ref2 = this._properties;
results = [];
for (n in ref2) {
v = ref2[n];
results.push(v);
}
return results;
}).call(this);
this.length = _.keys(this._properties).length;
if (model != null) {
if (typeof model.trigger === "function") {
model.trigger('unset', model, this, options);
}
}
return this.trigger('remove', model, this, key, options);
};
Model.prototype.path = function() {
var ref, ref1;
if (this._parent != null) {
return (this._parent.path()) + "/" + ((ref = this[this.idAttribute]) != null ? ref : '');
} else {
return (ref1 = this.rootPath) != null ? ref1 : '';
}
};
Model.prototype._root = function() {
var i, j, model;
model = this;
for (i = j = 0; j <= 10; i = ++j) {
if (model._parent == null) {
break;
}
model = model._parent;
}
if (!model._parent) {
return model;
} else {
console.warn("couldn't find root for:", this);
return void 0;
}
};
Model.prototype.fetch = function(queryObject, options) {
this._root().trigger('fetch_request', this.path(), this, queryObject, options);
return this.trigger('fetch', this, queryObject, options);
};
Model.prototype._triggerUp = function() {
var args, i, j, model, name, path, ref, results;
name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
model = this;
path = '';
results = [];
for (i = j = 0; j <= 20; i = ++j) {
if (model == null) {
break;
}
model.trigger.apply(model, [name, path].concat(slice.call(args)));
path = "/" + ((ref = model[this.idAttribute]) != null ? ref : '') + path;
results.push(model = model._parent);
}
return results;
};
Model.prototype.save = function(options) {
this._root().trigger('save_request', this.path(), this, options);
return this._triggerUp('up_save_request', this, options);
};
Model.prototype.destroy = function(options) {
var ref;
this.trigger('destroy', this, options);
if (!this.isNew()) {
this._root().trigger('destroy_request', this.path(), this, options);
}
return (ref = this._parent) != null ? ref.remove(this) : void 0;
};
Model.prototype.reset = function(a, b, options) {
var key, ref, value;
ref = this._properties;
for (key in ref) {
value = ref[key];
this.unset(key);
}
if (a != null) {
this.set(a, b, options);
}
return this.trigger('reset', this, options);
};
Model.prototype.isEmpty = function() {
return this.length === 0;
};
return Model;
})(Smackbone.Event);
Smackbone.Collection = (function(superClass) {
extend(Collection, superClass);
function Collection() {
return Collection.__super__.constructor.apply(this, arguments);
}
Collection.prototype.create = function(object) {
var model;
model = this._createModelFromName(object.id, object);
this.set(model);
model.save();
return model;
};
Collection.prototype.set = function(key, value, options) {
var array, attributes, id, j, len, o, ref, ref1;
if (typeof key === 'object') {
array = _.isArray(key) ? key : [key];
attributes = {};
options = value;
for (j = 0, len = array.length; j < len; j++) {
o = array[j];
id = (ref = o[this.idAttribute]) != null ? ref : o.cid;
if (id == null) {
o = this._createModelFromName(void 0, o, Smackbone.Model);
id = (ref1 = o[this.idAttribute]) != null ? ref1 : o.cid;
}
if (o instanceof Smackbone.Model) {
if (o._parent == null) {
o._parent = this;
}
}
attributes[id] = o;
}
} else {
(attributes = {})[key] = value;
}
return Collection.__super__.set.call(this, attributes, options);
};
Collection.prototype.toJSON = function() {
return _.toArray(Collection.__super__.toJSON.call(this));
};
return Collection;
})(Smackbone.Model);
Smackbone.Syncer = (function(superClass) {
extend(Syncer, superClass);
function Syncer(options) {
this._onDestroyRequest = bind(this._onDestroyRequest, this);
this._onSaveRequest = bind(this._onSaveRequest, this);
this._onFetchRequest = bind(this._onFetchRequest, this);
this.root = options.model;
this.root.on('fetch_request', this._onFetchRequest);
this.root.on('save_request', this._onSaveRequest);
this.root.on('destroy_request', this._onDestroyRequest);
}
Syncer.prototype._onFetchRequest = function(path, model, queryObject, options) {
var request;
options = options != null ? options : {};
request = {
type: 'GET',
done: (function(_this) {
return function(response) {
var method;
method = options.reset ? 'reset' : 'set';
return model[method](response);
};
})(this)
};
_.extend(request, options);
return this._request(request, path, queryObject);
};
Syncer.prototype._onSaveRequest = function(path, model) {
var options;
options = {
type: model.isNew() ? 'POST' : 'PUT',
data: model,
done: (function(_this) {
return function(response) {
return model.set(response);
};
})(this)
};
return this._request(options, path);
};
Syncer.prototype._onDestroyRequest = function(path, model) {
var options;
options = {
type: 'DELETE',
data: model,
done: (function(_this) {
return function(response) {
return model.reset();
};
})(this)
};
return this._request(options, path);
};
Syncer.prototype._encodeQueryObject = function(queryObject) {
var array, key, value;
array = (function() {
var results;
results = [];
for (key in queryObject) {
value = queryObject[key];
results.push(key + "=" + value);
}
return results;
})();
if (array.length) {
return encodeURI('?' + array.join('&'));
} else {
return '';
}
};
Syncer.prototype._request = function(options, path, queryObject) {
var queryString, ref, ref1;
queryString = this._encodeQueryObject(queryObject);
options.url = ((ref = this.urlRoot) != null ? ref : '') + path + queryString;
if (options.type === 'GET') {
options.data = void 0;
} else {
options.data = JSON.stringify((ref1 = options.data) != null ? ref1.toJSON() : void 0);
}
options.contentType = 'application/json';
this.trigger('request', options);
return Smackbone.$.ajax(options).done(options.done);
};
return Syncer;
})(Smackbone.Event);
}).call(this);