models
Version:
M form MVC
226 lines (199 loc) • 7.65 kB
JavaScript
/* vim:set ts=2 sw=2 sts=2 expandtab */
/*jshint undef: true es5: true node: true devel: true
forin: true, eqnull: true */
/*global define: true */
(typeof define === "undefined" ? function ($) { $(require, exports, module); } : define)(function (require, exports, module, undefined) {
"use strict";
var EventEmitter = require('!raw.github.com/Gozala/events/v0.4.0/events').EventEmitter;
exports.version = "0.1.0";
var isArray = Array.isArray;
function isFunction(value) { return typeof value === "function"; }
function getOwnPropertyDescriptors(object) {
var descriptors = {};
Object.getOwnPropertyNames(object).forEach(function(name) {
descriptors[name] = Object.getOwnPropertyDescriptor(object, name);
});
return descriptors;
}
function extend(target) {
Array.prototype.forEach.call(arguments, function(source) {
if (source !== target)
Object.defineProperties(target, getOwnPropertyDescriptors(source));
});
return target;
}
exports.Model = EventEmitter.extend({
constructor: function Model(attributes, options) {
if (!(this instanceof Model))
return new Model(attributes, options);
attributes = attributes || {};
var defaults = this.defaults;
if (defaults) {
if (isFunction(defaults))
defaults = defaults();
attributes = extend({}, defaults, attributes);
}
this.attributes = {};
this.set(attributes, { silent : true });
if (options && 'collection' in options && options.collection)
this.collection = options.collection;
this.initialize(attributes, options);
},
isModel: true,
isNew: function isNew() {
return !this.id;
},
/**
* The attributes property is the internal hash containing the model's state.
* Please use set to update the attributes instead of modifying them
* directly. If you'd like to retrieve and munge a copy of the model's
* attributes, use `toJSON` instead.
*/
attributes: null,
/**
* The defaults hash can be used to specify the default attributes for your
* model. When creating an instance of the model, any unspecified attributes
* will be set to their default value.
*/
defaults: null,
/**
* A special property of models, the id is an arbitrary string (integer id
* or UUID). If you set the id in the attributes hash, it will be copied onto
* the model as a direct property. Models can be retrieved by id from
* collections.
*/
id: null,
/**
* Attribute name that is mapped to an `id` property of this model. Analog to
* primary key in DB.
*/
'@' : 'id',
/**
* Initialize is an empty function by default. Override it with your own
* initialization logic.
* @param {Object} attributes
* @param {Object} options
*/
initialize : function initialize(attributes, options) { return this; },
/**
* Consumer must implement custom validation logic in this method. Method is
* called by a `set`, and is passed the attributes that are about to be
* updated. If the model and attributes are valid, don't return anything from
* validate. If the attributes are invalid, throw an Error of your choice. It
* can be as simple `Error` with an error message to be displayed, or a
* complete error object that describes the error programmatically. `set` and
* save will not continue if validate returns an error.
* Failed validations trigger an "error" event.
* @param {Object} attributes
* Map of key values that needs to be validated before they are set.
*/
validate: function validate(attributes) { return attributes; },
prase: function parse(data) { return data; },
// Returns `true` if the attribute contains a value that is not null
// or undefined.
has: function(attr) {
return this.attributes[attr] != null;
},
/**
* Get the current value of an attribute from the model.
*/
get: function get(key) {
return this.attributes[key];
},
/**
* Set a hash of attributes (one or many) on the model. If any of the
* attributes change the models state, a "change" event will be triggered,
* unless {silent: true} is passed as an option. Change events for specific
* attributes are also triggered, and you can bind to those as well, for
* example change:title, and change:content.
*/
set: function set(attributes, options) {
var changes, silent, id;
// Validate all the attributes using internal validation mechanism. If
// new attributes are returned that means that values were formated or
// overridden by a validator.
attributes = attributes.isModel ? attributes.attributes : attributes;
attributes = this.validate(attributes);
silent = options && options.silent;
// Check for changes of `id`.
if ((id = attributes[this['@']]))
this.id = id;
Object.keys(attributes).forEach(function(key) {
var previous = this.attributes[key];
var value = this.attributes[key] = attributes[key];
if (!silent && previous !== value) {
this.emit("change:" + key, ((changes || (changes = {}))[key] = {
key: key, previous: previous, value: value
}), this, options);
}
}, this);
if (!silent && changes)
this.emit("change", changes, this, options);
return this;
},
clone: function clone() {
return new this.constructor(this);
},
save: function save(attributes, options) {
options = options || {};
if (attributes)
this.set(attributes, options);
var action = this.isNew() ? "create" : "update";
(this.constructor.sync || this.sync)(action, this, options);
},
fetch: function fetch(options) {
options = options || {};
var model = this;
var success = options.success;
options.success = function(response) {
model.set(model.parse(response), options);
if (success) success(model, response);
};
(this.constructor.sync || this.sync)("read", this, options);
},
/**
* Remove an attribute by deleting it from the internal attributes hash.
* Fires a "change" event unless silent is passed as an option.
*/
unset: function unset(attributes, options) {
var changes, silent;
silent = options && options.silent;
(isArray(attributes) ? attributes : [attributes]).forEach(function(key) {
// Check for changes of `id`.
if (key === this['@'])
this.id = null;
var previous = this.attributes[key];
delete this.attributes[key];
var value = this.attributes[key];
if (!silent && previous !== value) {
this.emit("change:" + key, ((changes || (changes = {}))[key] = {
key: key, previous: previous, value: value
}));
}
}, this);
if (!silent && changes)
this.emit("change", changes);
},
/**
* Removes all attributes from the model. Fires a "change" event unless
* silent is passed as an option.
*/
clear: function clear(options) {
this.unset(Object.keys(this.attributes), options);
},
destroy: function destroy(options) {
(this.constructor.sync || this.sync)("delete", this, options);
},
/**
* Return a copy of the model's attributes for JSON stringification. This can
* be used for persistence, serialization, or for augmentation before being
* handed off to a view. The name of this method is a bit confusing, as it
* doesn't actually return a JSON string — but I'm afraid that it's the way
* that the [JavaScript API for JSON.stringify works]
* (https://developer.mozilla.org/en/JSON#toJSON()_method).
*/
toJSON: function toJSON() {
return JSON.parse(JSON.stringify(this.attributes));
}
});
});