3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('model', function (Y, NAME) {
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.
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.
33 var GlobalEnv = YUI.namespace('Env.Model'),
39 Fired when one or more attributes on this model are changed.
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.
48 EVT_CHANGE = 'change',
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.
55 @param {Any} error Error message, object, or exception generated by the
56 error. Calling `toString()` on this should result in a meaningful error
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
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
72 * `validate`: The model failed to validate. The attributes being validated
73 will be provided as the `attributes` property on the event facade.
78 Fired after model attributes are loaded from a sync layer.
81 @param {Object} parsed The parsed version of the sync layer's response to
83 @param {any} response The sync layer's raw, unparsed response to the load
90 Fired after model attributes are saved to a sync layer.
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.
102 Model.superclass.constructor.apply(this, arguments);
105 Y.Model = Y.extend(Model, Y.Base, {
106 // -- Public Properties ----------------------------------------------------
109 Hash of attributes that have changed since the last time this model was
118 Name of the attribute to use as the unique id (or primary key) for this
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
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.
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.
166 // `e.target` will refer to the model instance that fired the
175 // -- Protected Properties -------------------------------------------------
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
189 _allowAdHocAttrs: true,
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
204 // -- Lifecycle Methods ----------------------------------------------------
205 initializer: function (config) {
207 this.lastChange = {};
211 // -- Public Methods -------------------------------------------------------
214 Destroys this model instance and removes it from its containing lists, if
217 The _callback_, if one is provided, will be called after the model is
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
227 @param {Object} [options] Sync options. It's up to the custom sync
228 implementation to determine what options it supports or requires, if
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`.
238 destroy: function (options, callback) {
241 // Allow callback as only arg.
242 if (typeof options === 'function') {
247 self.onceAfter('destroy', function () {
248 function finish(err) {
250 YArray.each(self.lists.concat(), function (list) {
251 list.remove(self, options);
255 callback && callback.apply(null, arguments);
258 if (options && (options.remove || options['delete'])) {
259 self.sync('delete', options, finish);
265 return Model.superclass.destroy.call(self);
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
273 @method generateClientId
274 @return {String} Unique clientId.
276 generateClientId: function () {
277 GlobalEnv.lastId || (GlobalEnv.lastId = 0);
278 return this.constructor.NAME + '_' + (GlobalEnv.lastId += 1);
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.
289 // Set the 'foo' attribute to an object.
296 // Get the value of 'foo'.
298 // => {bar: {baz: 'quux'}}
300 // Get the value of 'foo.bar.baz'.
301 myModel.get('foo.bar.baz');
305 @param {String} name Attribute name or object property path.
306 @return {Any} Attribute value, or `undefined` if the attribute doesn't
310 // get() is defined by Y.Attribute.
313 Returns an HTML-escaped version of the value of the specified string
314 attribute. The value is escaped using `Y.Escape.html()`.
317 @param {String} name Attribute name or object property path.
318 @return {String} HTML-escaped attribute value.
320 getAsHTML: function (name) {
321 var value = this.get(name);
322 return Y.Escape.html(Lang.isValue(value) ? String(value) : '');
326 Returns a URL-encoded version of the value of the specified string
327 attribute. The value is encoded using the native `encodeURIComponent()`
331 @param {String} name Attribute name or object property path.
332 @return {String} URL-encoded attribute value.
334 getAsURL: function (name) {
335 var value = this.get(name);
336 return encodeURIComponent(Lang.isValue(value) ? String(value) : '');
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.
347 @return {Boolean} `true` if this model has changed since it was last saved,
350 isModified: function () {
351 return this.isNew() || !YObject.isEmpty(this.changed);
355 Returns `true` if this model is "new", meaning it hasn't been saved since it
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.
364 @return {Boolean} `true` if this model is new, `false` otherwise.
367 return !Lang.isValue(this.get('id'));
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.
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
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.
396 load: function (options, callback) {
399 // Allow callback as only arg.
400 if (typeof options === 'function') {
405 options || (options = {});
407 self.sync('read', options, function (err, response) {
419 self.fire(EVT_ERROR, facade);
422 if (!self._loadEvent) {
423 self._loadEvent = self.publish(EVT_LOAD, {
428 parsed = facade.parsed = self._parse(response);
430 self.setAttrs(parsed, options);
433 self.fire(EVT_LOAD, facade);
436 callback && callback.apply(null, arguments);
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
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.
456 @param {Any} response Server response.
457 @return {Object} Attribute hash.
459 parse: function (response) {
460 if (typeof response === 'string') {
462 return Y.JSON.parse(response);
464 this.fire(EVT_ERROR, {
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.
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.
504 save: function (options, callback) {
507 // Allow callback as only arg.
508 if (typeof options === 'function') {
513 options || (options = {});
515 self._validate(self.toJSON(), function (err) {
517 callback && callback.call(null, err);
521 self.sync(self.isNew() ? 'create' : 'update', options, function (err, response) {
533 self.fire(EVT_ERROR, facade);
536 if (!self._saveEvent) {
537 self._saveEvent = self.publish(EVT_SAVE, {
543 parsed = facade.parsed = self._parse(response);
544 self.setAttrs(parsed, options);
548 self.fire(EVT_SAVE, facade);
551 callback && callback.apply(null, arguments);
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.
565 model.set('foo', 'bar');
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
576 set: function (name, value, options) {
578 attributes[name] = value;
580 return this.setAttrs(attributes, options);
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.
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
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;
621 for (key in attributes) {
622 if (YObject.owns(attributes, key)) {
623 this._setAttr(key, attributes[key], options);
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;
645 if (!options.silent) {
646 // Lazy publish for the change event.
647 if (!this._changeEvent) {
648 this._changeEvent = this.publish(EVT_CHANGE, {
653 this.fire(EVT_CHANGE, Y.merge(options, {changed: lastChange}));
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()`.
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
680 @param {Any} [callback.response] The server's response.
682 sync: function (/* action, options, callback */) {
683 var callback = YArray(arguments, 0, true).pop();
685 if (typeof callback === 'function') {
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.
708 @return {Object} Copy of this model's attributes.
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') {
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.
737 @param {Array} [attrNames] Array of specific attribute names to revert. If
738 not specified, all attributes modified in the last change will be
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
746 undo: function (attrNames, options) {
747 var lastChange = this.lastChange,
748 idAttribute = this.idAttribute,
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;
760 toUndo[name] = lastChange[name].prevVal;
764 return needUndo ? this.setAttrs(toUndo, options) : this;
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
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.
783 model.validate = function (attrs, callback) {
784 if (attrs.pie !== true) {
786 callback('Must provide pie.');
795 @param {Object} attrs Attribute hash containing all model attributes to
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.
807 validate: function (attrs, callback) {
808 callback && callback();
811 // -- Protected Methods ----------------------------------------------------
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.
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.
831 addAttr: function (name, config, lazy) {
832 var idAttribute = this.idAttribute,
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;
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);
860 this._state.add('id', 'value', id);
865 return Model.superclass.addAttr.apply(this, arguments);
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.
876 @param {Any} response Server response.
877 @return {Object} Attribute hash.
882 _parse: function (response) {
883 return this.parse(response);
887 Calls the public, overridable `validate()` method and fires an `error` event
891 @param {Object} attributes Attribute hash.
892 @param {Function} callback Validation callback.
893 @param {Any} [callback.err] Value on failure, non-value on success.
896 _validate: function (attributes, callback) {
899 function handler(err) {
900 if (Lang.isValue(err)) {
901 // Validation failed. Fire an error.
902 self.fire(EVT_ERROR, {
903 attributes: attributes,
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));
920 self.validate(attributes, handler);
924 // -- Protected Event Handlers ---------------------------------------------
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
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();
941 e.newVal = this.get(attrName);
943 if (e._transaction) {
944 e._transaction[attrName] = e;
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.
965 valueFn : 'generateClientId',
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.
983 @type String|Number|null
991 }, '3.7.1', {"requires": ["base-build", "escape", "json-parse"]});