Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
            handler(self.validate(attributes, handler));
920
        } else {
921
            self.validate(attributes, handler);
922
        }
923
    },
924
 
925
    // -- Private Methods ----------------------------------------------------
926
 
927
    /**
928
     Overrides AttributeCore's `_setAttrVal`, to register the changed value if it's part
929
     of a Model `setAttrs` transaction.
930
 
931
     NOTE: AttributeCore's `_setAttrVal` is currently private, but until we support coalesced
932
     change events in attribute, we need this override.
933
 
934
     @method _setAttrVal
935
     @private
936
     @param {String} attrName The attribute name.
937
     @param {String} subAttrName The sub-attribute name, if setting a sub-attribute property ("x.y.z").
938
     @param {Any} prevVal The currently stored value of the attribute.
939
     @param {Any} newVal The value which is going to be stored.
940
     @param {Object} [opts] Optional data providing the circumstances for the change.
941
     @param {Object} [attrCfg] Optional config hash for the attribute. This is added for performance along the critical path,
942
     where the calling method has already obtained the config from state.
943
 
944
     @return {boolean} true if the new attribute value was stored, false if not.
945
     **/
946
    _setAttrVal : function(attrName, subAttrName, prevVal, newVal, opts, attrCfg) {
947
 
948
        var didChange = Model.superclass._setAttrVal.apply(this, arguments),
949
            transaction = opts && opts._transaction,
950
            initializing = attrCfg && attrCfg.initializing;
951
 
952
        // value actually changed inside a model setAttrs transaction
953
        if (didChange && transaction && !initializing) {
954
            transaction[attrName] = {
955
                newVal: this.get(attrName), // newVal may be impacted by getter
956
                prevVal: prevVal,
957
                src: opts.src || null
958
            };
959
        }
960
 
961
        return didChange;
962
    }
963
 
964
}, {
965
    NAME: 'model',
966
 
967
    ATTRS: {
968
        /**
969
        A client-only identifier for this model.
970
 
971
        Like the `id` attribute, `clientId` may be used to retrieve model
972
        instances from lists. Unlike the `id` attribute, `clientId` is
973
        automatically generated, and is only intended to be used on the client
974
        during the current pageview.
975
 
976
        @attribute clientId
977
        @type String
978
        @readOnly
979
        **/
980
        clientId: {
981
            valueFn : 'generateClientId',
982
            readOnly: true
983
        },
984
 
985
        /**
986
        A unique identifier for this model. Among other things, this id may be
987
        used to retrieve model instances from lists, so it should be unique.
988
 
989
        If the id is empty, this model instance is assumed to represent a new
990
        item that hasn't yet been saved.
991
 
992
        If you would prefer to use a custom attribute as this model's id instead
993
        of using the `id` attribute (for example, maybe you'd rather use `_id`
994
        or `uid` as the primary id), you may set the `idAttribute` property to
995
        the name of your custom id attribute. The `id` attribute will then
996
        act as an alias for your custom attribute.
997
 
998
        @attribute id
999
        @type String|Number|null
1000
        @default `null`
1001
        **/
1002
        id: {value: null}
1003
    }
1004
});
1005
 
1006
 
1007
}, '3.18.1', {"requires": ["base-build", "escape", "json-parse"]});