3
Copyright 2011 Yahoo! Inc. All rights reserved.
4
Licensed under the BSD License.
5
http://yuilibrary.com/license/
7
YUI.add('model-list', function(Y) {
10
Provides an API for managing an ordered list of Model instances.
17
Provides an API for managing an ordered list of Model instances.
19
In addition to providing convenient `add`, `create`, `reset`, and `remove`
20
methods for managing the models in the list, ModelLists are also bubble targets
21
for events on the model instances they contain. This means, for example, that
22
you can add several models to a list, and then subscribe to the `*:change` event
23
on the list to be notified whenever any model in the list changes.
25
ModelLists also maintain sort order efficiently as models are added and removed,
26
based on a custom `comparator` function you may define (if no comparator is
27
defined, models are sorted in insertion order).
36
var AttrProto = Y.Attribute.prototype,
41
Fired when a model is added to the list.
43
Listen to the `on` phase of this event to be notified before a model is
44
added to the list. Calling `e.preventDefault()` during the `on` phase will
45
prevent the model from being added.
47
Listen to the `after` phase of this event to be notified after a model has
48
been added to the list.
51
@param {Model} model The model being added.
52
@param {Number} index The index at which the model will be added.
53
@preventable _defAddFn
58
Fired when an error occurs, such as when an attempt is made to add a
59
duplicate model to the list, or when a sync layer response can't be parsed.
62
@param {Any} error Error message, object, or exception generated by the
63
error. Calling `toString()` on this should result in a meaningful error
65
@param {String} src Source of the error. May be one of the following (or any
66
custom error source defined by a ModelList subclass):
68
* `add`: Error while adding a model (probably because it's already in the
69
list and can't be added again). The model in question will be provided
70
as the `model` property on the event facade.
71
* `parse`: An error parsing a JSON response. The response in question will
72
be provided as the `response` property on the event facade.
73
* `remove`: Error while removing a model (probably because it isn't in the
74
list and can't be removed). The model in question will be provided as
75
the `model` property on the event facade.
80
Fired when the list is completely reset via the `reset()` method or sorted
81
via the `sort()` method.
83
Listen to the `on` phase of this event to be notified before the list is
84
reset. Calling `e.preventDefault()` during the `on` phase will prevent
85
the list from being reset.
87
Listen to the `after` phase of this event to be notified after the list has
91
@param {Model[]} models Array of the list's new models after the reset.
92
@param {String} src Source of the event. May be either `'reset'` or
94
@preventable _defResetFn
99
Fired when a model is removed from the list.
101
Listen to the `on` phase of this event to be notified before a model is
102
removed from the list. Calling `e.preventDefault()` during the `on` phase
103
will prevent the model from being removed.
105
Listen to the `after` phase of this event to be notified after a model has
106
been removed from the list.
109
@param {Model} model The model being removed.
110
@param {int} index The index of the model being removed.
111
@preventable _defRemoveFn
113
EVT_REMOVE = 'remove';
115
function ModelList() {
116
ModelList.superclass.constructor.apply(this, arguments);
119
Y.ModelList = Y.extend(ModelList, Y.Base, {
120
// -- Public Properties ----------------------------------------------------
123
The `Model` class or subclass of the models in this list.
125
This property is `null` by default, and is intended to be overridden in a
126
subclass or specified as a config property at instantiation time. It will be
127
used to create model instances automatically based on attribute hashes
128
passed to the `add()`, `create()`, and `reset()` methods.
136
// -- Lifecycle Methods ----------------------------------------------------
137
initializer: function (config) {
138
config || (config = {});
140
var model = this.model = config.model || this.model;
142
this.publish(EVT_ADD, {defaultFn: this._defAddFn});
143
this.publish(EVT_RESET, {defaultFn: this._defResetFn});
144
this.publish(EVT_REMOVE, {defaultFn: this._defRemoveFn});
147
this.after('*:idChange', this._afterIdChange);
149
Y.log('No model class specified.', 'warn', 'model-list');
155
destructor: function () {
156
YArray.each(this._items, this._detachList, this);
159
// -- Public Methods -------------------------------------------------------
162
Adds the specified model or array of models to this list.
165
// Add a single model instance.
166
list.add(new Model({foo: 'bar'}));
168
// Add a single model, creating a new instance automatically.
169
list.add({foo: 'bar'});
171
// Add multiple models, creating new instances automatically.
178
@param {Model|Model[]|Object|Object[]} models Models to add. May be existing
179
model instances or hashes of model attributes, in which case new model
180
instances will be created from the hashes.
181
@param {Object} [options] Data to be mixed into the event facade of the
182
`add` event(s) for the added models.
183
@param {Boolean} [options.silent=false] If `true`, no `add` event(s) will
185
@return {Model|Model[]} Added model or array of added models.
187
add: function (models, options) {
188
if (Lang.isArray(models)) {
189
return YArray.map(models, function (model) {
190
return this._add(model, options);
193
return this._add(models, options);
198
Define this method to provide a function that takes a model as a parameter
199
and returns a value by which that model should be sorted relative to other
202
By default, no comparator is defined, meaning that models will not be sorted
203
(they'll be stored in the order they're added).
206
var list = new Y.ModelList({model: Y.Model});
208
list.comparator = function (model) {
209
return model.get('id'); // Sort models by id.
213
@param {Model} model Model being sorted.
214
@return {Number|String} Value by which the model should be sorted relative
215
to other models in this list.
218
// comparator is not defined by default
221
Creates or updates the specified model on the server, then adds it to this
222
list if the server indicates success.
225
@param {Model|Object} model Model to create. May be an existing model
226
instance or a hash of model attributes, in which case a new model instance
227
will be created from the hash.
228
@param {Object} [options] Options to be passed to the model's `sync()` and
229
`set()` methods and mixed into the `add` event when the model is added
231
@param {Boolean} [options.silent=false] If `true`, no `add` event(s) will
233
@param {callback} [callback] Called when the sync operation finishes.
234
@param {Error} callback.err If an error occurred, this parameter will
235
contain the error. If the sync operation succeeded, _err_ will be
237
@param {mixed} callback.response The server's response.
238
@return {Model} Created model.
240
create: function (model, options, callback) {
243
// Allow callback as second arg.
244
if (typeof options === 'function') {
249
if (!(model instanceof Y.Model)) {
250
model = new this.model(model);
253
return model.save(options, function (err) {
255
self.add(model, options);
258
callback && callback.apply(null, arguments);
263
If _name_ refers to an attribute on this ModelList instance, returns the
264
value of that attribute. Otherwise, returns an array containing the values
265
of the specified attribute from each model in this list.
268
@param {String} name Attribute name or object property path.
269
@return {Any|Array} Attribute value or array of attribute values.
272
get: function (name) {
273
if (this.attrAdded(name)) {
274
return AttrProto.get.apply(this, arguments);
277
return this.invoke('get', name);
281
If _name_ refers to an attribute on this ModelList instance, returns the
282
HTML-escaped value of that attribute. Otherwise, returns an array containing
283
the HTML-escaped values of the specified attribute from each model in this
286
The values are escaped using `Escape.html()`.
289
@param {String} name Attribute name or object property path.
290
@return {String|String[]} HTML-escaped value or array of HTML-escaped
292
@see Model.getAsHTML()
294
getAsHTML: function (name) {
295
if (this.attrAdded(name)) {
296
return Y.Escape.html(AttrProto.get.apply(this, arguments));
299
return this.invoke('getAsHTML', name);
304
If _name_ refers to an attribute on this ModelList instance, returns the
305
URL-encoded value of that attribute. Otherwise, returns an array containing
306
the URL-encoded values of the specified attribute from each model in this
309
The values are encoded using the native `encodeURIComponent()` function.
312
@param {String} name Attribute name or object property path.
313
@return {String|String[]} URL-encoded value or array of URL-encoded values.
314
@see Model.getAsURL()
316
getAsURL: function (name) {
317
if (this.attrAdded(name)) {
318
return encodeURIComponent(AttrProto.get.apply(this, arguments));
321
return this.invoke('getAsURL', name);
325
Returns the model with the specified _clientId_, or `null` if not found.
327
@method getByClientId
328
@param {String} clientId Client id.
329
@return {Model} Model, or `null` if not found.
331
getByClientId: function (clientId) {
332
return this._clientIdMap[clientId] || null;
336
Returns the model with the specified _id_, or `null` if not found.
338
Note that models aren't expected to have an id until they're saved, so if
339
you're working with unsaved models, it may be safer to call
343
@param {String|Number} id Model id.
344
@return {Model} Model, or `null` if not found.
346
getById: function (id) {
347
return this._idMap[id] || null;
351
Calls the named method on every model in the list. Any arguments provided
352
after _name_ will be passed on to the invoked method.
355
@param {String} name Name of the method to call on each model.
356
@param {Any} [args*] Zero or more arguments to pass to the invoked method.
357
@return {Array} Array of return values, indexed according to the index of
358
the model on which the method was called.
360
invoke: function (name /*, args* */) {
361
var args = [this._items, name].concat(YArray(arguments, 1, true));
362
return YArray.invoke.apply(YArray, args);
366
Returns the model at the specified _index_.
369
@param {Number} index Index of the model to fetch.
370
@return {Model} The model at the specified index, or `undefined` if there
374
// item() is inherited from ArrayList.
377
Loads this list of models from the server.
379
This method delegates to the `sync()` method to perform the actual load
380
operation, which is an asynchronous action. Specify a _callback_ function to
381
be notified of success or failure.
383
If the load operation succeeds, a `reset` event will be fired.
386
@param {Object} [options] Options to be passed to `sync()` and to
387
`reset()` when adding the loaded models. It's up to the custom sync
388
implementation to determine what options it supports or requires, if any.
389
@param {Function} [callback] Called when the sync operation finishes.
390
@param {Error} callback.err If an error occurred, this parameter will
391
contain the error. If the sync operation succeeded, _err_ will be
393
@param {Any} callback.response The server's response. This value will
394
be passed to the `parse()` method, which is expected to parse it and
395
return an array of model attribute hashes.
398
load: function (options, callback) {
401
// Allow callback as only arg.
402
if (typeof options === 'function') {
407
this.sync('read', options, function (err, response) {
409
self.reset(self.parse(response), options);
412
callback && callback.apply(null, arguments);
419
Executes the specified function on each model in this list and returns an
420
array of the function's collected return values.
423
@param {Function} fn Function to execute on each model.
424
@param {Model} fn.model Current model being iterated.
425
@param {Number} fn.index Index of the current model in the list.
426
@param {Model[]} fn.models Array of models being iterated.
427
@param {Object} [thisObj] `this` object to use when calling _fn_.
428
@return {Array} Array of return values from _fn_.
430
map: function (fn, thisObj) {
431
return YArray.map(this._items, fn, thisObj);
435
Called to parse the _response_ when the list is loaded from the server.
436
This method receives a server _response_ and is expected to return an array
437
of model attribute hashes.
439
The default implementation assumes that _response_ is either an array of
440
attribute hashes or a JSON string that can be parsed into an array of
441
attribute hashes. If _response_ is a JSON string and either `Y.JSON` or the
442
native `JSON` object are available, it will be parsed automatically. If a
443
parse error occurs, an `error` event will be fired and the model will not be
446
You may override this method to implement custom parsing logic if necessary.
449
@param {Any} response Server response.
450
@return {Object[]} Array of model attribute hashes.
452
parse: function (response) {
453
if (typeof response === 'string') {
455
return Y.JSON.parse(response) || [];
457
this.fire(EVT_ERROR, {
467
return response || [];
471
Removes the specified model or array of models from this list.
474
@param {Model|Model[]} models Models to remove.
475
@param {Object} [options] Data to be mixed into the event facade of the
476
`remove` event(s) for the removed models.
477
@param {Boolean} [options.silent=false] If `true`, no `remove` event(s)
479
@return {Model|Model[]} Removed model or array of removed models.
481
remove: function (models, options) {
482
if (Lang.isArray(models)) {
483
return YArray.map(models, function (model) {
484
return this._remove(model, options);
487
return this._remove(models, options);
492
Completely replaces all models in the list with those specified, and fires a
493
single `reset` event.
495
Use `reset` when you want to add or remove a large number of items at once
496
without firing `add` or `remove` events for each one.
499
@param {Model[]|Object[]} [models] Models to add. May be existing model
500
instances or hashes of model attributes, in which case new model instances
501
will be created from the hashes. Calling `reset()` without passing in any
502
models will clear the list.
503
@param {Object} [options] Data to be mixed into the event facade of the
505
@param {Boolean} [options.silent=false] If `true`, no `reset` event will
509
reset: function (models, options) {
510
models || (models = []);
511
options || (options = {});
513
var facade = Y.merge(options, {
515
models: YArray.map(models, function (model) {
516
return model instanceof Y.Model ? model :
517
new this.model(model);
521
// Sort the models in the facade before firing the reset event.
522
if (this.comparator) {
523
facade.models.sort(Y.bind(this._sort, this));
526
options.silent ? this._defResetFn(facade) :
527
this.fire(EVT_RESET, facade);
533
Forcibly re-sorts the list.
535
Usually it shouldn't be necessary to call this method since the list
536
maintains its sort order when items are added and removed, but if you change
537
the `comparator` function after items are already in the list, you'll need
541
@param {Object} [options] Data to be mixed into the event facade of the
543
@param {Boolean} [options.silent=false] If `true`, no `reset` event will
547
sort: function (options) {
548
var models = this._items.concat(),
551
if (!this.comparator) {
555
options || (options = {});
557
models.sort(Y.bind(this._sort, this));
559
facade = Y.merge(options, {
564
options.silent ? this._defResetFn(facade) :
565
this.fire(EVT_RESET, facade);
571
Override this method to provide a custom persistence implementation for this
572
list. The default method just calls the callback without actually doing
575
This method is called internally by `load()`.
578
@param {String} action Sync action to perform. May be one of the following:
580
* `create`: Store a list of newly-created models for the first time.
581
* `delete`: Delete a list of existing models.
582
* `read` : Load a list of existing models.
583
* `update`: Update a list of existing models.
585
Currently, model lists only make use of the `read` action, but other
586
actions may be used in future versions.
588
@param {Object} [options] Sync options. It's up to the custom sync
589
implementation to determine what options it supports or requires, if any.
590
@param {Function} [callback] Called when the sync operation finishes.
591
@param {Error} callback.err If an error occurred, this parameter will
592
contain the error. If the sync operation succeeded, _err_ will be
594
@param {Any} [callback.response] The server's response. This value will
595
be passed to the `parse()` method, which is expected to parse it and
596
return an array of model attribute hashes.
598
sync: function (/* action, options, callback */) {
599
var callback = YArray(arguments, 0, true).pop();
601
if (typeof callback === 'function') {
607
Returns an array containing the models in this list.
610
@return {Array} Array containing the models in this list.
612
toArray: function () {
613
return this._items.concat();
617
Returns an array containing attribute hashes for each model in this list,
618
suitable for being passed to `Y.JSON.stringify()`.
620
Under the hood, this method calls `toJSON()` on each model in the list and
621
pushes the results into an array.
624
@return {Object[]} Array of model attribute hashes.
627
toJSON: function () {
628
return this.map(function (model) {
629
return model.toJSON();
633
// -- Protected Methods ----------------------------------------------------
636
Adds the specified _model_ if it isn't already in this list.
639
@param {Model|Object} model Model or object to add.
640
@param {Object} [options] Data to be mixed into the event facade of the
641
`add` event for the added model.
642
@param {Boolean} [options.silent=false] If `true`, no `add` event will be
644
@return {Model} The added model.
647
_add: function (model, options) {
650
options || (options = {});
652
if (!(model instanceof Y.Model)) {
653
model = new this.model(model);
656
if (this._clientIdMap[model.get('clientId')]) {
657
this.fire(EVT_ERROR, {
658
error: 'Model is already in the list.',
666
facade = Y.merge(options, {
667
index: this._findIndex(model),
671
options.silent ? this._defAddFn(facade) : this.fire(EVT_ADD, facade);
677
Adds this list as a bubble target for the specified model's events.
680
@param {Model} model Model to attach to this list.
683
_attachList: function (model) {
684
// Attach this list and make it a bubble target for the model.
685
model.lists.push(this);
686
model.addTarget(this);
690
Clears all internal state and the internal list of models, returning this
691
list to an empty state. Automatically detaches all models in the list.
696
_clear: function () {
697
YArray.each(this._items, this._detachList, this);
699
this._clientIdMap = {};
705
Removes this list as a bubble target for the specified model's events.
708
@param {Model} model Model to detach.
711
_detachList: function (model) {
712
var index = YArray.indexOf(model.lists, this);
715
model.lists.splice(index, 1);
716
model.removeTarget(this);
721
Returns the index at which the given _model_ should be inserted to maintain
722
the sort order of the list.
725
@param {Model} model The model being inserted.
726
@return {Number} Index at which the model should be inserted.
729
_findIndex: function (model) {
730
var comparator = this.comparator,
734
item, middle, needle;
736
if (!comparator || !items.length) { return items.length; }
738
needle = comparator(model);
740
// Perform an iterative binary search to determine the correct position
741
// based on the return value of the `comparator` function.
743
middle = (min + max) >> 1; // Divide by two and discard remainder.
744
item = items[middle];
746
if (comparator(item) < needle) {
757
Removes the specified _model_ if it's in this list.
760
@param {Model} model Model to remove.
761
@param {Object} [options] Data to be mixed into the event facade of the
762
`remove` event for the removed model.
763
@param {Boolean} [options.silent=false] If `true`, no `remove` event will
765
@return {Model} Removed model.
768
_remove: function (model, options) {
769
var index = this.indexOf(model),
772
options || (options = {});
775
this.fire(EVT_ERROR, {
776
error: 'Model is not in the list.',
784
facade = Y.merge(options, {
789
options.silent ? this._defRemoveFn(facade) :
790
this.fire(EVT_REMOVE, facade);
796
Array sort function used by `sort()` to re-sort the models in the list.
799
@param {Model} a First model to compare.
800
@param {Model} b Second model to compare.
801
@return {Number} `-1` if _a_ is less than _b_, `0` if equal, `1` if greater.
804
_sort: function (a, b) {
805
var aValue = this.comparator(a),
806
bValue = this.comparator(b);
808
return aValue < bValue ? -1 : (aValue > bValue ? 1 : 0);
811
// -- Event Handlers -------------------------------------------------------
814
Updates the model maps when a model's `id` attribute changes.
816
@method _afterIdChange
817
@param {EventFacade} e
820
_afterIdChange: function (e) {
821
Lang.isValue(e.prevVal) && delete this._idMap[e.prevVal];
822
Lang.isValue(e.newVal) && (this._idMap[e.newVal] = e.target);
825
// -- Default Event Handlers -----------------------------------------------
828
Default event handler for `add` events.
831
@param {EventFacade} e
834
_defAddFn: function (e) {
836
id = model.get('id');
838
this._clientIdMap[model.get('clientId')] = model;
840
if (Lang.isValue(id)) {
841
this._idMap[id] = model;
844
this._attachList(model);
845
this._items.splice(e.index, 0, model);
849
Default event handler for `remove` events.
852
@param {EventFacade} e
855
_defRemoveFn: function (e) {
857
id = model.get('id');
859
this._detachList(model);
860
delete this._clientIdMap[model.get('clientId')];
862
if (Lang.isValue(id)) {
863
delete this._idMap[id];
866
this._items.splice(e.index, 1);
870
Default event handler for `reset` events.
873
@param {EventFacade} e
876
_defResetFn: function (e) {
877
// When fired from the `sort` method, we don't need to clear the list or
878
// add any models, since the existing models are sorted in place.
879
if (e.src === 'sort') {
880
this._items = e.models.concat();
886
if (e.models.length) {
887
this.add(e.models, {silent: true});
894
Y.augment(ModelList, Y.ArrayList);
897
}, '3.4.1' ,{requires:['array-extras', 'array-invoke', 'arraylist', 'base-build', 'escape', 'json-parse', 'model']});