kaiwa/clientapp/modules/strictmodel.js

566 lines
17 KiB
JavaScript

// (c) 2013 Henrik Joreteg
// MIT Licensed
// For all details and documentation:
// https://github.com/HenrikJoreteg/StrictModel
(function () {
'use strict';
// Initial setup
// -------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both CommonJS and the browser.
var Strict = typeof exports !== 'undefined' ? exports : root.Strict = {},
toString = Object.prototype.toString,
slice = Array.prototype.slice;
// Current version of the library. Keep in sync with `package.json`.
Strict.VERSION = '0.0.1';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// Require Backbone, if we're on the server, and it's not already present.
var Backbone = root.Backbone;
if (!Backbone && (typeof require !== 'undefined')) Backbone = require('backbone');
// Backbone Collection compatibility fix:
// In backbone, when you add an already instantiated model to a collection
// the collection checks to see if what you're adding is already a model
// the problem is, it does this witn an instanceof check. We're wanting to
// use completely different models so the instanceof will fail even if they
// are "real" models. So we work around this by overwriting this method from
// backbone 1.0.0. The only difference is it compares against our Strict.Model
// instead of backbone's.
Backbone.Collection.prototype._prepareModel = function (attrs, options) {
if (attrs instanceof Strict.Model) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options || (options = {});
options.collection = this;
var model = new this.model(attrs, options);
if (!model._validate(attrs, options)) {
this.trigger('invalid', this, attrs, options);
return false;
}
return model;
};
// Helpers
// -------
// Shared empty constructor function to aid in prototype-chain creation.
var Constructor = function () {};
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var inherits = function (parent, protoProps, staticProps) {
var child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function () { return parent.apply(this, arguments); };
}
// Inherit class (static) properties from parent.
_.extend(child, parent);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
Constructor.prototype = parent.prototype;
child.prototype = new Constructor();
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// Add static properties to the constructor function, if supplied.
if (staticProps) _.extend(child, staticProps);
// Correctly set child's `prototype.constructor`.
child.prototype.constructor = child;
// Set a convenience property in case the parent's prototype is needed later.
child.__super__ = parent.prototype;
return child;
};
var extend = function (protoProps, classProps) {
var child = inherits(this, protoProps, classProps);
child.extend = this.extend;
return child;
};
// Mixins
// ------
// Sugar for defining properties a la ES5.
var Mixins = Strict.Mixins = {
// shortcut for Object.defineProperty
define: function (name, def) {
Object.defineProperty(this, name, def);
},
defineGetter: function (name, handler) {
this.define(name, {
get: handler.bind(this)
});
},
defineSetter: function (name, handler) {
this.define(name, {
set: handler.bind(this)
});
}
};
// Strict.Registry
// ---------------
// Internal storage for models, seperate namespace
// storage from default to prevent collision of matching
// model type+id and namespace name
var Registry = Strict.Registry = function () {
this._cache = {};
this._namespaces = {};
};
// Attach all inheritable methods to the Registry prototype.
_.extend(Registry.prototype, {
// Get the general or namespaced internal cache
_getCache: function (ns) {
if (ns) {
this._namespaces[ns] || (this._namespaces[ns] = {});
return this._namespaces[ns];
}
return this._cache;
},
// Find the cached model
lookup: function (type, id, ns) {
var cache = this._getCache(ns);
return cache && cache[type + id];
},
// Add a model to the cache if it has not already been set
store: function (model) {
var cache = this._getCache(model._namespace),
key = model.type + model.id;
// Prevent overriding a previously stored model
cache[key] = cache[key] || model;
return this;
},
// Remove a stored model from the cache, return `true` if removed
remove: function (type, id, ns) {
var cache = this._getCache(ns);
if (this.lookup.apply(this, arguments)) {
delete cache[type + id];
return true;
}
return false;
},
// Reset internal cache
clear: function () {
this._cache = {};
this._namespaces = {};
}
});
// Create the default Strict.registry.
Strict.registry = new Registry();
// Strict.Model
// ------------
var Model = Strict.Model = function (attrs, options) {
attrs = attrs || {};
options = options || {};
var modelFound,
opts = _.defaults(options || {}, {
seal: true
});
this._namespace = opts.namespace;
this._initted = false;
this._deps = {};
this._initProperties();
this._initCollections();
this._cache = {};
this._verifyRequired();
this.set(attrs, {silent: true});
this.init.apply(this, arguments);
if (attrs.id) Strict.registry.store(this);
this._previous = _.clone(this.attributes); // Should this be set right away?
this._initted = true;
};
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Backbone.Events, Mixins, {
idAttribute: 'id',
idDefinition: {
type: 'number',
setOnce: true
},
// stubbed out to be overwritten
init: function () {
return this;
},
// Remove model from the registry and unbind events
remove: function () {
if (this.id) {
Strict.registry.remove(this.type, this.id, this._namespace);
}
this.trigger('remove', this);
this.off();
return this;
},
set: function (key, value, options) {
var self = this,
changing = self._changing,
opts,
changes = [],
newType,
interpretedType,
newVal,
def,
attr,
attrs,
val;
self._changing = true;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (_.isObject(key) || key === null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
opts = options || {};
// For each `set` attribute...
for (attr in attrs) {
val = attrs[attr];
newType = typeof val;
newVal = val;
def = this.definition[attr] || {};
// check type if we have one
if (def.type === 'date') {
if (!_.isDate(val)) {
try {
newVal = (new Date(parseInt(val, 10))).valueOf();
newType = 'date';
} catch (e) {
newType = typeof val;
}
} else {
newType = 'date';
newVal = val.valueOf();
}
} else if (def.type === 'array') {
newType = _.isArray(val) ? 'array' : typeof val;
} else if (def.type === 'object') {
// we have to have a way of supporting "missing" objects.
// Null is an object, but setting a value to undefined
// should work too, IMO. We just override it, in that case.
if (typeof val !== 'object' && _.isUndefined(val)) {
newVal = null;
newType = 'object';
}
}
// If we have a defined type and the new type doesn't match, throw error.
// Unless it's not required and the value is undefined.
if (def.type && def.type !== newType && (!def.required && !_.isUndefined(val))) {
throw new TypeError('Property \'' + attr + '\' must be of type ' + def.type + '. Tried to set ' + val);
}
// if trying to set id after it's already been set
// reject that
if (def.setOnce && def.value !== undefined && !_.isEqual(def.value, newVal)) {
throw new TypeError('Property \'' + key + '\' can only be set once.');
}
// only change if different
if (!_.isEqual(def.value, newVal)) {
self._previous && (self._previous[attr] = def.value);
def.value = newVal;
changes.push(attr);
}
}
_.each(changes, function (key) {
if (!opts.silent) {
self.trigger('change:' + key, self, self[key]);
}
// TODO: ensure that all deps are not undefined before triggering a change event
(self._deps[key] || []).forEach(function (derTrigger) {
// blow away our cache
delete self._cache[derTrigger];
if (!opts.silent) self.trigger('change:' + derTrigger, self, self.derived[derTrigger]);
});
});
// fire general change events
if (changes.length) {
if (!opts.silent) self.trigger('change', self);
}
},
get: function (attr) {
return this[attr];
},
// convenience methods for manipulating array properties
addListVal: function (prop, value, prepend) {
var list = _.clone(this[prop]) || [];
if (!_(list).contains(value)) {
list[prepend ? 'unshift' : 'push'](value);
this[prop] = list;
}
return this;
},
previous: function (attr) {
return attr ? this._previous[attr] : _.clone(this._previous);
},
removeListVal: function (prop, value) {
var list = _.clone(this[prop]) || [];
if (_(list).contains(value)) {
this[prop] = _(list).without(value);
}
return this;
},
hasListVal: function (prop, value) {
return _.contains(this[prop] || [], value);
},
// -----------------------------------------------------------------------
_initCollections: function () {
var coll;
if (!this.collections) return;
for (coll in this.collections) {
this[coll] = new this.collections[coll]();
this[coll].parent = this;
}
},
// Check that all required attributes are present
// TODO: should this throw an error or return boolean?
_verifyRequired: function () {
var attrs = this.attributes;
for (var def in this.definition) {
if (this.definition[def].required && typeof attrs[def] === 'undefined') {
return false;
}
}
return true;
},
_initProperties: function () {
var self = this,
definition = this.definition = {},
val,
prop,
item,
type,
filler;
this.cid = _.uniqueId('model');
function addToDef(name, val, isSession) {
var def = definition[name] = {};
if (_.isString(val)) {
// grab our type if all we've got is a string
type = self._ensureValidType(val);
if (type) def.type = type;
} else {
type = self._ensureValidType(val[0] || val.type);
if (type) def.type = type;
if (val[1] || val.required) def.required = true;
// set default if defined
def.value = !_.isUndefined(val[2]) ? val[2] : val.default;
if (isSession) def.session = true;
if (val.setOnce) def.setOnce = true;
}
}
// loop through given properties
for (item in this.props) {
addToDef(item, this.props[item]);
}
// loop through session props
for (prop in this.session) {
addToDef(prop, this.session[prop], true);
}
// always add "id" as a definition or make sure it's 'setOnce'
if (definition.id) {
definition[this.idAttribute].setOnce = true;
} else {
addToDef(this.idAttribute, this.idDefinition);
}
// register derived properties as part of the definition
this._registerDerived();
this._createGettersSetters();
// freeze attributes used to define object
if (this.session) Object.freeze(this.session);
//if (this.derived) Object.freeze(this.derived);
if (this.props) Object.freeze(this.props);
},
// just makes friendlier errors when trying to define a new model
// only used when setting up original property definitions
_ensureValidType: function (type) {
return _.contains(['string', 'number', 'boolean', 'array', 'object', 'date'], type) ? type : undefined;
},
_validate: function () {
return true;
},
_createGettersSetters: function () {
var item, def, desc, self = this;
// create getters/setters based on definitions
for (item in this.definition) {
def = this.definition[item];
desc = {};
// create our setter
desc.set = function (def, item) {
return function (val, options) {
self.set(item, val);
};
}(def, item);
// create our getter
desc.get = function (def, attributes) {
return function (val) {
if (typeof def.value !== 'undefined') {
if (def.type === 'date') {
return new Date(def.value);
}
return def.value;
}
return;
};
}(def);
// define our property
this.define(item, desc);
}
this.defineGetter('attributes', function () {
var res = {};
for (var item in this.definition) res[item] = this[item];
return res;
});
this.defineGetter('keys', function () {
return Object.keys(this.attributes);
});
this.defineGetter('json', function () {
return JSON.stringify(this._getAttributes(false, true));
});
this.defineGetter('derived', function () {
var res = {};
for (var item in this._derived) res[item] = this._derived[item].fn.apply(this);
return res;
});
this.defineGetter('toTemplate', function () {
return _.extend(this._getAttributes(true), this.derived);
});
},
_getAttributes: function (includeSession, raw) {
var res = {};
for (var item in this.definition) {
if (!includeSession) {
if (!this.definition[item].session) {
res[item] = (raw) ? this.definition[item].value : this[item];
}
} else {
res[item] = (raw) ? this.definition[item].value : this[item];
}
}
return res;
},
// stores an object of arrays that specifies the derivedProperties
// that depend on each attribute
_registerDerived: function () {
var self = this, depList;
if (!this.derived) return;
this._derived = this.derived;
for (var key in this.derived) {
depList = this.derived[key].deps || [];
_.each(depList, function (dep) {
self._deps[dep] = _(self._deps[dep] || []).union([key]);
});
// defined a top-level getter for derived keys
this.define(key, {
get: _.bind(function (key) {
// is this a derived property we should cache?
if (this._derived[key].cache) {
// do we have it?
if (this._cache.hasOwnProperty(key)) {
return this._cache[key];
} else {
return this._cache[key] = this._derived[key].fn.apply(this);
}
} else {
return this._derived[key].fn.apply(this);
}
}, this, key),
set: _.bind(function (key) {
var deps = this._derived[key].deps,
msg = '"' + key + '" is a derived property, you can\'t set it directly.';
if (deps && deps.length) {
throw new TypeError(msg + ' It is dependent on "' + deps.join('" and "') + '".');
} else {
throw new TypeError(msg);
}
}, this, key)
});
}
}
});
// Set up inheritance for the model
Strict.Model.extend = extend;
// Overwrite Backbone.Model so that collections don't need to be modified in Backbone core
Backbone.Model = Strict.Model;
}).call(this);