3
Copyright 2011 Yahoo! Inc. All rights reserved.
4
Licensed under the BSD License.
5
http://yuilibrary.com/license/
7
YUI.add('model', function(Y) {
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.
18
Attribute-based data model with APIs for getting, setting, validating, and
19
syncing attribute values, as well as events for being notified of model changes.
21
In most cases, you'll want to create your own subclass of `Y.Model` and
22
customize it to meet your needs. In particular, the `sync()` and `validate()`
23
methods are meant to be overridden by custom implementations. You may also want
24
to override the `parse()` method to parse non-generic server responses.
32
var GlobalEnv = YUI.namespace('Env.Model'),
38
Fired when one or more attributes on this model are changed.
41
@param {Object} changed Hash of change information for each attribute that
42
changed. Each item in the hash has the following properties:
43
@param {Any} changed.newVal New value of the attribute.
44
@param {Any} changed.prevVal Previous value of the attribute.
45
@param {String|null} changed.src Source of the change event, if any.
47
EVT_CHANGE = 'change',
50
Fired when an error occurs, such as when the model doesn't validate or when
51
a sync layer response can't be parsed.
54
@param {Any} error Error message, object, or exception generated by the
55
error. Calling `toString()` on this should result in a meaningful error
57
@param {String} src Source of the error. May be one of the following (or any
58
custom error source defined by a Model subclass):
60
* `parse`: An error parsing a JSON response. The response in question will
61
be provided as the `response` property on the event facade.
62
* `validate`: The model failed to validate. The attributes being validated
63
will be provided as the `attributes` property on the event facade.
68
Model.superclass.constructor.apply(this, arguments);
71
Y.Model = Y.extend(Model, Y.Base, {
72
// -- Public Properties ----------------------------------------------------
75
Hash of attributes that have changed since the last time this model was
84
Name of the attribute to use as the unique id (or primary key) for this
87
The default is `id`, but if your persistence layer uses a different name for
88
the primary key (such as `_id` or `uid`), you can specify that here.
90
The built-in `id` attribute will always be an alias for whatever attribute
91
name you specify here, so getting and setting `id` will always behave the
92
same as getting and setting your custom id attribute.
101
Hash of attributes that were changed in the last `change` event. Each item
102
in this hash is an object with the following properties:
104
* `newVal`: The new value of the attribute after it changed.
105
* `prevVal`: The old value of the attribute before it changed.
106
* `src`: The source of the change, or `null` if no source was specified.
114
Array of `ModelList` instances that contain this model.
116
When a model is in one or more lists, the model's events will bubble up to
117
those lists. You can subscribe to a model event on a list to be notified
118
when any model in the list fires that event.
120
This property is updated automatically when this model is added to or
121
removed from a `ModelList` instance. You shouldn't alter it manually. When
122
working with models in a list, you should always add and remove models using
123
the list's `add()` and `remove()` methods.
125
@example Subscribing to model events on a list:
127
// Assuming `list` is an existing Y.ModelList instance.
128
list.on('*:change', function (e) {
129
// This function will be called whenever any model in the list
130
// fires a `change` event.
132
// `e.target` will refer to the model instance that fired the
141
// -- Lifecycle Methods ----------------------------------------------------
142
initializer: function (config) {
144
this.lastChange = {};
148
// -- Public Methods -------------------------------------------------------
151
Destroys this model instance and removes it from its containing lists, if
154
If `options['delete']` is `true`, then this method also delegates to the
155
`sync()` method to delete the model from the persistence layer, which is an
156
asynchronous action. Provide a _callback_ function to be notified of success
160
@param {Object} [options] Sync options. It's up to the custom sync
161
implementation to determine what options it supports or requires, if
163
@param {Boolean} [options.delete=false] If `true`, the model will be
164
deleted via the sync layer in addition to the instance being destroyed.
165
@param {callback} [callback] Called when the sync operation finishes.
166
@param {Error|null} callback.err If an error occurred, this parameter will
167
contain the error. If the sync operation succeeded, _err_ will be
171
destroy: function (options, callback) {
174
// Allow callback as only arg.
175
if (typeof options === 'function') {
180
function finish(err) {
182
YArray.each(self.lists.concat(), function (list) {
183
list.remove(self, options);
186
Model.superclass.destroy.call(self);
189
callback && callback.apply(null, arguments);
192
if (options && options['delete']) {
193
this.sync('delete', options, finish);
202
Returns a clientId string that's unique among all models on the current page
203
(even models in other YUI instances). Uniqueness across pageviews is
206
@method generateClientId
207
@return {String} Unique clientId.
209
generateClientId: function () {
210
GlobalEnv.lastId || (GlobalEnv.lastId = 0);
211
return this.constructor.NAME + '_' + (GlobalEnv.lastId += 1);
215
Returns the value of the specified attribute.
217
If the attribute's value is an object, _name_ may use dot notation to
218
specify the path to a specific property within the object, and the value of
219
that property will be returned.
222
// Set the 'foo' attribute to an object.
229
// Get the value of 'foo'.
231
// => {bar: {baz: 'quux'}}
233
// Get the value of 'foo.bar.baz'.
234
myModel.get('foo.bar.baz');
238
@param {String} name Attribute name or object property path.
239
@return {Any} Attribute value, or `undefined` if the attribute doesn't
243
// get() is defined by Y.Attribute.
246
Returns an HTML-escaped version of the value of the specified string
247
attribute. The value is escaped using `Y.Escape.html()`.
250
@param {String} name Attribute name or object property path.
251
@return {String} HTML-escaped attribute value.
253
getAsHTML: function (name) {
254
var value = this.get(name);
255
return Y.Escape.html(Lang.isValue(value) ? String(value) : '');
259
Returns a URL-encoded version of the value of the specified string
260
attribute. The value is encoded using the native `encodeURIComponent()`
264
@param {String} name Attribute name or object property path.
265
@return {String} URL-encoded attribute value.
267
getAsURL: function (name) {
268
var value = this.get(name);
269
return encodeURIComponent(Lang.isValue(value) ? String(value) : '');
273
Returns `true` if any attribute of this model has been changed since the
274
model was last saved.
276
New models (models for which `isNew()` returns `true`) are implicitly
277
considered to be "modified" until the first time they're saved.
280
@return {Boolean} `true` if this model has changed since it was last saved,
283
isModified: function () {
284
return this.isNew() || !YObject.isEmpty(this.changed);
288
Returns `true` if this model is "new", meaning it hasn't been saved since it
291
Newness is determined by checking whether the model's `id` attribute has
292
been set. An empty id is assumed to indicate a new model, whereas a
293
non-empty id indicates a model that was either loaded or has been saved
294
since it was created.
297
@return {Boolean} `true` if this model is new, `false` otherwise.
300
return !Lang.isValue(this.get('id'));
304
Loads this model from the server.
306
This method delegates to the `sync()` method to perform the actual load
307
operation, which is an asynchronous action. Specify a _callback_ function to
308
be notified of success or failure.
310
If the load operation succeeds and one or more of the loaded attributes
311
differ from this model's current attributes, a `change` event will be fired.
314
@param {Object} [options] Options to be passed to `sync()` and to `set()`
315
when setting the loaded attributes. It's up to the custom sync
316
implementation to determine what options it supports or requires, if any.
317
@param {callback} [callback] Called when the sync operation finishes.
318
@param {Error|null} callback.err If an error occurred, this parameter will
319
contain the error. If the sync operation succeeded, _err_ will be
321
@param {Any} callback.response The server's response. This value will
322
be passed to the `parse()` method, which is expected to parse it and
323
return an attribute hash.
326
load: function (options, callback) {
329
// Allow callback as only arg.
330
if (typeof options === 'function') {
335
this.sync('read', options, function (err, response) {
337
self.setAttrs(self.parse(response), options);
341
callback && callback.apply(null, arguments);
348
Called to parse the _response_ when the model is loaded from the server.
349
This method receives a server _response_ and is expected to return an
352
The default implementation assumes that _response_ is either an attribute
353
hash or a JSON string that can be parsed into an attribute hash. If
354
_response_ is a JSON string and either `Y.JSON` or the native `JSON` object
355
are available, it will be parsed automatically. If a parse error occurs, an
356
`error` event will be fired and the model will not be updated.
358
You may override this method to implement custom parsing logic if necessary.
361
@param {Any} response Server response.
362
@return {Object} Attribute hash.
364
parse: function (response) {
365
if (typeof response === 'string') {
367
return Y.JSON.parse(response);
369
this.fire(EVT_ERROR, {
383
Saves this model to the server.
385
This method delegates to the `sync()` method to perform the actual save
386
operation, which is an asynchronous action. Specify a _callback_ function to
387
be notified of success or failure.
389
If the save operation succeeds and one or more of the attributes returned in
390
the server's response differ from this model's current attributes, a
391
`change` event will be fired.
394
@param {Object} [options] Options to be passed to `sync()` and to `set()`
395
when setting synced attributes. It's up to the custom sync implementation
396
to determine what options it supports or requires, if any.
397
@param {Function} [callback] Called when the sync operation finishes.
398
@param {Error|null} callback.err If an error occurred or validation
399
failed, this parameter will contain the error. If the sync operation
400
succeeded, _err_ will be `null`.
401
@param {Any} callback.response The server's response. This value will
402
be passed to the `parse()` method, which is expected to parse it and
403
return an attribute hash.
406
save: function (options, callback) {
408
validation = self._validate(self.toJSON());
410
// Allow callback as only arg.
411
if (typeof options === 'function') {
416
if (!validation.valid) {
417
callback && callback.call(null, validation.error);
421
self.sync(self.isNew() ? 'create' : 'update', options, function (err, response) {
424
self.setAttrs(self.parse(response), options);
430
callback && callback.apply(null, arguments);
437
Sets the value of a single attribute. If model validation fails, the
438
attribute will not be set and an `error` event will be fired.
440
Use `setAttrs()` to set multiple attributes at once.
443
model.set('foo', 'bar');
446
@param {String} name Attribute name or object property path.
447
@param {any} value Value to set.
448
@param {Object} [options] Data to be mixed into the event facade of the
449
`change` event(s) for these attributes.
450
@param {Boolean} [options.silent=false] If `true`, no `change` event will
454
set: function (name, value, options) {
456
attributes[name] = value;
458
return this.setAttrs(attributes, options);
462
Sets the values of multiple attributes at once. If model validation fails,
463
the attributes will not be set and an `error` event will be fired.
472
@param {Object} attributes Hash of attribute names and values to set.
473
@param {Object} [options] Data to be mixed into the event facade of the
474
`change` event(s) for these attributes.
475
@param {Boolean} [options.silent=false] If `true`, no `change` event will
479
setAttrs: function (attributes, options) {
480
var idAttribute = this.idAttribute,
481
changed, e, key, lastChange, transaction;
483
options || (options = {});
484
transaction = options._transaction = {};
486
// When a custom id attribute is in use, always keep the default `id`
487
// attribute in sync.
488
if (idAttribute !== 'id') {
489
// So we don't modify someone else's object.
490
attributes = Y.merge(attributes);
492
if (YObject.owns(attributes, idAttribute)) {
493
attributes.id = attributes[idAttribute];
494
} else if (YObject.owns(attributes, 'id')) {
495
attributes[idAttribute] = attributes.id;
499
for (key in attributes) {
500
if (YObject.owns(attributes, key)) {
501
this._setAttr(key, attributes[key], options);
505
if (!YObject.isEmpty(transaction)) {
506
changed = this.changed;
507
lastChange = this.lastChange = {};
509
for (key in transaction) {
510
if (YObject.owns(transaction, key)) {
511
e = transaction[key];
513
changed[key] = e.newVal;
523
if (!options.silent) {
524
// Lazy publish for the change event.
525
if (!this._changeEvent) {
526
this._changeEvent = this.publish(EVT_CHANGE, {
531
this.fire(EVT_CHANGE, {changed: lastChange});
539
Override this method to provide a custom persistence implementation for this
540
model. The default just calls the callback without actually doing anything.
542
This method is called internally by `load()`, `save()`, and `destroy()`.
545
@param {String} action Sync action to perform. May be one of the following:
547
* `create`: Store a newly-created model for the first time.
548
* `delete`: Delete an existing model.
549
* `read` : Load an existing model.
550
* `update`: Update an existing model.
552
@param {Object} [options] Sync options. It's up to the custom sync
553
implementation to determine what options it supports or requires, if any.
554
@param {callback} [callback] Called when the sync operation finishes.
555
@param {Error|null} callback.err If an error occurred, this parameter will
556
contain the error. If the sync operation succeeded, _err_ will be
558
@param {Any} [callback.response] The server's response. This value will
559
be passed to the `parse()` method, which is expected to parse it and
560
return an attribute hash.
562
sync: function (/* action, options, callback */) {
563
var callback = YArray(arguments, 0, true).pop();
565
if (typeof callback === 'function') {
571
Returns a copy of this model's attributes that can be passed to
572
`Y.JSON.stringify()` or used for other nefarious purposes.
574
The `clientId` attribute is not included in the returned object.
576
If you've specified a custom attribute name in the `idAttribute` property,
577
the default `id` attribute will not be included in the returned object.
580
@return {Object} Copy of this model's attributes.
582
toJSON: function () {
583
var attrs = this.getAttrs();
585
delete attrs.clientId;
586
delete attrs.destroyed;
587
delete attrs.initialized;
589
if (this.idAttribute !== 'id') {
597
Reverts the last change to the model.
599
If an _attrNames_ array is provided, then only the named attributes will be
600
reverted (and only if they were modified in the previous change). If no
601
_attrNames_ array is provided, then all changed attributes will be reverted
602
to their previous values.
604
Note that only one level of undo is available: from the current state to the
605
previous state. If `undo()` is called when no previous state is available,
606
it will simply do nothing.
609
@param {Array} [attrNames] Array of specific attribute names to revert. If
610
not specified, all attributes modified in the last change will be
612
@param {Object} [options] Data to be mixed into the event facade of the
613
change event(s) for these attributes.
614
@param {Boolean} [options.silent=false] If `true`, no `change` event will
618
undo: function (attrNames, options) {
619
var lastChange = this.lastChange,
620
idAttribute = this.idAttribute,
624
attrNames || (attrNames = YObject.keys(lastChange));
626
YArray.each(attrNames, function (name) {
627
if (YObject.owns(lastChange, name)) {
628
// Don't generate a double change for custom id attributes.
629
name = name === idAttribute ? 'id' : name;
632
toUndo[name] = lastChange[name].prevVal;
636
return needUndo ? this.setAttrs(toUndo, options) : this;
640
Override this method to provide custom validation logic for this model.
641
While attribute-specific validators can be used to validate individual
642
attributes, this method gives you a hook to validate a hash of all
643
attributes before the model is saved. This method is called automatically
644
before `save()` takes any action. If validation fails, the `save()` call
647
A call to `validate` that doesn't return anything (or that returns `null`)
648
will be treated as a success. If the `validate` method returns a value, it
649
will be treated as a failure, and the returned value (which may be a string
650
or an object containing information about the failure) will be passed along
651
to the `error` event.
654
@param {Object} attributes Attribute hash containing all model attributes to
656
@return {Any} Any return value other than `undefined` or `null` will be
657
treated as a validation failure.
659
validate: function (/* attributes */) {},
661
// -- Protected Methods ----------------------------------------------------
664
Duckpunches the `addAttr` method provided by `Y.Attribute` to keep the
665
`id` attribute’s value and a custom id attribute’s (if provided) value
666
in sync when adding the attributes to the model instance object.
668
Marked as protected to hide it from Model's public API docs, even though
669
this is a public method in Attribute.
672
@param {String} name The name of the attribute.
673
@param {Object} config An object with attribute configuration property/value
674
pairs, specifying the configuration for the attribute.
675
@param {boolean} lazy (optional) Whether or not to add this attribute lazily
676
(on the first call to get/set).
677
@return {Object} A reference to the host object.
681
addAttr: function (name, config, lazy) {
682
var idAttribute = this.idAttribute,
685
if (idAttribute && name === idAttribute) {
686
idAttrCfg = this._isLazyAttr('id') || this._getAttrCfg('id');
687
id = config.value === config.defaultValue ? null : config.value;
689
if (!Lang.isValue(id)) {
690
// Hunt for the id value.
691
id = idAttrCfg.value === idAttrCfg.defaultValue ? null : idAttrCfg.value;
693
if (!Lang.isValue(id)) {
694
// No id value provided on construction, check defaults.
695
id = Lang.isValue(config.defaultValue) ?
696
config.defaultValue :
697
idAttrCfg.defaultValue;
703
// Make sure `id` is in sync.
704
if (idAttrCfg.value !== id) {
705
idAttrCfg.value = id;
707
if (this._isLazyAttr('id')) {
708
this._state.add('id', 'lazy', idAttrCfg);
710
this._state.add('id', 'value', id);
715
return Model.superclass.addAttr.apply(this, arguments);
719
Calls the public, overridable `validate()` method and fires an `error` event
723
@param {Object} attributes Attribute hash.
724
@return {Object} Validation results.
727
_validate: function (attributes) {
728
var error = this.validate(attributes);
730
if (Lang.isValue(error)) {
731
// Validation failed. Fire an error.
732
this.fire(EVT_ERROR, {
733
attributes: attributes,
738
return {valid: false, error: error};
741
return {valid: true};
744
// -- Protected Event Handlers ---------------------------------------------
747
Duckpunches the `_defAttrChangeFn()` provided by `Y.Attribute` so we can
748
have a single global notification when a change event occurs.
750
@method _defAttrChangeFn
751
@param {EventFacade} e
754
_defAttrChangeFn: function (e) {
755
var attrName = e.attrName;
757
if (!this._setAttrVal(attrName, e.subAttrName, e.prevVal, e.newVal)) {
758
Y.log('State not updated and stopImmediatePropagation called for attribute: ' + attrName + ' , value:' + e.newVal, 'warn', 'attribute');
759
// Prevent "after" listeners from being invoked since nothing changed.
760
e.stopImmediatePropagation();
762
e.newVal = this.get(attrName);
764
if (e._transaction) {
765
e._transaction[attrName] = e;
774
A client-only identifier for this model.
776
Like the `id` attribute, `clientId` may be used to retrieve model
777
instances from lists. Unlike the `id` attribute, `clientId` is
778
automatically generated, and is only intended to be used on the client
779
during the current pageview.
786
valueFn : 'generateClientId',
791
A unique identifier for this model. Among other things, this id may be
792
used to retrieve model instances from lists, so it should be unique.
794
If the id is empty, this model instance is assumed to represent a new
795
item that hasn't yet been saved.
797
If you would prefer to use a custom attribute as this model's id instead
798
of using the `id` attribute (for example, maybe you'd rather use `_id`
799
or `uid` as the primary id), you may set the `idAttribute` property to
800
the name of your custom id attribute. The `id` attribute will then
801
act as an alias for your custom attribute.
804
@type String|Number|null
812
}, '3.4.1' ,{requires:['base-build', 'escape', 'json-parse']});