ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
501 lines (487 loc) • 17.7 kB
JavaScript
(function (enyo) {
//*@protected
/**
We create this reusable object for properties passed to the mixin
method so as not to create and throw away a new object every time
a new model is created.
*/
var _mixinOpts = {ignore: true, filter: function (k, v, s, t) {
// only use the default value if the attributes value is undefined and the default
// entry itself is not undefined
return (typeof t[k] == "undefined") && (typeof v != "undefined");
}};
//*@public
/**
_enyo.Model_ is a kind used to create data records. For the sake of
efficiency and simplicity, it has been designed as a special object not
derived from any other Enyo kind.
__Getting and Setting enyo.Model values__
Unlike kinds based on [enyo.Object](#enyo.Object), any call to _set()_ or
_get()_ on a model affects only the schema of the model, which is tracked in
its _attributes_ hash. That is, when you set a property on a model via
_set()_, you are setting the property's entry in the model's _attributes_
hash, not setting the property on the model itself.
Note that, even though you are changing the contents of the _attributes_
hash, you should not specify _"attributes"_ as a parameter to _set()_ or
_get()_. If you do so, you will create a schema object called _"attributes"_
nested inside the model's _attributes_ hash.
Also note that the _set()_ method has the ability to accept a hash of keys
and values to be applied to the model all at once.
__Computed Properties and enyo.Model__
Computed properties only exist for attributes of a model. Otherwise, they
function just as you would expect from the [ComputedSupport
mixin](#enyo.ComputedSupport) on _enyo.Object_. The only exception is that
all functions in the attributes schema are considered to be computed
properties; these are fairly useless, though, unless you declare their
dependencies.
__Bindings__
Bindings may be applied to _enyo.Model_ instances with the understanding
that they will only be activated via changes to _attributes_.
__Observers and Notifications__
The notification system for observers works the same as it does with
_enyo.Object_, except that observers are only notified of changes made to
properties in the _attributes_ hash.
__Events__
The events in _enyo.Model_ differ from those in
[enyo.Component](#enyo.Component). Instead of _bubbled_ or _waterfall_
events, _enyo.Model_ has _change_ and _destroy_ events. To work with these
events, use the [addListener()](#enyo.RegisteredEventSupport::addListener),
[removeListener()](#enyo.RegisteredEventSupport::removeListener), and
[triggerEvent()](#enyo.RegisteredEventSupport::triggerEvent) methods.
*/
enyo.kind({
name: "enyo.Model",
//*@protected
kind: null,
mixins: [enyo.ObserverSupport, enyo.BindingSupport, enyo.RegisteredEventSupport],
noDefer: true,
//*@public
/**
A hash of attributes known as the record's schema. This is where the
values of any attributes are stored for an active record.
*/
attributes: null,
/**
An optional hash of values and properties to be applied to the attributes
of the record at initialization. Any value in _defaults_ that already
exists on the attributes schema will be ignored.
*/
defaults: {},
/**
Set this flag to true if this model is read-only and will not need to
commit or destroy any changes via a source. This will cause a _destroy()_
call to safely execute _destroyLocal()_ by default.
*/
readOnly: false,
/**
All models have a _store_ reference. You can set this to a specific store
instance in your application or use the default (the _enyo.store_ global).
*/
store: null,
/**
This is the fall-back driver to use when fetching, destroying, or
comitting a model. A driver may always be specified at the time of method
execution, but when it is not specified, the default will be used. This is
a string that should be paired with a known driver for this records store.
*/
defaultSource: "ajax",
/**
An optional array of strings specifying the properties that will be
included in the return values of _raw()_ and _toJSON()_. By default, all
properties in the _attributes_ hash will be included.
*/
includeKeys: null,
/**
Set this property to the URL to be used when generating the request for
this record from any specified source or the _defaultDriver_ of the
record. Note that, by default, the _url_ for a _fetch()_ will have the its
_primaryKey_ appended to the request. Overload the _getUrl()_ method to
extend this behavior. Also see _urlRoot_.
*/
url: "",
/**
For models used outside of collections, an optional base-kind for models
with the same root but different _url_ values. If no _getUrl()_ method is
provided and the _url_ property does not contain a protocol identifier for
the source, it will assume that this value exists and use it as the root
instead.
*/
urlRoot: "",
/**
Boolean value indicating whether a change that needs to be committed has
occurred in the record
*/
dirty: false,
/**
Attribute that, if present in the model, will be used for reference in
_enyo.Collections_ and in the _models_ _store_. It will also be used,
by default, when generating the _url_ for the model. The value of
_primaryKey_ is stored in the _attributes_ hash.
*/
primaryKey: "id",
/**
Set this to an array of keys to use for comparative purposes when using
the _merge_ strategy in the store or any collection.
*/
mergeKeys: null,
/**
An arbitrary, unique value that is assigned to every model. Models may be
requested via this property in collections and the store. Unlike
_primaryKey_, this value is stored on the model and not its _attributes_
hash.
*/
euid: "",
/**
Boolean value indicating whether the record was created locally or is
pulled from a source. You should not modify this value, as this will cause
the source to change its behavior.
*/
isNew: true,
/**
Retrieves the requested model attribute, returning the current value or
undefined. If the attribute is a function, it is assumed to be a computed
property and will be called in the context of the model, with its return
value being returned.
*/
get: function (prop) {
if (this.attributes) {
var fn = this.attributes[prop];
return (fn && "function" == typeof fn)? fn.call(this): fn;
}
},
//*@public
/**
Sets values on specified model attributes. Accepts a single property name
and value or a single hash of _keys_ and _values_ to be set all at once.
Returns the model for chaining. If the attribute being set is a function
in the schema, it will be ignored.
*/
set: function (prop, value, force) {
if (this.attributes) {
if (enyo.isObject(prop)) { return this.setObject(prop); }
var rv = this.attributes[prop],
ch, en;
this._updated = false;
if (rv && "function" == typeof rv) { return this; }
if (force || rv !== value) {
this.previous[prop] = rv;
if (this.computedMap) {
if ((en=this.computedMap[prop])) {
if (typeof en == "string") {
en = this.computedMap[prop] = enyo.trim(en).split(" ");
}
ch = {};
for (var i=0, p; (p=en[i]); ++i) {
this.attributes[prop] = rv;
this.previous[p] = ch[p] = this.get(p);
this.attributes[prop] = value;
this.changed[p] = this.get(p);
this._updated = true;
}
}
}
this.changed[prop] = this.attributes[prop] = value;
this.notifyObservers(prop, rv, value);
this._updated = true;
// if this is a dependent of a computed property we mark that
// as changed as well
if (ch) {
for (var k in ch) {
this.notifyObservers(k, this.previous[k], ch[k]);
}
}
if (!this.isSilenced() && this._updated) {
// note we only clear this here if we are the ones to fire the
// changed event
this._updated = false;
this.triggerEvent("change");
this.changed = {};
}
this.dirty = true;
}
}
return this;
},
/**
A setter that accepts a hash of key/value pairs. Returns the model for
chaining (and consistency with _set()_). All keys in _props_ will be added
to the _attributes_ schema when this method is used.
*/
setObject: function (props) {
if (this.attributes) {
if (props) {
this.stopNotifications();
this.silence();
var updated = false;
for (var k in props) {
this.set(k, props[k]);
updated = updated || this._updated;
}
this.startNotifications();
this.unsilence();
if (updated) {
this._updated = false;
this.triggerEvent("change");
}
this.changed = {};
}
}
return this;
},
/**
While models should normally be instanced using _enyo.store.createRecord()_,
the same applies to the constructor. The first parameter will be used as
the attributes of the model; the optional second parameter will be used as
configuration for the model. Note that _attributes_ being passed into this
method will be passed to the _parse_ method. The options may include overloaded
methods for the kind, but note that since it is done at instantiation, it will
be executed for each new model created this way; it is recommended that you
subkind the base kind instead.
*/
constructor: function (attributes, opts) {
if (opts) { this.importProps(opts); }
this.euid = enyo.uuid();
var a = this.attributes = (this.attributes? enyo.clone(this.attributes): {}),
d = this.defaults,
x = attributes;
if (x) { enyo.mixin(a, this.parse(x)); }
if (d) { enyo.mixin(a, d, _mixinOpts); }
this.changed = {};
// populate the previous property with the actual values as would be expected
// for further updates
this.previous = this.raw();
this.storeChanged();
},
//*@protected
importProps: function (p) {
if (p) {
enyo.kind.statics.extend(p, this);
}
},
//*@public
/**
Produces an immutable hash of the known attributes of this record. If the
_includeKeys_ array exists, it will determine the keys that are included
in the return value; otherwise, all known properties will be included.
*/
raw: function () {
var i = this.includeKeys,
a = this.attributes,
r = i? enyo.only(i, a): enyo.clone(a);
for (var k in r) {
if ("function" == typeof r[k]) {
r[k] = r[k].call(this);
} else if (r[k] instanceof enyo.Collection) {
r[k] = r[k].raw();
}
}
return r;
},
/**
Returns the JSON-stringified version of the output of _raw()_ for this
record.
*/
toJSON: function () {
return enyo.json.stringify(this.raw());
},
/**
By default, uses any _urlRoot_ with the _url_ property; if the record has
a _primaryKey_ value (_"id"_ by default), it will be added at the end.
*/
getUrl: function () {
var pk = this.primaryKey,
id = this.get(pk),
u = this.urlRoot + "/" + this.url;
if (id) {
u += ("/" + id);
}
return u;
},
/**
Commits the current state of the record to either the specified source or
the _records_ default source. The source and any other options may be
specified in the _opts_ hash. You may provide _success_ and _fail_ methods
that will be executed on those conditions. The _success_ method will be
called with the same parameters as the built-in _didCommit()_ method.
*/
commit: function (opts) {
var o = opts? enyo.clone(opts): {};
o.success = enyo.bindSafely(this, "didCommit", this, opts);
o.fail = enyo.bindSafely(this, "didFail", "commit", this, opts);
this.store.commitRecord(this, o);
},
/**
Using the state of the record and any options passed in via the _opts_
hash, tries to fetch the current model attributes from the specified (or
default) source for this record. You may provide _success_ and _fail_
methods that will be executed on those conditions. The _success_ method
will be called with the same parameters as the built-in _didFetch()_ method.
*/
fetch: function (opts) {
var o = opts? enyo.clone(opts): {};
o.success = enyo.bindSafely(this, "didFetch", this, opts);
o.fail = enyo.bindSafely(this, "didFail", "fetch", this, opts);
this.store.fetchRecord(this, o);
},
/**
Requests a _destroy_ action for the given record and the specified (or
default) source in the optional _opts_ hash. You may provide _success_ and
_fail_ methods that will be executed on those conditions. The _success_
method will be called with the same parameters as the built-in
_didDestroy()_ method. If the record is read-only or has its _isNew_ flag
set to true, it will call its synchronous _destroyLocal()_ method instead
and will not use any callbacks.
*/
destroy: function (opts) {
if (this.readOnly || this.isNew) { return this.destroyLocal(); }
var o = opts? enyo.clone(opts): {};
o.success = enyo.bindSafely(this, "didDestroy", this, opts);
o.fail = enyo.bindSafely(this, "didFail", "destroy", this, opts);
this.store.destroyRecord(this, o);
},
/**
Completely removes the record locally without sending a destroy request to
any source. This is the proper method for destroying local-only records.
*/
destroyLocal: function () {
var o = {};
o.success = enyo.bindSafely(this, "didDestroy", this);
this.store.destroyRecordLocal(this, o);
},
/**
Overload this method to change the structure of the data as it is returned
from a _fetch_ or _commit_. By default, just returns the data as it was
retrieved from the source.
*/
parse: function (data) {
return data;
},
/**
When a record is successfully fetched, this method is called before any
user-provided callbacks are executed. It properly inserts the incoming
data into the record and notifies any observers of the properties that
have changed.
*/
didFetch: function (rec, opts, res) {
// the actual result has to be checked post-parse
var r = this.parse(res);
if (r) {
this.setObject(r);
}
// once notifications have taken place we clear the dirty status so the
// state of the model is now clean
this.dirty = false;
// the record can no longer be considered new
this.isNew = false;
if (opts) {
if (opts.success) {
opts.success(rec, opts, res);
}
}
},
/**
When a record is successfully committed, this method is called before any
user-provided callbacks are executed.
*/
didCommit: function (rec, opts, res) {
// the actual result has to be checked post-parse
var r = this.parse(res);
if (r) {
this.setObject(r);
}
// once notifications have taken place we clear the dirty status so the
// state of the model is now clean
this.dirty = false;
// since this was successful this can no longer be considered a new record
this.isNew = false;
if (opts) {
if (opts.success) {
opts.success(rec, opts, res);
}
}
},
/**
When a record is successfully destroyed, this method is called before any
user-provided callbacks are executed.
*/
didDestroy: function (rec, opts, res) {
for (var k in this.attributes) {
if (this.attributes[k] instanceof enyo.Model || this.attributes[k] instanceof enyo.Collection) {
if (this.attributes[k].owner === this) {
this.attributes[k].destroy();
}
}
}
this.triggerEvent("destroy");
this.store._recordDestroyed(this);
this.previous = null;
this.changed = null;
this.defaults = null;
this.includeKeys = null;
this.mergeKeys = null;
this.store = null;
this.destroyed = true;
// we don't call the inherited destroy chain so we do our own cleanup
// to avoid lingering entries
this.removeAllObservers();
this.removeAllListeners();
if (opts && opts.success) {
opts.success(rec, opts, res);
}
},
/**
When a record fails during a request, this method is executed with the
name of the command that failed, followed by a reference to the record,
the original options, and the result (if any).
*/
didFail: function (which, rec, opts, res) {
if (opts && opts.fail) {
opts.fail(rec, opts, res);
}
},
//*@protected
storeChanged: function () {
var s = this.store || enyo.store;
if (s) {
if (enyo.isString(s)) {
s = enyo.getPath(s);
if (!s) {
enyo.warn("enyo.Model: could not find the requested store -> ", this.store, ", using" +
"the default store");
}
}
}
s = this.store = s || enyo.store;
s.addRecord(this);
},
_attributeSpy: function () {
var pkey = this.primaryKey,
prop = arguments[2];
if (pkey == prop) {
// we need to let the store know that our id (unique primaryKey value)
// changed - we do this here so it doesn't need to register a new observer
// for every record created
this.store._recordKeyChanged(this, this.previous[this.primaryKey]);
}
},
observers: {
_attributeSpy: "*"
}
});
//*@protected
enyo.Model.concat = function (ctor, props) {
var p = ctor.prototype || ctor;
if (props.attributes) {
p.attributes = (p.attributes? enyo.mixin(enyo.clone(p.attributes), props.attributes): props.attributes);
delete props.attributes;
}
if (props.defaults) {
p.defaults = (p.defaults? enyo.mixin(enyo.clone(p.defaults), props.defaults): props.defaults);
delete props.defaults;
}
if (props.mergeKeys) {
p.mergeKeys = (p.mergeKeys? enyo.merge(p.mergeKeys, props.mergeKeys): props.mergeKeys.slice());
delete props.mergeKeys;
}
};
})(enyo);