2
YUI 3.10.3 (build 2fb5187)
3
Copyright 2013 Yahoo! Inc. All rights reserved.
4
Licensed under the BSD License.
5
http://yuilibrary.com/license/
8
YUI.add('model', function (Y, NAME) {
11
Attribute-based data model with APIs for getting, setting, validating, and
12
syncing attribute values, as well as events for being notified of model changes.
20
Attribute-based data model with APIs for getting, setting, validating, and
21
syncing attribute values, as well as events for being notified of model changes.
23
In most cases, you'll want to create your own subclass of `Y.Model` and
24
customize it to meet your needs. In particular, the `sync()` and `validate()`
25
methods are meant to be overridden by custom implementations. You may also want
26
to override the `parse()` method to parse non-generic server responses.
34
var GlobalEnv = YUI.namespace('Env.Model'),
40
Fired when one or more attributes on this model are changed.
43
@param {Object} changed Hash of change information for each attribute that
44
changed. Each item in the hash has the following properties:
45
@param {Any} changed.newVal New value of the attribute.
46
@param {Any} changed.prevVal Previous value of the attribute.
47
@param {String|null} changed.src Source of the change event, if any.
49
EVT_CHANGE = 'change',
52
Fired when an error occurs, such as when the model doesn't validate or when
53
a sync layer response can't be parsed.
56
@param {Any} error Error message, object, or exception generated by the
57
error. Calling `toString()` on this should result in a meaningful error
59
@param {String} src Source of the error. May be one of the following (or any
60
custom error source defined by a Model subclass):
62
* `load`: An error loading the model from a sync layer. The sync layer's
63
response (if any) will be provided as the `response` property on the
66
* `parse`: An error parsing a JSON response. The response in question will
67
be provided as the `response` property on the event facade.
69
* `save`: An error saving the model to a sync layer. The sync layer's
70
response (if any) will be provided as the `response` property on the
73
* `validate`: The model failed to validate. The attributes being validated
74
will be provided as the `attributes` property on the event facade.
79
Fired after model attributes are loaded from a sync layer.
82
@param {Object} parsed The parsed version of the sync layer's response to
84
@param {any} response The sync layer's raw, unparsed response to the load
91
Fired after model attributes are saved to a sync layer.
94
@param {Object} [parsed] The parsed version of the sync layer's response to
95
the save request, if there was a response.
96
@param {any} [response] The sync layer's raw, unparsed response to the save
97
request, if there was one.
103
Model.superclass.constructor.apply(this, arguments);
106
Y.Model = Y.extend(Model, Y.Base, {
107
// -- Public Properties ----------------------------------------------------
110
Hash of attributes that have changed since the last time this model was
119
Name of the attribute to use as the unique id (or primary key) for this
122
The default is `id`, but if your persistence layer uses a different name for
123
the primary key (such as `_id` or `uid`), you can specify that here.
125
The built-in `id` attribute will always be an alias for whatever attribute
126
name you specify here, so getting and setting `id` will always behave the
127
same as getting and setting your custom id attribute.
129
@property idAttribute
136
Hash of attributes that were changed in the last `change` event. Each item
137
in this hash is an object with the following properties:
139
* `newVal`: The new value of the attribute after it changed.
140
* `prevVal`: The old value of the attribute before it changed.
141
* `src`: The source of the change, or `null` if no source was specified.
149
Array of `ModelList` instances that contain this model.
151
When a model is in one or more lists, the model's events will bubble up to
152
those lists. You can subscribe to a model event on a list to be notified
153
when any model in the list fires that event.
155
This property is updated automatically when this model is added to or
156
removed from a `ModelList` instance. You shouldn't alter it manually. When
157
working with models in a list, you should always add and remove models using
158
the list's `add()` and `remove()` methods.
160
@example Subscribing to model events on a list:
162
// Assuming `list` is an existing Y.ModelList instance.
163
list.on('*:change', function (e) {
164
// This function will be called whenever any model in the list
165
// fires a `change` event.
167
// `e.target` will refer to the model instance that fired the
176
// -- Protected Properties -------------------------------------------------
179
This tells `Y.Base` that it should create ad-hoc attributes for config
180
properties passed to Model's constructor. This makes it possible to
181
instantiate a model and set a bunch of attributes without having to subclass
182
`Y.Model` and declare all those attributes first.
184
@property _allowAdHocAttrs
190
_allowAdHocAttrs: true,
193
Total hack to allow us to identify Model instances without using
194
`instanceof`, which won't work when the instance was created in another
195
window or YUI sandbox.
197
@property _isYUIModel
205
// -- Lifecycle Methods ----------------------------------------------------
206
initializer: function (config) {
208
this.lastChange = {};
212
// -- Public Methods -------------------------------------------------------
215
Destroys this model instance and removes it from its containing lists, if
218
The _callback_, if one is provided, will be called after the model is
221
If `options.remove` is `true`, then this method delegates to the `sync()`
222
method to delete the model from the persistence layer, which is an
223
asynchronous action. In this case, the _callback_ (if provided) will be
224
called after the sync layer indicates success or failure of the delete
228
@param {Object} [options] Sync options. It's up to the custom sync
229
implementation to determine what options it supports or requires, if
231
@param {Boolean} [options.remove=false] If `true`, the model will be
232
deleted via the sync layer in addition to the instance being destroyed.
233
@param {callback} [callback] Called after the model has been destroyed (and
234
deleted via the sync layer if `options.remove` is `true`).
235
@param {Error|null} callback.err If an error occurred, this parameter will
236
contain the error. Otherwise _err_ will be `null`.
239
destroy: function (options, callback) {
242
// Allow callback as only arg.
243
if (typeof options === 'function') {
248
self.onceAfter('destroy', function () {
249
function finish(err) {
251
YArray.each(self.lists.concat(), function (list) {
252
list.remove(self, options);
256
callback && callback.apply(null, arguments);
259
if (options && (options.remove || options['delete'])) {
260
self.sync('delete', options, finish);
266
return Model.superclass.destroy.call(self);
270
Returns a clientId string that's unique among all models on the current page
271
(even models in other YUI instances). Uniqueness across pageviews is
274
@method generateClientId
275
@return {String} Unique clientId.
277
generateClientId: function () {
278
GlobalEnv.lastId || (GlobalEnv.lastId = 0);
279
return this.constructor.NAME + '_' + (GlobalEnv.lastId += 1);
283
Returns the value of the specified attribute.
285
If the attribute's value is an object, _name_ may use dot notation to
286
specify the path to a specific property within the object, and the value of
287
that property will be returned.
290
// Set the 'foo' attribute to an object.
297
// Get the value of 'foo'.
299
// => {bar: {baz: 'quux'}}
301
// Get the value of 'foo.bar.baz'.
302
myModel.get('foo.bar.baz');
306
@param {String} name Attribute name or object property path.
307
@return {Any} Attribute value, or `undefined` if the attribute doesn't
311
// get() is defined by Y.Attribute.
314
Returns an HTML-escaped version of the value of the specified string
315
attribute. The value is escaped using `Y.Escape.html()`.
318
@param {String} name Attribute name or object property path.
319
@return {String} HTML-escaped attribute value.
321
getAsHTML: function (name) {
322
var value = this.get(name);
323
return Y.Escape.html(Lang.isValue(value) ? String(value) : '');
327
Returns a URL-encoded version of the value of the specified string
328
attribute. The value is encoded using the native `encodeURIComponent()`
332
@param {String} name Attribute name or object property path.
333
@return {String} URL-encoded attribute value.
335
getAsURL: function (name) {
336
var value = this.get(name);
337
return encodeURIComponent(Lang.isValue(value) ? String(value) : '');
341
Returns `true` if any attribute of this model has been changed since the
342
model was last saved.
344
New models (models for which `isNew()` returns `true`) are implicitly
345
considered to be "modified" until the first time they're saved.
348
@return {Boolean} `true` if this model has changed since it was last saved,
351
isModified: function () {
352
return this.isNew() || !YObject.isEmpty(this.changed);
356
Returns `true` if this model is "new", meaning it hasn't been saved since it
359
Newness is determined by checking whether the model's `id` attribute has
360
been set. An empty id is assumed to indicate a new model, whereas a
361
non-empty id indicates a model that was either loaded or has been saved
362
since it was created.
365
@return {Boolean} `true` if this model is new, `false` otherwise.
368
return !Lang.isValue(this.get('id'));
372
Loads this model from the server.
374
This method delegates to the `sync()` method to perform the actual load
375
operation, which is an asynchronous action. Specify a _callback_ function to
376
be notified of success or failure.
378
A successful load operation will fire a `load` event, while an unsuccessful
379
load operation will fire an `error` event with the `src` value "load".
381
If the load operation succeeds and one or more of the loaded attributes
382
differ from this model's current attributes, a `change` event will be fired.
385
@param {Object} [options] Options to be passed to `sync()` and to `set()`
386
when setting the loaded attributes. It's up to the custom sync
387
implementation to determine what options it supports or requires, if any.
388
@param {callback} [callback] Called when the sync operation finishes.
389
@param {Error|null} callback.err If an error occurred, this parameter will
390
contain the error. If the sync operation succeeded, _err_ will be
392
@param {Any} callback.response The server's response. This value will
393
be passed to the `parse()` method, which is expected to parse it and
394
return an attribute hash.
397
load: function (options, callback) {
400
// Allow callback as only arg.
401
if (typeof options === 'function') {
406
options || (options = {});
408
self.sync('read', options, function (err, response) {
420
self.fire(EVT_ERROR, facade);
423
if (!self._loadEvent) {
424
self._loadEvent = self.publish(EVT_LOAD, {
429
parsed = facade.parsed = self._parse(response);
431
self.setAttrs(parsed, options);
434
self.fire(EVT_LOAD, facade);
437
callback && callback.apply(null, arguments);
444
Called to parse the _response_ when the model is loaded from the server.
445
This method receives a server _response_ and is expected to return an
448
The default implementation assumes that _response_ is either an attribute
449
hash or a JSON string that can be parsed into an attribute hash. If
450
_response_ is a JSON string and either `Y.JSON` or the native `JSON` object
451
are available, it will be parsed automatically. If a parse error occurs, an
452
`error` event will be fired and the model will not be updated.
454
You may override this method to implement custom parsing logic if necessary.
457
@param {Any} response Server response.
458
@return {Object} Attribute hash.
460
parse: function (response) {
461
if (typeof response === 'string') {
463
return Y.JSON.parse(response);
465
this.fire(EVT_ERROR, {
479
Saves this model to the server.
481
This method delegates to the `sync()` method to perform the actual save
482
operation, which is an asynchronous action. Specify a _callback_ function to
483
be notified of success or failure.
485
A successful save operation will fire a `save` event, while an unsuccessful
486
save operation will fire an `error` event with the `src` value "save".
488
If the save operation succeeds and one or more of the attributes returned in
489
the server's response differ from this model's current attributes, a
490
`change` event will be fired.
493
@param {Object} [options] Options to be passed to `sync()` and to `set()`
494
when setting synced attributes. It's up to the custom sync implementation
495
to determine what options it supports or requires, if any.
496
@param {Function} [callback] Called when the sync operation finishes.
497
@param {Error|null} callback.err If an error occurred or validation
498
failed, this parameter will contain the error. If the sync operation
499
succeeded, _err_ will be `null`.
500
@param {Any} callback.response The server's response. This value will
501
be passed to the `parse()` method, which is expected to parse it and
502
return an attribute hash.
505
save: function (options, callback) {
508
// Allow callback as only arg.
509
if (typeof options === 'function') {
514
options || (options = {});
516
self._validate(self.toJSON(), function (err) {
518
callback && callback.call(null, err);
522
self.sync(self.isNew() ? 'create' : 'update', options, function (err, response) {
534
self.fire(EVT_ERROR, facade);
537
if (!self._saveEvent) {
538
self._saveEvent = self.publish(EVT_SAVE, {
544
parsed = facade.parsed = self._parse(response);
545
self.setAttrs(parsed, options);
549
self.fire(EVT_SAVE, facade);
552
callback && callback.apply(null, arguments);
560
Sets the value of a single attribute. If model validation fails, the
561
attribute will not be set and an `error` event will be fired.
563
Use `setAttrs()` to set multiple attributes at once.
566
model.set('foo', 'bar');
569
@param {String} name Attribute name or object property path.
570
@param {any} value Value to set.
571
@param {Object} [options] Data to be mixed into the event facade of the
572
`change` event(s) for these attributes.
573
@param {Boolean} [options.silent=false] If `true`, no `change` event will
577
set: function (name, value, options) {
579
attributes[name] = value;
581
return this.setAttrs(attributes, options);
585
Sets the values of multiple attributes at once. If model validation fails,
586
the attributes will not be set and an `error` event will be fired.
595
@param {Object} attributes Hash of attribute names and values to set.
596
@param {Object} [options] Data to be mixed into the event facade of the
597
`change` event(s) for these attributes.
598
@param {Boolean} [options.silent=false] If `true`, no `change` event will
602
setAttrs: function (attributes, options) {
603
var idAttribute = this.idAttribute,
604
changed, e, key, lastChange, transaction;
606
// Makes a shallow copy of the `options` object before adding the
607
// `_transaction` object to it so we don't modify someone else's object.
608
options = Y.merge(options);
609
transaction = options._transaction = {};
611
// When a custom id attribute is in use, always keep the default `id`
612
// attribute in sync.
613
if (idAttribute !== 'id') {
614
// So we don't modify someone else's object.
615
attributes = Y.merge(attributes);
617
if (YObject.owns(attributes, idAttribute)) {
618
attributes.id = attributes[idAttribute];
619
} else if (YObject.owns(attributes, 'id')) {
620
attributes[idAttribute] = attributes.id;
624
for (key in attributes) {
625
if (YObject.owns(attributes, key)) {
626
this._setAttr(key, attributes[key], options);
630
if (!YObject.isEmpty(transaction)) {
631
changed = this.changed;
632
lastChange = this.lastChange = {};
634
for (key in transaction) {
635
if (YObject.owns(transaction, key)) {
636
e = transaction[key];
638
changed[key] = e.newVal;
648
if (!options.silent) {
649
// Lazy publish for the change event.
650
if (!this._changeEvent) {
651
this._changeEvent = this.publish(EVT_CHANGE, {
656
options.changed = lastChange;
658
this.fire(EVT_CHANGE, options);
666
Override this method to provide a custom persistence implementation for this
667
model. The default just calls the callback without actually doing anything.
669
This method is called internally by `load()`, `save()`, and `destroy()`, and
670
their implementations rely on the callback being called. This effectively
671
means that when a callback is provided, it must be called at some point for
672
the class to operate correctly.
675
@param {String} action Sync action to perform. May be one of the following:
677
* `create`: Store a newly-created model for the first time.
678
* `delete`: Delete an existing model.
679
* `read` : Load an existing model.
680
* `update`: Update an existing model.
682
@param {Object} [options] Sync options. It's up to the custom sync
683
implementation to determine what options it supports or requires, if any.
684
@param {Function} [callback] Called when the sync operation finishes.
685
@param {Error|null} callback.err If an error occurred, this parameter will
686
contain the error. If the sync operation succeeded, _err_ will be
688
@param {Any} [callback.response] The server's response.
690
sync: function (/* action, options, callback */) {
691
var callback = YArray(arguments, 0, true).pop();
693
if (typeof callback === 'function') {
699
Returns a copy of this model's attributes that can be passed to
700
`Y.JSON.stringify()` or used for other nefarious purposes.
702
The `clientId` attribute is not included in the returned object.
704
If you've specified a custom attribute name in the `idAttribute` property,
705
the default `id` attribute will not be included in the returned object.
707
Note: The ECMAScript 5 specification states that objects may implement a
708
`toJSON` method to provide an alternate object representation to serialize
709
when passed to `JSON.stringify(obj)`. This allows class instances to be
710
serialized as if they were plain objects. This is why Model's `toJSON`
711
returns an object, not a JSON string.
713
See <http://es5.github.com/#x15.12.3> for details.
716
@return {Object} Copy of this model's attributes.
718
toJSON: function () {
719
var attrs = this.getAttrs();
721
delete attrs.clientId;
722
delete attrs.destroyed;
723
delete attrs.initialized;
725
if (this.idAttribute !== 'id') {
733
Reverts the last change to the model.
735
If an _attrNames_ array is provided, then only the named attributes will be
736
reverted (and only if they were modified in the previous change). If no
737
_attrNames_ array is provided, then all changed attributes will be reverted
738
to their previous values.
740
Note that only one level of undo is available: from the current state to the
741
previous state. If `undo()` is called when no previous state is available,
742
it will simply do nothing.
745
@param {Array} [attrNames] Array of specific attribute names to revert. If
746
not specified, all attributes modified in the last change will be
748
@param {Object} [options] Data to be mixed into the event facade of the
749
change event(s) for these attributes.
750
@param {Boolean} [options.silent=false] If `true`, no `change` event will
754
undo: function (attrNames, options) {
755
var lastChange = this.lastChange,
756
idAttribute = this.idAttribute,
760
attrNames || (attrNames = YObject.keys(lastChange));
762
YArray.each(attrNames, function (name) {
763
if (YObject.owns(lastChange, name)) {
764
// Don't generate a double change for custom id attributes.
765
name = name === idAttribute ? 'id' : name;
768
toUndo[name] = lastChange[name].prevVal;
772
return needUndo ? this.setAttrs(toUndo, options) : this;
776
Override this method to provide custom validation logic for this model.
778
While attribute-specific validators can be used to validate individual
779
attributes, this method gives you a hook to validate a hash of all
780
attributes before the model is saved. This method is called automatically
781
before `save()` takes any action. If validation fails, the `save()` call
784
In your validation method, call the provided `callback` function with no
785
arguments to indicate success. To indicate failure, pass a single argument,
786
which may contain an error message, an array of error messages, or any other
787
value. This value will be passed along to the `error` event.
791
model.validate = function (attrs, callback) {
792
if (attrs.pie !== true) {
794
callback('Must provide pie.');
803
@param {Object} attrs Attribute hash containing all model attributes to
805
@param {Function} callback Validation callback. Call this function when your
806
validation logic finishes. To trigger a validation failure, pass any
807
value as the first argument to the callback (ideally a meaningful
808
validation error of some kind).
810
@param {Any} [callback.err] Validation error. Don't provide this
811
argument if validation succeeds. If validation fails, set this to an
812
error message or some other meaningful value. It will be passed
813
along to the resulting `error` event.
815
validate: function (attrs, callback) {
816
callback && callback();
819
// -- Protected Methods ----------------------------------------------------
822
Duckpunches the `addAttr` method provided by `Y.Attribute` to keep the
823
`id` attribute’s value and a custom id attribute’s (if provided) value
824
in sync when adding the attributes to the model instance object.
826
Marked as protected to hide it from Model's public API docs, even though
827
this is a public method in Attribute.
830
@param {String} name The name of the attribute.
831
@param {Object} config An object with attribute configuration property/value
832
pairs, specifying the configuration for the attribute.
833
@param {Boolean} lazy (optional) Whether or not to add this attribute lazily
834
(on the first call to get/set).
835
@return {Object} A reference to the host object.
839
addAttr: function (name, config, lazy) {
840
var idAttribute = this.idAttribute,
843
if (idAttribute && name === idAttribute) {
844
idAttrCfg = this._isLazyAttr('id') || this._getAttrCfg('id');
845
id = config.value === config.defaultValue ? null : config.value;
847
if (!Lang.isValue(id)) {
848
// Hunt for the id value.
849
id = idAttrCfg.value === idAttrCfg.defaultValue ? null : idAttrCfg.value;
851
if (!Lang.isValue(id)) {
852
// No id value provided on construction, check defaults.
853
id = Lang.isValue(config.defaultValue) ?
854
config.defaultValue :
855
idAttrCfg.defaultValue;
861
// Make sure `id` is in sync.
862
if (idAttrCfg.value !== id) {
863
idAttrCfg.value = id;
865
if (this._isLazyAttr('id')) {
866
this._state.add('id', 'lazy', idAttrCfg);
868
this._state.add('id', 'value', id);
873
return Model.superclass.addAttr.apply(this, arguments);
877
Calls the public, overrideable `parse()` method and returns the result.
879
Override this method to provide a custom pre-parsing implementation. This
880
provides a hook for custom persistence implementations to "prep" a response
881
before calling the `parse()` method.
884
@param {Any} response Server response.
885
@return {Object} Attribute hash.
890
_parse: function (response) {
891
return this.parse(response);
895
Calls the public, overridable `validate()` method and fires an `error` event
899
@param {Object} attributes Attribute hash.
900
@param {Function} callback Validation callback.
901
@param {Any} [callback.err] Value on failure, non-value on success.
904
_validate: function (attributes, callback) {
907
function handler(err) {
908
if (Lang.isValue(err)) {
909
// Validation failed. Fire an error.
910
self.fire(EVT_ERROR, {
911
attributes: attributes,
923
if (self.validate.length === 1) {
924
// Backcompat for 3.4.x-style synchronous validate() functions that
925
// don't take a callback argument.
926
handler(self.validate(attributes, handler));
928
self.validate(attributes, handler);
932
// -- Protected Event Handlers ---------------------------------------------
935
Duckpunches the `_defAttrChangeFn()` provided by `Y.Attribute` so we can
936
have a single global notification when a change event occurs.
938
@method _defAttrChangeFn
939
@param {EventFacade} e
942
_defAttrChangeFn: function (e) {
943
var attrName = e.attrName;
945
if (!this._setAttrVal(attrName, e.subAttrName, e.prevVal, e.newVal)) {
946
// Prevent "after" listeners from being invoked since nothing changed.
947
e.stopImmediatePropagation();
949
e.newVal = this.get(attrName);
951
if (e._transaction) {
952
e._transaction[attrName] = e;
961
A client-only identifier for this model.
963
Like the `id` attribute, `clientId` may be used to retrieve model
964
instances from lists. Unlike the `id` attribute, `clientId` is
965
automatically generated, and is only intended to be used on the client
966
during the current pageview.
973
valueFn : 'generateClientId',
978
A unique identifier for this model. Among other things, this id may be
979
used to retrieve model instances from lists, so it should be unique.
981
If the id is empty, this model instance is assumed to represent a new
982
item that hasn't yet been saved.
984
If you would prefer to use a custom attribute as this model's id instead
985
of using the `id` attribute (for example, maybe you'd rather use `_id`
986
or `uid` as the primary id), you may set the `idAttribute` property to
987
the name of your custom id attribute. The `id` attribute will then
988
act as an alias for your custom attribute.
991
@type String|Number|null
999
}, '3.10.3', {"requires": ["base-build", "escape", "json-parse"]});