NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / model / model.js
blob62b2f29720136970a0b1d2fb5ef89f20cb348a8e
1 /*
2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
8 YUI.add('model', function (Y, NAME) {
10 /**
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.
14 @module app
15 @submodule model
16 @since 3.4.0
17 **/
19 /**
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.
28 @class Model
29 @constructor
30 @extends Base
31 @since 3.4.0
32 **/
34 var GlobalEnv = YUI.namespace('Env.Model'),
35     Lang      = Y.Lang,
36     YArray    = Y.Array,
37     YObject   = Y.Object,
39     /**
40     Fired when one or more attributes on this model are changed.
42     @event change
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.
48     **/
49     EVT_CHANGE = 'change',
51     /**
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.
55     @event error
56     @param {Any} error Error message, object, or exception generated by the
57       error. Calling `toString()` on this should result in a meaningful error
58       message.
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
64         event facade.
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
71         event facade.
73       * `validate`: The model failed to validate. The attributes being validated
74         will be provided as the `attributes` property on the event facade.
75     **/
76     EVT_ERROR = 'error',
78     /**
79     Fired after model attributes are loaded from a sync layer.
81     @event load
82     @param {Object} parsed The parsed version of the sync layer's response to
83         the load request.
84     @param {any} response The sync layer's raw, unparsed response to the load
85         request.
86     @since 3.5.0
87     **/
88     EVT_LOAD = 'load',
90     /**
91     Fired after model attributes are saved to a sync layer.
93     @event save
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.
98     @since 3.5.0
99     **/
100     EVT_SAVE = 'save';
102 function Model() {
103     Model.superclass.constructor.apply(this, arguments);
106 Y.Model = Y.extend(Model, Y.Base, {
107     // -- Public Properties ----------------------------------------------------
109     /**
110     Hash of attributes that have changed since the last time this model was
111     saved.
113     @property changed
114     @type Object
115     @default {}
116     **/
118     /**
119     Name of the attribute to use as the unique id (or primary key) for this
120     model.
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
130     @type String
131     @default `'id'`
132     **/
133     idAttribute: 'id',
135     /**
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.
143     @property lastChange
144     @type Object
145     @default {}
146     **/
148     /**
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.
166             //
167             // `e.target` will refer to the model instance that fired the
168             // event.
169         });
171     @property lists
172     @type ModelList[]
173     @default `[]`
174     **/
176     // -- Protected Properties -------------------------------------------------
178     /**
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
185     @type Boolean
186     @default true
187     @protected
188     @since 3.5.0
189     **/
190     _allowAdHocAttrs: true,
192     /**
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
198     @type Boolean
199     @default true
200     @protected
201     @since 3.5.0
202     **/
203     _isYUIModel: true,
205     // -- Lifecycle Methods ----------------------------------------------------
206     initializer: function (config) {
207         this.changed    = {};
208         this.lastChange = {};
209         this.lists      = [];
210     },
212     // -- Public Methods -------------------------------------------------------
214     /**
215     Destroys this model instance and removes it from its containing lists, if
216     any.
218     The _callback_, if one is provided, will be called after the model is
219     destroyed.
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
225     operation.
227     @method destroy
228     @param {Object} [options] Sync options. It's up to the custom sync
229         implementation to determine what options it supports or requires, if
230         any.
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`.
237     @chainable
238     **/
239     destroy: function (options, callback) {
240         var self = this;
242         // Allow callback as only arg.
243         if (typeof options === 'function') {
244             callback = options;
245             options  = null;
246         }
248         self.onceAfter('destroy', function () {
249             function finish(err) {
250                 if (!err) {
251                     YArray.each(self.lists.concat(), function (list) {
252                         list.remove(self, options);
253                     });
254                 }
256                 callback && callback.apply(null, arguments);
257             }
259             if (options && (options.remove || options['delete'])) {
260                 self.sync('delete', options, finish);
261             } else {
262                 finish();
263             }
264         });
266         return Model.superclass.destroy.call(self);
267     },
269     /**
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
272     unlikely.
274     @method generateClientId
275     @return {String} Unique clientId.
276     **/
277     generateClientId: function () {
278         GlobalEnv.lastId || (GlobalEnv.lastId = 0);
279         return this.constructor.NAME + '_' + (GlobalEnv.lastId += 1);
280     },
282     /**
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.
289     @example
290         // Set the 'foo' attribute to an object.
291         myModel.set('foo', {
292             bar: {
293                 baz: 'quux'
294             }
295         });
297         // Get the value of 'foo'.
298         myModel.get('foo');
299         // => {bar: {baz: 'quux'}}
301         // Get the value of 'foo.bar.baz'.
302         myModel.get('foo.bar.baz');
303         // => 'quux'
305     @method get
306     @param {String} name Attribute name or object property path.
307     @return {Any} Attribute value, or `undefined` if the attribute doesn't
308       exist.
309     **/
311     // get() is defined by Y.Attribute.
313     /**
314     Returns an HTML-escaped version of the value of the specified string
315     attribute. The value is escaped using `Y.Escape.html()`.
317     @method getAsHTML
318     @param {String} name Attribute name or object property path.
319     @return {String} HTML-escaped attribute value.
320     **/
321     getAsHTML: function (name) {
322         var value = this.get(name);
323         return Y.Escape.html(Lang.isValue(value) ? String(value) : '');
324     },
326     /**
327     Returns a URL-encoded version of the value of the specified string
328     attribute. The value is encoded using the native `encodeURIComponent()`
329     function.
331     @method getAsURL
332     @param {String} name Attribute name or object property path.
333     @return {String} URL-encoded attribute value.
334     **/
335     getAsURL: function (name) {
336         var value = this.get(name);
337         return encodeURIComponent(Lang.isValue(value) ? String(value) : '');
338     },
340     /**
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.
347     @method isModified
348     @return {Boolean} `true` if this model has changed since it was last saved,
349       `false` otherwise.
350     **/
351     isModified: function () {
352         return this.isNew() || !YObject.isEmpty(this.changed);
353     },
355     /**
356     Returns `true` if this model is "new", meaning it hasn't been saved since it
357     was created.
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.
364     @method isNew
365     @return {Boolean} `true` if this model is new, `false` otherwise.
366     **/
367     isNew: function () {
368         return !Lang.isValue(this.get('id'));
369     },
371     /**
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.
384     @method load
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
391         `null`.
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.
395     @chainable
396     **/
397     load: function (options, callback) {
398         var self = this;
400         // Allow callback as only arg.
401         if (typeof options === 'function') {
402             callback = options;
403             options  = {};
404         }
406         options || (options = {});
408         self.sync('read', options, function (err, response) {
409             var facade = {
410                     options : options,
411                     response: response
412                 },
414                 parsed;
416             if (err) {
417                 facade.error = err;
418                 facade.src   = 'load';
420                 self.fire(EVT_ERROR, facade);
421             } else {
422                 // Lazy publish.
423                 if (!self._loadEvent) {
424                     self._loadEvent = self.publish(EVT_LOAD, {
425                         preventable: false
426                     });
427                 }
429                 parsed = facade.parsed = self._parse(response);
431                 self.setAttrs(parsed, options);
432                 self.changed = {};
434                 self.fire(EVT_LOAD, facade);
435             }
437             callback && callback.apply(null, arguments);
438         });
440         return self;
441     },
443     /**
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
446     attribute hash.
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.
456     @method parse
457     @param {Any} response Server response.
458     @return {Object} Attribute hash.
459     **/
460     parse: function (response) {
461         if (typeof response === 'string') {
462             try {
463                 return Y.JSON.parse(response);
464             } catch (ex) {
465                 this.fire(EVT_ERROR, {
466                     error   : ex,
467                     response: response,
468                     src     : 'parse'
469                 });
471                 return null;
472             }
473         }
475         return response;
476     },
478     /**
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.
492     @method save
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.
503     @chainable
504     **/
505     save: function (options, callback) {
506         var self = this;
508         // Allow callback as only arg.
509         if (typeof options === 'function') {
510             callback = options;
511             options  = {};
512         }
514         options || (options = {});
516         self._validate(self.toJSON(), function (err) {
517             if (err) {
518                 callback && callback.call(null, err);
519                 return;
520             }
522             self.sync(self.isNew() ? 'create' : 'update', options, function (err, response) {
523                 var facade = {
524                         options : options,
525                         response: response
526                     },
528                     parsed;
530                 if (err) {
531                     facade.error = err;
532                     facade.src   = 'save';
534                     self.fire(EVT_ERROR, facade);
535                 } else {
536                     // Lazy publish.
537                     if (!self._saveEvent) {
538                         self._saveEvent = self.publish(EVT_SAVE, {
539                             preventable: false
540                         });
541                     }
543                     if (response) {
544                         parsed = facade.parsed = self._parse(response);
545                         self.setAttrs(parsed, options);
546                     }
548                     self.changed = {};
549                     self.fire(EVT_SAVE, facade);
550                 }
552                 callback && callback.apply(null, arguments);
553             });
554         });
556         return self;
557     },
559     /**
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.
565     @example
566         model.set('foo', 'bar');
568     @method set
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
574           be fired.
575     @chainable
576     **/
577     set: function (name, value, options) {
578         var attributes = {};
579         attributes[name] = value;
581         return this.setAttrs(attributes, options);
582     },
584     /**
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.
588     @example
589         model.setAttrs({
590             foo: 'bar',
591             baz: 'quux'
592         });
594     @method setAttrs
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
599           be fired.
600     @chainable
601     **/
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;
621             }
622         }
624         for (key in attributes) {
625             if (YObject.owns(attributes, key)) {
626                 this._setAttr(key, attributes[key], options);
627             }
628         }
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;
640                     lastChange[key] = {
641                         newVal : e.newVal,
642                         prevVal: e.prevVal,
643                         src    : e.src || null
644                     };
645                 }
646             }
648             if (!options.silent) {
649                 // Lazy publish for the change event.
650                 if (!this._changeEvent) {
651                     this._changeEvent = this.publish(EVT_CHANGE, {
652                         preventable: false
653                     });
654                 }
656                 options.changed = lastChange;
658                 this.fire(EVT_CHANGE, options);
659             }
660         }
662         return this;
663     },
665     /**
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.
674     @method sync
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
687         falsy.
688       @param {Any} [callback.response] The server's response.
689     **/
690     sync: function (/* action, options, callback */) {
691         var callback = YArray(arguments, 0, true).pop();
693         if (typeof callback === 'function') {
694             callback();
695         }
696     },
698     /**
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.
715     @method toJSON
716     @return {Object} Copy of this model's attributes.
717     **/
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') {
726             delete attrs.id;
727         }
729         return attrs;
730     },
732     /**
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.
744     @method undo
745     @param {Array} [attrNames] Array of specific attribute names to revert. If
746       not specified, all attributes modified in the last change will be
747       reverted.
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
751           be fired.
752     @chainable
753     **/
754     undo: function (attrNames, options) {
755         var lastChange  = this.lastChange,
756             idAttribute = this.idAttribute,
757             toUndo      = {},
758             needUndo;
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;
767                 needUndo     = true;
768                 toUndo[name] = lastChange[name].prevVal;
769             }
770         });
772         return needUndo ? this.setAttrs(toUndo, options) : this;
773     },
775     /**
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
782     will be aborted.
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.
789     @example
791         model.validate = function (attrs, callback) {
792             if (attrs.pie !== true) {
793                 // No pie?! Invalid!
794                 callback('Must provide pie.');
795                 return;
796             }
798             // Success!
799             callback();
800         };
802     @method validate
803     @param {Object} attrs Attribute hash containing all model attributes to
804         be validated.
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.
814     **/
815     validate: function (attrs, callback) {
816         callback && callback();
817     },
819     // -- Protected Methods ----------------------------------------------------
821     /**
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.
829     @method addAttr
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.
836     @chainable
837     @protected
838     **/
839     addAttr: function (name, config, lazy) {
840         var idAttribute = this.idAttribute,
841             idAttrCfg, id;
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;
856                 }
857             }
859             config.value = id;
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);
867                 } else {
868                     this._state.add('id', 'value', id);
869                 }
870             }
871         }
873         return Model.superclass.addAttr.apply(this, arguments);
874     },
876     /**
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.
883     @method _parse
884     @param {Any} response Server response.
885     @return {Object} Attribute hash.
886     @protected
887     @see Model.parse()
888     @since 3.7.0
889     **/
890     _parse: function (response) {
891         return this.parse(response);
892     },
894     /**
895     Calls the public, overridable `validate()` method and fires an `error` event
896     if validation fails.
898     @method _validate
899     @param {Object} attributes Attribute hash.
900     @param {Function} callback Validation callback.
901         @param {Any} [callback.err] Value on failure, non-value on success.
902     @protected
903     **/
904     _validate: function (attributes, callback) {
905         var self = this;
907         function handler(err) {
908             if (Lang.isValue(err)) {
909                 // Validation failed. Fire an error.
910                 self.fire(EVT_ERROR, {
911                     attributes: attributes,
912                     error     : err,
913                     src       : 'validate'
914                 });
916                 callback(err);
917                 return;
918             }
920             callback();
921         }
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));
927         } else {
928             self.validate(attributes, handler);
929         }
930     },
932     // -- Private Methods ----------------------------------------------------
934     /**
935      Overrides AttributeCore's `_setAttrVal`, to register the changed value if it's part
936      of a Model `setAttrs` transaction.
938      NOTE: AttributeCore's `_setAttrVal` is currently private, but until we support coalesced
939      change events in attribute, we need this override.
941      @method _setAttrVal
942      @private
943      @param {String} attrName The attribute name.
944      @param {String} subAttrName The sub-attribute name, if setting a sub-attribute property ("x.y.z").
945      @param {Any} prevVal The currently stored value of the attribute.
946      @param {Any} newVal The value which is going to be stored.
947      @param {Object} [opts] Optional data providing the circumstances for the change.
948      @param {Object} [attrCfg] Optional config hash for the attribute. This is added for performance along the critical path,
949      where the calling method has already obtained the config from state.
951      @return {boolean} true if the new attribute value was stored, false if not.
952      **/
953     _setAttrVal : function(attrName, subAttrName, prevVal, newVal, opts, attrCfg) {
955         var didChange = Model.superclass._setAttrVal.apply(this, arguments),
956             transaction = opts && opts._transaction,
957             initializing = attrCfg && attrCfg.initializing;
959         // value actually changed inside a model setAttrs transaction
960         if (didChange && transaction && !initializing) {
961             transaction[attrName] = {
962                 newVal: this.get(attrName), // newVal may be impacted by getter
963                 prevVal: prevVal,
964                 src: opts.src || null
965             };
966         }
968         return didChange;
969     }
971 }, {
972     NAME: 'model',
974     ATTRS: {
975         /**
976         A client-only identifier for this model.
978         Like the `id` attribute, `clientId` may be used to retrieve model
979         instances from lists. Unlike the `id` attribute, `clientId` is
980         automatically generated, and is only intended to be used on the client
981         during the current pageview.
983         @attribute clientId
984         @type String
985         @readOnly
986         **/
987         clientId: {
988             valueFn : 'generateClientId',
989             readOnly: true
990         },
992         /**
993         A unique identifier for this model. Among other things, this id may be
994         used to retrieve model instances from lists, so it should be unique.
996         If the id is empty, this model instance is assumed to represent a new
997         item that hasn't yet been saved.
999         If you would prefer to use a custom attribute as this model's id instead
1000         of using the `id` attribute (for example, maybe you'd rather use `_id`
1001         or `uid` as the primary id), you may set the `idAttribute` property to
1002         the name of your custom id attribute. The `id` attribute will then
1003         act as an alias for your custom attribute.
1005         @attribute id
1006         @type String|Number|null
1007         @default `null`
1008         **/
1009         id: {value: null}
1010     }
1014 }, '3.13.0', {"requires": ["base-build", "escape", "json-parse"]});