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/
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 // -- Private Methods ----------------------------------------------------
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.
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.
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
964 src: opts.src || null
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.
988 valueFn : 'generateClientId',
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.
1006 @type String|Number|null
1014 }, '3.13.0', {"requires": ["base-build", "escape", "json-parse"]});