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