este-library-oldschool
Version:
Library for github.com/steida/este.git
603 lines (556 loc) • 13.9 kB
JavaScript
// Generated by github.com/steida/coffee2closure 900.1.18
/**
@fileoverview Base class for models with attributes and schema. Use model
instead of plain json object and you will be able to listen model changes,
validate model, and serialize/deserialize it.
Features:
- setters, getters, validators
- 'add', 'remove', 'change', 'sort', 'update' events
- JSON serialization/deserialization
Why not plain objects with properties?
- strings are better for uncompiled attributes from DOM, storages etc.
- http://www.devthought.com/2012/01/18/an-object-is-not-a-hash
How to modify complex properties?
```user.set 'tags', (tags) -> tags.push 'some tag'```
@see /demos/model/model.html
@see /demos/app/todomvc/index.html
*/
var 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.superClass_ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
goog.provide('este.Model');
goog.provide('este.Model.EventType');
goog.provide('este.Model.Event');
goog.require('este.Base');
goog.require('este.json');
goog.require('este.model.getters');
goog.require('este.model.setters');
goog.require('goog.events.Event');
goog.require('goog.object');
goog.require('goog.ui.IdGenerator');
goog.require('este.validators.Base');
goog.require('este.validators.digits');
goog.require('este.validators.email');
goog.require('este.validators.exclusion');
goog.require('este.validators.format');
goog.require('este.validators.inclusion');
goog.require('este.validators.max');
goog.require('este.validators.maxLength');
goog.require('este.validators.min');
goog.require('este.validators.minLength');
goog.require('este.validators.number');
goog.require('este.validators.range');
goog.require('este.validators.rangeLength');
goog.require('este.validators.required');
goog.require('este.validators.url');
/**
@param {Object=} json
@param {function(): string=} idGenerator
@constructor
@extends {este.Base}
*/
este.Model = function(json, idGenerator) {
var defaults;
este.Model.superClass_.constructor.call(this);
this.attributes = {};
if (this.schema == null) {
this.schema = {};
}
defaults = this.getResolvedDefaults();
if (defaults) {
this.setAttributes(defaults);
}
if (json) {
this.setAttributes(json);
}
this.ensureClientId(idGenerator);
}
extend(este.Model, superClass);
/**
@enum {string}
*/
este.Model.EventType = {
CHANGE: 'change',
ADD: 'add',
REMOVE: 'remove',
SORT: 'sort',
UPDATE: 'update'
};
/**
@param {?} a
@param {?} b
@suppress {missingProperties} Because runtime detection.
*/
este.Model.equal = function(a, b) {
if (!a || !b) {
return a === b;
}
if (a != null ? a.toJson : void 0) {
a = este.json.stringify(a.toJson());
}
if (b != null ? b.toJson : void 0) {
b = este.json.stringify(b.toJson());
}
return este.json.equal(a, b);
};
/**
http://www.restapitutorial.com/lessons/restfulresourcenaming.html
Url has to start with '/'. Function type is usefull for inheritance.
@type {string|function(): string}
@protected
*/
este.Model.prototype.url = '/models';
/**
@type {Object}
@protected
*/
este.Model.prototype.defaults = null;
/**
@type {Object}
@protected
*/
este.Model.prototype.schema = null;
/**
Custom ID attribute, ex. 'userId' or '_id'.
Nested keys are supported, ex. '_id.$oid'.
@type {string}
@protected
*/
este.Model.prototype.idAttribute = 'id';
/**
@type {string}
@protected
*/
este.Model.prototype.id = '';
/**
@type {Object}
@protected
*/
este.Model.prototype.attributes = null;
/**
@param {string|number} id
*/
este.Model.prototype.setId = function(id) {
var i, idAttribute, idAttributes, j, json, len, nested;
json = {};
nested = json;
idAttributes = this.idAttribute.split('.');
for (i = j = 0, len = idAttributes.length; j < len; i = ++j) {
idAttribute = idAttributes[i];
if (i === idAttributes.length - 1) {
nested[idAttribute] = id;
} else {
nested = nested[idAttribute] = {};
}
}
return this.set(json);
};
/**
@return {string}
*/
este.Model.prototype.getId = function() {
return this.id;
};
/**
@return {string}
*/
este.Model.prototype.getUrl = function() {
var url;
url = this.url;
if (goog.isFunction(url)) {
url = url();
}
return url;
};
/**
Generates URLs of the form:
"/[collection.getUrl()]/[getId()]" if collection
"/[model.url]/[getId()]" without collection
@param {este.Collection=} collection
@return {string}
*/
este.Model.prototype.createUrl = function(collection) {
var id, url;
url = collection ? collection.getUrl() : this.getUrl();
id = this.getId();
if (!id) {
return url;
}
return url + '/' + id;
};
/**
Set attributes and dispatch events on model change.
Example:
```coffee
model.set 'foo', 1
model set 'foo': 1, 'bla': 2
```
@param {Object|string} json Object of key value pairs or string key.
@param {*=} value value or nothing.
*/
este.Model.prototype.set = function(json, value) {
var _json, changes;
if (typeof json === 'string') {
_json = {};
_json[json] = value;
json = _json;
}
changes = this.getChanges(json);
if (!changes) {
return;
}
this.setAttributes(changes);
this.dispatchChangeEvent(changes);
};
/**
@param {Object} json
@return {Object}
@protected
*/
este.Model.prototype.getChanges = function(json) {
var changes, clone, key, ref, set, value;
changes = null;
for (key in json) {
value = json[key];
if (goog.isFunction(value)) {
clone = this.getClone(key);
value(clone);
value = clone;
}
set = (ref = this.schema[key]) != null ? ref.set : void 0;
if (set) {
value = set(value);
}
if (este.Model.equal(value, this.get(key))) {
continue;
}
if (changes == null) {
changes = {};
}
changes[key] = value;
}
return changes;
};
/**
@param {Object} json
@protected
*/
este.Model.prototype.setAttributes = function(json) {
var $key, currentValue, idAttributes, key, value;
idAttributes = this.idAttribute.split('.');
for (key in json) {
value = json[key];
$key = this.getKey(key);
currentValue = this.attributes[$key];
if (key === idAttributes[0]) {
this.setIdAttribute(value, currentValue, idAttributes);
}
if (key === '_cid' && (currentValue != null)) {
goog.asserts.fail('Model _cid is immutable.');
}
this.attributes[$key] = value;
if (value instanceof este.Base) {
value.addParent(this);
}
}
};
/**
Returns model attribute(s).
@param {string|Array.<string>} key
@return {*|Object.<string, *>}
*/
este.Model.prototype.get = function(key) {
var get, j, json, k, len, meta, ref, ref1, value;
if (typeof key !== 'string') {
json = {};
for (j = 0, len = key.length; j < len; j++) {
k = key[j];
json[k] = this.get(k);
}
return json;
}
meta = (ref = this.schema[key]) != null ? ref['meta'] : void 0;
if (meta) {
return meta(this);
}
value = this.attributes[this.getKey(key)];
get = (ref1 = this.schema[key]) != null ? ref1.get : void 0;
if (get) {
return get(value);
}
return value;
};
/**
Returns cloned model attribute(s).
@param {string|Array.<string>} key
@return {*|Object.<string, *>}
*/
este.Model.prototype.getClone = function(key) {
return goog.object.unsafeClone(this.get(key));
};
/**
@param {string} key
@return {boolean}
*/
este.Model.prototype.has = function(key) {
var ref;
if (this.getKey(key) in this.attributes) {
return true;
}
if ((ref = this.schema[key]) != null ? ref['meta'] : void 0) {
return true;
}
return false;
};
/**
@param {string} key
@return {boolean} true if removed
*/
este.Model.prototype.remove = function(key) {
var _key, changed, value;
_key = this.getKey(key);
if (!(_key in this.attributes)) {
return false;
}
value = this.attributes[_key];
if (value instanceof este.Base) {
value.removeParent(this);
}
delete this.attributes[_key];
changed = {};
changed[key] = value;
this.dispatchChangeEvent(changed);
return true;
};
/**
Return a model JSON representation, which can be used for persistence,
serialization, or model view rendering.
@param {boolean=} raw If true, _cid, metas, and getters are ignored.
@return {Object}
*/
este.Model.prototype.toJson = function(raw) {
var $key, attr, json, key, meta, ref, ref1, value;
json = {};
ref = this.attributes;
for ($key in ref) {
value = ref[$key];
key = $key.substring(1);
if (raw && key === '_cid') {
continue;
}
attr = raw ? value : this.get(key);
if (attr != null ? attr.toJson : void 0) {
json[key] = attr.toJson(raw);
} else {
json[key] = attr;
}
}
if (!raw) {
ref1 = this.schema;
for (key in ref1) {
value = ref1[key];
meta = value != null ? value['meta'] : void 0;
if (!meta) {
continue;
}
json[key] = meta(this);
}
}
return json;
};
/**
Validate model recursively.
@return {Array.<este.validators.Base>}
*/
este.Model.prototype.validate = function() {
var allErrors, errors, key, keys, ref, value, values;
keys = (function() {
var ref, results;
ref = this.schema;
results = [];
for (key in ref) {
value = ref[key];
if (value != null ? value['validators'] : void 0) {
results.push(key);
}
}
return results;
}).call(this);
values = /** @type {Object} */(this.get(keys));
allErrors = this.getErrors(values) || [];
ref = this.attributes;
for (key in ref) {
value = ref[key];
if (!value || !goog.isFunction(value.validate)) {
continue;
}
errors = value.validate();
if (!errors) {
continue;
}
allErrors.push.apply(allErrors, errors);
}
if (allErrors.length) {
return allErrors;
}
return null;
};
/**
Prefix because http://www.devthought.com/2012/01/18/an-object-is-not-a-hash
@param {string} key
@return {string}
*/
este.Model.prototype.getKey = function(key) {
return '$' + key;
};
/**
Returns true if model has no ID.
@return {boolean}
*/
este.Model.prototype.isNew = function() {
return !this.getId();
};
/**
@return {Object}
@protected
*/
este.Model.prototype.getResolvedDefaults = function() {
if (!this.defaults) {
return {};
}
return goog.object.map(this.defaults, function(value, key) {
switch (goog.typeOf(value)) {
case 'function':
return value();
case 'object':
case 'array':
return goog.object.unsafeClone(value);
default:
return value;
}
});
};
/**
@protected
*/
este.Model.prototype.setIdAttribute = function(value, currentValue, idAttributes) {
var nestedId;
if (currentValue != null) {
goog.asserts.fail('Model id is immutable.');
}
if (idAttributes.length === 1) {
this.id = value.toString();
} else {
nestedId = goog.object.getValueByKeys(value, idAttributes.slice(1));
this.id = nestedId.toString();
}
};
/**
@param {Object} json key is attr, value is its value
@return {Array.<este.validators.Base>}
@protected
*/
este.Model.prototype.getErrors = function(json) {
var errors, j, key, len, ref, validator, validatorFactory, validators, value;
errors = [];
for (key in json) {
value = json[key];
validators = (ref = this.schema[key]) != null ? ref['validators'] : void 0;
if (!validators) {
continue;
}
for (j = 0, len = validators.length; j < len; j++) {
validatorFactory = validators[j];
validator = validatorFactory();
validator.model = this;
validator.key = key;
validator.value = value;
if (!validator.isValidable()) {
continue;
}
if (validator.validate(value)) {
continue;
}
errors.push(validator);
}
}
if (errors.length) {
return errors;
}
return null;
};
/**
@param {Object} changed
@protected
*/
este.Model.prototype.dispatchChangeEvent = function(changed) {
var changeEvent;
changeEvent = new este.Model.Event(este.Model.EventType.CHANGE, this);
changeEvent.model = this;
changeEvent.changed = changed;
return this.dispatchModelEvent(changeEvent);
};
/**
@param {este.Model.Event} e
@protected
*/
este.Model.prototype.dispatchModelEvent = function(e) {
var updateEvent;
this.dispatchEvent(e);
updateEvent = new este.Model.Event(este.Model.EventType.UPDATE, this);
updateEvent.origin = e;
return this.dispatchEvent(updateEvent);
};
/**
@param {function(): string=} idGenerator
@protected
*/
este.Model.prototype.ensureClientId = function(idGenerator) {
var _cid;
if (this.get('_cid')) {
return;
}
_cid = idGenerator ? idGenerator() : goog.ui.IdGenerator.getInstance().getNextUniqueId();
return this.setAttributes({
'_cid': _cid
});
};
/**
@fileoverview este.Model.Event.
*/
/**
@param {string} type Event Type.
@param {goog.events.EventTarget} target
@constructor
@extends {goog.events.Event}
*/
este.Model.Event = function(type, target) {
este.Model.Event.superClass_.constructor.call(this, type, target);
}
extend(este.Model.Event, superClass);
/**
@type {este.Model}
*/
este.Model.Event.prototype.model = null;
/**
Changed model attributes.
@type {Object}
*/
este.Model.Event.prototype.changed = null;
/**
Added models.
@type {Array.<este.Model>}
*/
este.Model.Event.prototype.added = null;
/**
Removed models.
@type {Array.<este.Model>}
*/
este.Model.Event.prototype.removed = null;
/**
@type {este.Model.Event}
*/
este.Model.Event.prototype.origin = null;
/**
* @type {Object}
*/
este.Model.Event.prototype.data = null;