weekly release 2.4dev
[moodle.git] / lib / yuilib / 3.7.1 / build / model / model.js
blob6ed8032af170261293e8ccd2a0ad64b7254e0e7a
1 /*
2 YUI 3.7.1 (build 5627)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('model', function (Y, NAME) {
9 /**
10 Attribute-based data model with APIs for getting, setting, validating, and
11 syncing attribute values, as well as events for being notified of model changes.
13 @module app
14 @submodule model
15 @since 3.4.0
16 **/
18 /**
19 Attribute-based data model with APIs for getting, setting, validating, and
20 syncing attribute values, as well as events for being notified of model changes.
22 In most cases, you'll want to create your own subclass of `Y.Model` and
23 customize it to meet your needs. In particular, the `sync()` and `validate()`
24 methods are meant to be overridden by custom implementations. You may also want
25 to override the `parse()` method to parse non-generic server responses.
27 @class Model
28 @constructor
29 @extends Base
30 @since 3.4.0
31 **/
33 var GlobalEnv = YUI.namespace('Env.Model'),
34     Lang      = Y.Lang,
35     YArray    = Y.Array,
36     YObject   = Y.Object,
38     /**
39     Fired when one or more attributes on this model are changed.
41     @event change
42     @param {Object} changed Hash of change information for each attribute that
43         changed. Each item in the hash has the following properties:
44       @param {Any} changed.newVal New value of the attribute.
45       @param {Any} changed.prevVal Previous value of the attribute.
46       @param {String|null} changed.src Source of the change event, if any.
47     **/
48     EVT_CHANGE = 'change',
50     /**
51     Fired when an error occurs, such as when the model doesn't validate or when
52     a sync layer response can't be parsed.
54     @event error
55     @param {Any} error Error message, object, or exception generated by the
56       error. Calling `toString()` on this should result in a meaningful error
57       message.
58     @param {String} src Source of the error. May be one of the following (or any
59       custom error source defined by a Model subclass):
61       * `load`: An error loading the model from a sync layer. The sync layer's
62         response (if any) will be provided as the `response` property on the
63         event facade.
65       * `parse`: An error parsing a JSON response. The response in question will
66         be provided as the `response` property on the event facade.
68       * `save`: An error saving the model to a sync layer. The sync layer's
69         response (if any) will be provided as the `response` property on the
70         event facade.
72       * `validate`: The model failed to validate. The attributes being validated
73         will be provided as the `attributes` property on the event facade.
74     **/
75     EVT_ERROR = 'error',
77     /**
78     Fired after model attributes are loaded from a sync layer.
80     @event load
81     @param {Object} parsed The parsed version of the sync layer's response to
82         the load request.
83     @param {any} response The sync layer's raw, unparsed response to the load
84         request.
85     @since 3.5.0
86     **/
87     EVT_LOAD = 'load',
89     /**
90     Fired after model attributes are saved to a sync layer.
92     @event save
93     @param {Object} [parsed] The parsed version of the sync layer's response to
94         the save request, if there was a response.
95     @param {any} [response] The sync layer's raw, unparsed response to the save
96         request, if there was one.
97     @since 3.5.0
98     **/
99     EVT_SAVE = 'save';
101 function Model() {
102     Model.superclass.constructor.apply(this, arguments);
105 Y.Model = Y.extend(Model, Y.Base, {
106     // -- Public Properties ----------------------------------------------------
108     /**
109     Hash of attributes that have changed since the last time this model was
110     saved.
112     @property changed
113     @type Object
114     @default {}
115     **/
117     /**
118     Name of the attribute to use as the unique id (or primary key) for this
119     model.
121     The default is `id`, but if your persistence layer uses a different name for
122     the primary key (such as `_id` or `uid`), you can specify that here.
124     The built-in `id` attribute will always be an alias for whatever attribute
125     name you specify here, so getting and setting `id` will always behave the
126     same as getting and setting your custom id attribute.
128     @property idAttribute
129     @type String
130     @default `'id'`
131     **/
132     idAttribute: 'id',
134     /**
135     Hash of attributes that were changed in the last `change` event. Each item
136     in this hash is an object with the following properties:
138       * `newVal`: The new value of the attribute after it changed.
139       * `prevVal`: The old value of the attribute before it changed.
140       * `src`: The source of the change, or `null` if no source was specified.
142     @property lastChange
143     @type Object
144     @default {}
145     **/
147     /**
148     Array of `ModelList` instances that contain this model.
150     When a model is in one or more lists, the model's events will bubble up to
151     those lists. You can subscribe to a model event on a list to be notified
152     when any model in the list fires that event.
154     This property is updated automatically when this model is added to or
155     removed from a `ModelList` instance. You shouldn't alter it manually. When
156     working with models in a list, you should always add and remove models using
157     the list's `add()` and `remove()` methods.
159     @example Subscribing to model events on a list:
161         // Assuming `list` is an existing Y.ModelList instance.
162         list.on('*:change', function (e) {
163             // This function will be called whenever any model in the list
164             // fires a `change` event.
165             //
166             // `e.target` will refer to the model instance that fired the
167             // event.
168         });
170     @property lists
171     @type ModelList[]
172     @default `[]`
173     **/
175     // -- Protected Properties -------------------------------------------------
177     /**
178     This tells `Y.Base` that it should create ad-hoc attributes for config
179     properties passed to Model's constructor. This makes it possible to
180     instantiate a model and set a bunch of attributes without having to subclass
181     `Y.Model` and declare all those attributes first.
183     @property _allowAdHocAttrs
184     @type Boolean
185     @default true
186     @protected
187     @since 3.5.0
188     **/
189     _allowAdHocAttrs: true,
191     /**
192     Total hack to allow us to identify Model instances without using
193     `instanceof`, which won't work when the instance was created in another
194     window or YUI sandbox.
196     @property _isYUIModel
197     @type Boolean
198     @default true
199     @protected
200     @since 3.5.0
201     **/
202     _isYUIModel: true,
204     // -- Lifecycle Methods ----------------------------------------------------
205     initializer: function (config) {
206         this.changed    = {};
207         this.lastChange = {};
208         this.lists      = [];
209     },
211     // -- Public Methods -------------------------------------------------------
213     /**
214     Destroys this model instance and removes it from its containing lists, if
215     any.
217     The _callback_, if one is provided, will be called after the model is
218     destroyed.
220     If `options.remove` is `true`, then this method delegates to the `sync()`
221     method to delete the model from the persistence layer, which is an
222     asynchronous action. In this case, the _callback_ (if provided) will be
223     called after the sync layer indicates success or failure of the delete
224     operation.
226     @method destroy
227     @param {Object} [options] Sync options. It's up to the custom sync
228         implementation to determine what options it supports or requires, if
229         any.
230       @param {Boolean} [options.remove=false] If `true`, the model will be
231         deleted via the sync layer in addition to the instance being destroyed.
232     @param {callback} [callback] Called after the model has been destroyed (and
233         deleted via the sync layer if `options.remove` is `true`).
234       @param {Error|null} callback.err If an error occurred, this parameter will
235         contain the error. Otherwise _err_ will be `null`.
236     @chainable
237     **/
238     destroy: function (options, callback) {
239         var self = this;
241         // Allow callback as only arg.
242         if (typeof options === 'function') {
243             callback = options;
244             options  = null;
245         }
247         self.onceAfter('destroy', function () {
248             function finish(err) {
249                 if (!err) {
250                     YArray.each(self.lists.concat(), function (list) {
251                         list.remove(self, options);
252                     });
253                 }
255                 callback && callback.apply(null, arguments);
256             }
258             if (options && (options.remove || options['delete'])) {
259                 self.sync('delete', options, finish);
260             } else {
261                 finish();
262             }
263         });
265         return Model.superclass.destroy.call(self);
266     },
268     /**
269     Returns a clientId string that's unique among all models on the current page
270     (even models in other YUI instances). Uniqueness across pageviews is
271     unlikely.
273     @method generateClientId
274     @return {String} Unique clientId.
275     **/
276     generateClientId: function () {
277         GlobalEnv.lastId || (GlobalEnv.lastId = 0);
278         return this.constructor.NAME + '_' + (GlobalEnv.lastId += 1);
279     },
281     /**
282     Returns the value of the specified attribute.
284     If the attribute's value is an object, _name_ may use dot notation to
285     specify the path to a specific property within the object, and the value of
286     that property will be returned.
288     @example
289         // Set the 'foo' attribute to an object.
290         myModel.set('foo', {
291             bar: {
292                 baz: 'quux'
293             }
294         });
296         // Get the value of 'foo'.
297         myModel.get('foo');
298         // => {bar: {baz: 'quux'}}
300         // Get the value of 'foo.bar.baz'.
301         myModel.get('foo.bar.baz');
302         // => 'quux'
304     @method get
305     @param {String} name Attribute name or object property path.
306     @return {Any} Attribute value, or `undefined` if the attribute doesn't
307       exist.
308     **/
310     // get() is defined by Y.Attribute.
312     /**
313     Returns an HTML-escaped version of the value of the specified string
314     attribute. The value is escaped using `Y.Escape.html()`.
316     @method getAsHTML
317     @param {String} name Attribute name or object property path.
318     @return {String} HTML-escaped attribute value.
319     **/
320     getAsHTML: function (name) {
321         var value = this.get(name);
322         return Y.Escape.html(Lang.isValue(value) ? String(value) : '');
323     },
325     /**
326     Returns a URL-encoded version of the value of the specified string
327     attribute. The value is encoded using the native `encodeURIComponent()`
328     function.
330     @method getAsURL
331     @param {String} name Attribute name or object property path.
332     @return {String} URL-encoded attribute value.
333     **/
334     getAsURL: function (name) {
335         var value = this.get(name);
336         return encodeURIComponent(Lang.isValue(value) ? String(value) : '');
337     },
339     /**
340     Returns `true` if any attribute of this model has been changed since the
341     model was last saved.
343     New models (models for which `isNew()` returns `true`) are implicitly
344     considered to be "modified" until the first time they're saved.
346     @method isModified
347     @return {Boolean} `true` if this model has changed since it was last saved,
348       `false` otherwise.
349     **/
350     isModified: function () {
351         return this.isNew() || !YObject.isEmpty(this.changed);
352     },
354     /**
355     Returns `true` if this model is "new", meaning it hasn't been saved since it
356     was created.
358     Newness is determined by checking whether the model's `id` attribute has
359     been set. An empty id is assumed to indicate a new model, whereas a
360     non-empty id indicates a model that was either loaded or has been saved
361     since it was created.
363     @method isNew
364     @return {Boolean} `true` if this model is new, `false` otherwise.
365     **/
366     isNew: function () {
367         return !Lang.isValue(this.get('id'));
368     },
370     /**
371     Loads this model from the server.
373     This method delegates to the `sync()` method to perform the actual load
374     operation, which is an asynchronous action. Specify a _callback_ function to
375     be notified of success or failure.
377     A successful load operation will fire a `load` event, while an unsuccessful
378     load operation will fire an `error` event with the `src` value "load".
380     If the load operation succeeds and one or more of the loaded attributes
381     differ from this model's current attributes, a `change` event will be fired.
383     @method load
384     @param {Object} [options] Options to be passed to `sync()` and to `set()`
385       when setting the loaded attributes. It's up to the custom sync
386       implementation to determine what options it supports or requires, if any.
387     @param {callback} [callback] Called when the sync operation finishes.
388       @param {Error|null} callback.err If an error occurred, this parameter will
389         contain the error. If the sync operation succeeded, _err_ will be
390         `null`.
391       @param {Any} callback.response The server's response. This value will
392         be passed to the `parse()` method, which is expected to parse it and
393         return an attribute hash.
394     @chainable
395     **/
396     load: function (options, callback) {
397         var self = this;
399         // Allow callback as only arg.
400         if (typeof options === 'function') {
401             callback = options;
402             options  = {};
403         }
405         options || (options = {});
407         self.sync('read', options, function (err, response) {
408             var facade = {
409                     options : options,
410                     response: response
411                 },
413                 parsed;
415             if (err) {
416                 facade.error = err;
417                 facade.src   = 'load';
419                 self.fire(EVT_ERROR, facade);
420             } else {
421                 // Lazy publish.
422                 if (!self._loadEvent) {
423                     self._loadEvent = self.publish(EVT_LOAD, {
424                         preventable: false
425                     });
426                 }
428                 parsed = facade.parsed = self._parse(response);
430                 self.setAttrs(parsed, options);
431                 self.changed = {};
433                 self.fire(EVT_LOAD, facade);
434             }
436             callback && callback.apply(null, arguments);
437         });
439         return self;
440     },
442     /**
443     Called to parse the _response_ when the model is loaded from the server.
444     This method receives a server _response_ and is expected to return an
445     attribute hash.
447     The default implementation assumes that _response_ is either an attribute
448     hash or a JSON string that can be parsed into an attribute hash. If
449     _response_ is a JSON string and either `Y.JSON` or the native `JSON` object
450     are available, it will be parsed automatically. If a parse error occurs, an
451     `error` event will be fired and the model will not be updated.
453     You may override this method to implement custom parsing logic if necessary.
455     @method parse
456     @param {Any} response Server response.
457     @return {Object} Attribute hash.
458     **/
459     parse: function (response) {
460         if (typeof response === 'string') {
461             try {
462                 return Y.JSON.parse(response);
463             } catch (ex) {
464                 this.fire(EVT_ERROR, {
465                     error   : ex,
466                     response: response,
467                     src     : 'parse'
468                 });
470                 return null;
471             }
472         }
474         return response;
475     },
477     /**
478     Saves this model to the server.
480     This method delegates to the `sync()` method to perform the actual save
481     operation, which is an asynchronous action. Specify a _callback_ function to
482     be notified of success or failure.
484     A successful save operation will fire a `save` event, while an unsuccessful
485     save operation will fire an `error` event with the `src` value "save".
487     If the save operation succeeds and one or more of the attributes returned in
488     the server's response differ from this model's current attributes, a
489     `change` event will be fired.
491     @method save
492     @param {Object} [options] Options to be passed to `sync()` and to `set()`
493       when setting synced attributes. It's up to the custom sync implementation
494       to determine what options it supports or requires, if any.
495     @param {Function} [callback] Called when the sync operation finishes.
496       @param {Error|null} callback.err If an error occurred or validation
497         failed, this parameter will contain the error. If the sync operation
498         succeeded, _err_ will be `null`.
499       @param {Any} callback.response The server's response. This value will
500         be passed to the `parse()` method, which is expected to parse it and
501         return an attribute hash.
502     @chainable
503     **/
504     save: function (options, callback) {
505         var self = this;
507         // Allow callback as only arg.
508         if (typeof options === 'function') {
509             callback = options;
510             options  = {};
511         }
513         options || (options = {});
515         self._validate(self.toJSON(), function (err) {
516             if (err) {
517                 callback && callback.call(null, err);
518                 return;
519             }
521             self.sync(self.isNew() ? 'create' : 'update', options, function (err, response) {
522                 var facade = {
523                         options : options,
524                         response: response
525                     },
527                     parsed;
529                 if (err) {
530                     facade.error = err;
531                     facade.src   = 'save';
533                     self.fire(EVT_ERROR, facade);
534                 } else {
535                     // Lazy publish.
536                     if (!self._saveEvent) {
537                         self._saveEvent = self.publish(EVT_SAVE, {
538                             preventable: false
539                         });
540                     }
542                     if (response) {
543                         parsed = facade.parsed = self._parse(response);
544                         self.setAttrs(parsed, options);
545                     }
547                     self.changed = {};
548                     self.fire(EVT_SAVE, facade);
549                 }
551                 callback && callback.apply(null, arguments);
552             });
553         });
555         return self;
556     },
558     /**
559     Sets the value of a single attribute. If model validation fails, the
560     attribute will not be set and an `error` event will be fired.
562     Use `setAttrs()` to set multiple attributes at once.
564     @example
565         model.set('foo', 'bar');
567     @method set
568     @param {String} name Attribute name or object property path.
569     @param {any} value Value to set.
570     @param {Object} [options] Data to be mixed into the event facade of the
571         `change` event(s) for these attributes.
572       @param {Boolean} [options.silent=false] If `true`, no `change` event will
573           be fired.
574     @chainable
575     **/
576     set: function (name, value, options) {
577         var attributes = {};
578         attributes[name] = value;
580         return this.setAttrs(attributes, options);
581     },
583     /**
584     Sets the values of multiple attributes at once. If model validation fails,
585     the attributes will not be set and an `error` event will be fired.
587     @example
588         model.setAttrs({
589             foo: 'bar',
590             baz: 'quux'
591         });
593     @method setAttrs
594     @param {Object} attributes Hash of attribute names and values to set.
595     @param {Object} [options] Data to be mixed into the event facade of the
596         `change` event(s) for these attributes.
597       @param {Boolean} [options.silent=false] If `true`, no `change` event will
598           be fired.
599     @chainable
600     **/
601     setAttrs: function (attributes, options) {
602         var idAttribute = this.idAttribute,
603             changed, e, key, lastChange, transaction;
605         options || (options = {});
606         transaction = options._transaction = {};
608         // When a custom id attribute is in use, always keep the default `id`
609         // attribute in sync.
610         if (idAttribute !== 'id') {
611             // So we don't modify someone else's object.
612             attributes = Y.merge(attributes);
614             if (YObject.owns(attributes, idAttribute)) {
615                 attributes.id = attributes[idAttribute];
616             } else if (YObject.owns(attributes, 'id')) {
617                 attributes[idAttribute] = attributes.id;
618             }
619         }
621         for (key in attributes) {
622             if (YObject.owns(attributes, key)) {
623                 this._setAttr(key, attributes[key], options);
624             }
625         }
627         if (!YObject.isEmpty(transaction)) {
628             changed    = this.changed;
629             lastChange = this.lastChange = {};
631             for (key in transaction) {
632                 if (YObject.owns(transaction, key)) {
633                     e = transaction[key];
635                     changed[key] = e.newVal;
637                     lastChange[key] = {
638                         newVal : e.newVal,
639                         prevVal: e.prevVal,
640                         src    : e.src || null
641                     };
642                 }
643             }
645             if (!options.silent) {
646                 // Lazy publish for the change event.
647                 if (!this._changeEvent) {
648                     this._changeEvent = this.publish(EVT_CHANGE, {
649                         preventable: false
650                     });
651                 }
653                 this.fire(EVT_CHANGE, Y.merge(options, {changed: lastChange}));
654             }
655         }
657         return this;
658     },
660     /**
661     Override this method to provide a custom persistence implementation for this
662     model. The default just calls the callback without actually doing anything.
664     This method is called internally by `load()`, `save()`, and `destroy()`.
666     @method sync
667     @param {String} action Sync action to perform. May be one of the following:
669       * `create`: Store a newly-created model for the first time.
670       * `delete`: Delete an existing model.
671       * `read`  : Load an existing model.
672       * `update`: Update an existing model.
674     @param {Object} [options] Sync options. It's up to the custom sync
675       implementation to determine what options it supports or requires, if any.
676     @param {Function} [callback] Called when the sync operation finishes.
677       @param {Error|null} callback.err If an error occurred, this parameter will
678         contain the error. If the sync operation succeeded, _err_ will be
679         falsy.
680       @param {Any} [callback.response] The server's response.
681     **/
682     sync: function (/* action, options, callback */) {
683         var callback = YArray(arguments, 0, true).pop();
685         if (typeof callback === 'function') {
686             callback();
687         }
688     },
690     /**
691     Returns a copy of this model's attributes that can be passed to
692     `Y.JSON.stringify()` or used for other nefarious purposes.
694     The `clientId` attribute is not included in the returned object.
696     If you've specified a custom attribute name in the `idAttribute` property,
697     the default `id` attribute will not be included in the returned object.
699     Note: The ECMAScript 5 specification states that objects may implement a
700     `toJSON` method to provide an alternate object representation to serialize
701     when passed to `JSON.stringify(obj)`.  This allows class instances to be
702     serialized as if they were plain objects.  This is why Model's `toJSON`
703     returns an object, not a JSON string.
705     See <http://es5.github.com/#x15.12.3> for details.
707     @method toJSON
708     @return {Object} Copy of this model's attributes.
709     **/
710     toJSON: function () {
711         var attrs = this.getAttrs();
713         delete attrs.clientId;
714         delete attrs.destroyed;
715         delete attrs.initialized;
717         if (this.idAttribute !== 'id') {
718             delete attrs.id;
719         }
721         return attrs;
722     },
724     /**
725     Reverts the last change to the model.
727     If an _attrNames_ array is provided, then only the named attributes will be
728     reverted (and only if they were modified in the previous change). If no
729     _attrNames_ array is provided, then all changed attributes will be reverted
730     to their previous values.
732     Note that only one level of undo is available: from the current state to the
733     previous state. If `undo()` is called when no previous state is available,
734     it will simply do nothing.
736     @method undo
737     @param {Array} [attrNames] Array of specific attribute names to revert. If
738       not specified, all attributes modified in the last change will be
739       reverted.
740     @param {Object} [options] Data to be mixed into the event facade of the
741         change event(s) for these attributes.
742       @param {Boolean} [options.silent=false] If `true`, no `change` event will
743           be fired.
744     @chainable
745     **/
746     undo: function (attrNames, options) {
747         var lastChange  = this.lastChange,
748             idAttribute = this.idAttribute,
749             toUndo      = {},
750             needUndo;
752         attrNames || (attrNames = YObject.keys(lastChange));
754         YArray.each(attrNames, function (name) {
755             if (YObject.owns(lastChange, name)) {
756                 // Don't generate a double change for custom id attributes.
757                 name = name === idAttribute ? 'id' : name;
759                 needUndo     = true;
760                 toUndo[name] = lastChange[name].prevVal;
761             }
762         });
764         return needUndo ? this.setAttrs(toUndo, options) : this;
765     },
767     /**
768     Override this method to provide custom validation logic for this model.
770     While attribute-specific validators can be used to validate individual
771     attributes, this method gives you a hook to validate a hash of all
772     attributes before the model is saved. This method is called automatically
773     before `save()` takes any action. If validation fails, the `save()` call
774     will be aborted.
776     In your validation method, call the provided `callback` function with no
777     arguments to indicate success. To indicate failure, pass a single argument,
778     which may contain an error message, an array of error messages, or any other
779     value. This value will be passed along to the `error` event.
781     @example
783         model.validate = function (attrs, callback) {
784             if (attrs.pie !== true) {
785                 // No pie?! Invalid!
786                 callback('Must provide pie.');
787                 return;
788             }
790             // Success!
791             callback();
792         };
794     @method validate
795     @param {Object} attrs Attribute hash containing all model attributes to
796         be validated.
797     @param {Function} callback Validation callback. Call this function when your
798         validation logic finishes. To trigger a validation failure, pass any
799         value as the first argument to the callback (ideally a meaningful
800         validation error of some kind).
802         @param {Any} [callback.err] Validation error. Don't provide this
803             argument if validation succeeds. If validation fails, set this to an
804             error message or some other meaningful value. It will be passed
805             along to the resulting `error` event.
806     **/
807     validate: function (attrs, callback) {
808         callback && callback();
809     },
811     // -- Protected Methods ----------------------------------------------------
813     /**
814     Duckpunches the `addAttr` method provided by `Y.Attribute` to keep the
815     `id` attribute’s value and a custom id attribute’s (if provided) value
816     in sync when adding the attributes to the model instance object.
818     Marked as protected to hide it from Model's public API docs, even though
819     this is a public method in Attribute.
821     @method addAttr
822     @param {String} name The name of the attribute.
823     @param {Object} config An object with attribute configuration property/value
824       pairs, specifying the configuration for the attribute.
825     @param {Boolean} lazy (optional) Whether or not to add this attribute lazily
826       (on the first call to get/set).
827     @return {Object} A reference to the host object.
828     @chainable
829     @protected
830     **/
831     addAttr: function (name, config, lazy) {
832         var idAttribute = this.idAttribute,
833             idAttrCfg, id;
835         if (idAttribute && name === idAttribute) {
836             idAttrCfg = this._isLazyAttr('id') || this._getAttrCfg('id');
837             id        = config.value === config.defaultValue ? null : config.value;
839             if (!Lang.isValue(id)) {
840                 // Hunt for the id value.
841                 id = idAttrCfg.value === idAttrCfg.defaultValue ? null : idAttrCfg.value;
843                 if (!Lang.isValue(id)) {
844                     // No id value provided on construction, check defaults.
845                     id = Lang.isValue(config.defaultValue) ?
846                         config.defaultValue :
847                         idAttrCfg.defaultValue;
848                 }
849             }
851             config.value = id;
853             // Make sure `id` is in sync.
854             if (idAttrCfg.value !== id) {
855                 idAttrCfg.value = id;
857                 if (this._isLazyAttr('id')) {
858                     this._state.add('id', 'lazy', idAttrCfg);
859                 } else {
860                     this._state.add('id', 'value', id);
861                 }
862             }
863         }
865         return Model.superclass.addAttr.apply(this, arguments);
866     },
868     /**
869     Calls the public, overrideable `parse()` method and returns the result.
871     Override this method to provide a custom pre-parsing implementation. This
872     provides a hook for custom persistence implementations to "prep" a response
873     before calling the `parse()` method.
875     @method _parse
876     @param {Any} response Server response.
877     @return {Object} Attribute hash.
878     @protected
879     @see Model.parse()
880     @since 3.7.0
881     **/
882     _parse: function (response) {
883         return this.parse(response);
884     },
886     /**
887     Calls the public, overridable `validate()` method and fires an `error` event
888     if validation fails.
890     @method _validate
891     @param {Object} attributes Attribute hash.
892     @param {Function} callback Validation callback.
893         @param {Any} [callback.err] Value on failure, non-value on success.
894     @protected
895     **/
896     _validate: function (attributes, callback) {
897         var self = this;
899         function handler(err) {
900             if (Lang.isValue(err)) {
901                 // Validation failed. Fire an error.
902                 self.fire(EVT_ERROR, {
903                     attributes: attributes,
904                     error     : err,
905                     src       : 'validate'
906                 });
908                 callback(err);
909                 return;
910             }
912             callback();
913         }
915         if (self.validate.length === 1) {
916             // Backcompat for 3.4.x-style synchronous validate() functions that
917             // don't take a callback argument.
918             handler(self.validate(attributes, handler));
919         } else {
920             self.validate(attributes, handler);
921         }
922     },
924     // -- Protected Event Handlers ---------------------------------------------
926     /**
927     Duckpunches the `_defAttrChangeFn()` provided by `Y.Attribute` so we can
928     have a single global notification when a change event occurs.
930     @method _defAttrChangeFn
931     @param {EventFacade} e
932     @protected
933     **/
934     _defAttrChangeFn: function (e) {
935         var attrName = e.attrName;
937         if (!this._setAttrVal(attrName, e.subAttrName, e.prevVal, e.newVal)) {
938             // Prevent "after" listeners from being invoked since nothing changed.
939             e.stopImmediatePropagation();
940         } else {
941             e.newVal = this.get(attrName);
943             if (e._transaction) {
944                 e._transaction[attrName] = e;
945             }
946         }
947     }
948 }, {
949     NAME: 'model',
951     ATTRS: {
952         /**
953         A client-only identifier for this model.
955         Like the `id` attribute, `clientId` may be used to retrieve model
956         instances from lists. Unlike the `id` attribute, `clientId` is
957         automatically generated, and is only intended to be used on the client
958         during the current pageview.
960         @attribute clientId
961         @type String
962         @readOnly
963         **/
964         clientId: {
965             valueFn : 'generateClientId',
966             readOnly: true
967         },
969         /**
970         A unique identifier for this model. Among other things, this id may be
971         used to retrieve model instances from lists, so it should be unique.
973         If the id is empty, this model instance is assumed to represent a new
974         item that hasn't yet been saved.
976         If you would prefer to use a custom attribute as this model's id instead
977         of using the `id` attribute (for example, maybe you'd rather use `_id`
978         or `uid` as the primary id), you may set the `idAttribute` property to
979         the name of your custom id attribute. The `id` attribute will then
980         act as an alias for your custom attribute.
982         @attribute id
983         @type String|Number|null
984         @default `null`
985         **/
986         id: {value: null}
987     }
991 }, '3.7.1', {"requires": ["base-build", "escape", "json-parse"]});