src/Model.js
/**
* PrototypeJS Model extension - Enables Prototype JS users to fetch / store
* Models from / to a backend using AJAX / REST
*
* Inspired by (but not copied) [Backbone's Backbone.Model](http://backbonejs.org/) and Backbone.sync
*
* Usage example:
*
* ```javascript
* // Create your own Model class:
* var Person = Class.create(Protobone.Model,{
* urlRoot: '/entity/Person'
* });
*
* // Use an instance of the model:
* var alex = new Person({
* name: 'Schenkel',
* firstname: 'Alex'
* });
* alex.save({onSuccess: function(res,model){
* console.log(model.getId());
* }});
* ```
*
* @author Alexander Schenkel <alex@alexi.ch>
* @copyright 2015 Alexander Schenkel
* @license Released under the MIT License
* @class Protobone.Model
* @extends Protobone.Base
* @constructor
*/
var statics = require('./statics.js');
var Base = require('./Base.js');
var Model = Class.create(Base, {
/**
* Defines the name of the ID attribute. Defaults to `id`.
*
* @property id
*/
idAttribute: 'id',
/**
* The URL root for this Model. Must be set in child classes,
* e.g. to '/entities/Person'.
* Used by the url() function to build the persistence URL.
*
* @property urlRoot
* @type String
*/
urlRoot: '',
/**
* Used by the parse() function, it defines the root property
* in the server's json response which contains the
* payload data for the model. Defaults to null (delivered json
* directly contains model attributes)
*
* @property rootProperty
*/
rootProperty: null,
/**
* Constructor. Sets the given data (key/value pairs)
* as attributes on new model instances.
*
* @method constructor
* @param {Object} data Initial data (key/value pairs) to set on the new Model instance, e.g.: `{name: 'Alex',age: 26}`
*/
initialize: function($super, data) {
$super();
data = data || {};
this._attributes = {};
/** TODO: Implement dirty attribute detection */
this._dirtyAttributes = {};
this.set(data);
},
/**
* Returns the instance's ID of the model. Null means it is a new, not saved
* instance.
*
* @method getId
* @return {mixed} The ID (int, string), if any
*/
getId: function() {
return this[this.idAttribute] || null;
},
/**
* Sets the Model instance's ID. It also sets it as attribute
* value so that it is sent to the server when synced.
*
* @method setId
* @param {mixed} id The id to set (e.g. an integer, or even a string)
* @return {this} Supports fluent interface by returning itself
*/
setId: function(id) {
this[this.idAttribute] = id || null;
this._attributes[this.idAttribute] = id || null;
return this;
},
/**
* Sets Model attributes (key/values). Takes either a key and a value,
* or a plain object containing key/value pairs.
*
* @method set
* @param {string}/Object keyOrObject A string representing the key (e.g. 'name')
* or an object with key/values (e.g. 'name':'alex','age':'too old')
* @param {mixed} value The value to set if keyOrObject is a string. Ignored when keyOrObject is an object.
* @return {this} Supports fluent interface by returning itself
*/
set: function(keyOrObject, value) {
var oldValues = {},
newValues = {},
obj = {};
if (typeof keyOrObject === 'string') {
obj[keyOrObject] = value;
} else if (typeof keyOrObject === 'object') {
obj = keyOrObject;
}
$H(obj).each(function(pair) {
this._setAttribute(pair.key, pair.value,newValues,oldValues);
}.bind(this));
this.fireEvent('updated',this,newValues,oldValues);
return this;
},
/**
* Sets a single Model attribute (e.g. 'name' to 'Alex'). Internal helper function.
* Please use set() instead.
*
* @param {string} key The key of the attribute to set, e.g. 'name'
* @param {mixed} value The value to set
* @return {this} Supports fluent interface by returning itself
*/
_setAttribute: function(key, value,newVals, oldVals) {
if (typeof key === 'string') {
if (this._attributes[key] !== value) {
if (oldVals) oldVals[key] = this._attributes[key];
if (newVals) newVals[key] = value;
this._dirtyAttributes[key] = value;
}
this._attributes[key] = value;
if (key === this.idAttribute) {
this.setId(value);
}
}
return this;
},
/**
* Returns a specific attribute, or all if key is omitted
*
* @method get
* @param {string} key The name of the attribute to get. If omitted, an object
* containing all attributes (key/value) is returned.
* @return {mixed} The value of the requested attribute, or an object with all attributes
*/
get: function(key) {
if (!key) {
return Object.clone(this._attributes);
}
if (this.hasAttribute(key)) {
return this._attributes[key];
} else {
return null;
}
},
/**
* Creates the REST url for the actual state of the Model. Override this
* method if you want to implement your own URL scheme. Here is how it works
* by default:
*
* - non-persistent state (id = null): return '<urlRoot>'
* - persistent state (id <> null): return '<urlRoot>/<id>'
*
* @method url
* @return {String} The URL for this Model instance, e.g. `/root/Entity/3`
*/
url: function() {
var url = this.urlRoot;
if (!url) throw new Error("urlRoot not set. Please define an urlRoot in your model.");
if (!!this.getId()) {
url += '/' + String(this.getId());
}
return url;
},
/**
* Makes this model persistent by sending the data to a REST interface (by default).
* Make sure to set the urlRool property on class definition.
*
* options are all options that Prototype's Ajax.Request understands, so you
* can e.g. deliver a onSuccess callback:
*
* ```javascript
* myModel.save({onSuccess: function(response,model){
* // do something after save here
* }});
* ```
*
* @method save
* @param {Object} options Additional Ajax options to be sent to Ajax.Request.
*/
save: function(options) {
var url = this.url(),
method = !!this.getId()?'update':'create';
return this._request(url, method, options);
},
/**
* Fetches this Model's representation from the server. Only
* allowed for existing (id <> null) models. options is passed
* along to Prototype's Ajax.Request function.
*
* @method fetch
* @param {Object} options Additional Ajax options to be sent to Ajax.Request.
*/
fetch: function(options) {
if (!this.getId()) throw new Error('Cannot be called for new Models');
var url = this.url(),
method = 'read';
return this._request(url, method, options);
},
/**
* invokes a delete request to the server. Only
* allowed for existing (id <> null) models. options is passed
* along to Prototype's Ajax.Request function.
*
* After the deletion was successful, the model instance is updated with the
* server data, even if the server removed the instance.
*
* @method destroy
* @param {Object} options Additional Ajax options to be sent to Ajax.Request.
*/
destroy: function(options) {
if (!this.getId()) throw new Error('Cannot be called for new Models');
var url = this.url(),
method = 'delete';
return this._request(url, method, options);
},
/**
* internal helper function for initiating the requests for save, fetch, destroy
*/
_request: function (url,method,options) {
var syncOptions = {};
options = options || {};
Object.extend(syncOptions, {
onSuccess: (function(callback) {
return function(response) {
this.parse(response);
if (callback instanceof Function) {
callback(response,this);
}
}.bind(this);
}.bind(this)(options.onSuccess))
});
return this.sync(url, method, this, syncOptions);
},
/**
* Just calls Protobone.Model.sync. If you want your own, Model-specific implementation,
* override this function.
*
* @see Protobone.Model.sync. Also here: inspired by http://backbonejs.org/#Sync
* @method sync
*/
sync: function() {
return statics.sync.apply(statics,arguments);
},
/**
* Called by save() and fetch() with the server's data response. Fills in the
* server response to the model. In the default implementation, it just
* takes the plain JSON object from the server (if any) and store the values on the model.
* if `rootProperty`is set, the data entry point is set to the rootProperty data.
*
* @method parse
*/
parse: function(response) {
if (response && response.responseJSON) {
if (this.rootProperty && response.responseJSON[this.rootProperty]) {
this.set(response.responseJSON[this.rootProperty]);
} else {
this.set(response.responseJSON);
}
}
},
/**
* Checks if the model has a certain attribute.
*
* @method hasAttribute
* @param {String} key The attribute name to check
* @return {Boolean}
*/
hasAttribute: function(key) {
return Object.keys(this._attributes).indexOf(key) >= 0;
}
});
// Adding support for JS Modules through browserify / ES 6:
module.exports = Model;