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