Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / data / AbstractStore.js
1 /**
2  * @author Ed Spencer
3  * @class Ext.data.AbstractStore
4  *
5  * <p>AbstractStore is a superclass of {@link Ext.data.Store} and {@link Ext.data.TreeStore}. It's never used directly,
6  * but offers a set of methods used by both of those subclasses.</p>
7  * 
8  * <p>We've left it here in the docs for reference purposes, but unless you need to make a whole new type of Store, what
9  * you're probably looking for is {@link Ext.data.Store}. If you're still interested, here's a brief description of what 
10  * AbstractStore is and is not.</p>
11  * 
12  * <p>AbstractStore provides the basic configuration for anything that can be considered a Store. It expects to be 
13  * given a {@link Ext.data.Model Model} that represents the type of data in the Store. It also expects to be given a 
14  * {@link Ext.data.proxy.Proxy Proxy} that handles the loading of data into the Store.</p>
15  * 
16  * <p>AbstractStore provides a few helpful methods such as {@link #load} and {@link #sync}, which load and save data
17  * respectively, passing the requests through the configured {@link #proxy}. Both built-in Store subclasses add extra
18  * behavior to each of these functions. Note also that each AbstractStore subclass has its own way of storing data - 
19  * in {@link Ext.data.Store} the data is saved as a flat {@link Ext.util.MixedCollection MixedCollection}, whereas in
20  * {@link Ext.data.TreeStore TreeStore} we use a {@link Ext.data.Tree} to maintain the data's hierarchy.</p>
21  * 
22  * TODO: Update these docs to explain about the sortable and filterable mixins.
23  * <p>Finally, AbstractStore provides an API for sorting and filtering data via its {@link #sorters} and {@link #filters}
24  * {@link Ext.util.MixedCollection MixedCollections}. Although this functionality is provided by AbstractStore, there's a
25  * good description of how to use it in the introduction of {@link Ext.data.Store}.
26  * 
27  */
28 Ext.define('Ext.data.AbstractStore', {
29     requires: ['Ext.util.MixedCollection', 'Ext.data.Operation', 'Ext.util.Filter'],
30     
31     mixins: {
32         observable: 'Ext.util.Observable',
33         sortable: 'Ext.util.Sortable'
34     },
35     
36     statics: {
37         create: function(store){
38             if (!store.isStore) {
39                 if (!store.type) {
40                     store.type = 'store';
41                 }
42                 store = Ext.createByAlias('store.' + store.type, store);
43             }
44             return store;
45         }    
46     },
47     
48     remoteSort  : false,
49     remoteFilter: false,
50
51     /**
52      * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
53      * object or a Proxy instance - see {@link #setProxy} for details.
54      */
55
56     /**
57      * @cfg {Boolean/Object} autoLoad If data is not specified, and if autoLoad is true or an Object, this store's load method
58      * is automatically called after creation. If the value of autoLoad is an Object, this Object will be passed to the store's
59      * load method. Defaults to false.
60      */
61     autoLoad: false,
62
63     /**
64      * @cfg {Boolean} autoSync True to automatically sync the Store with its Proxy after every edit to one of its Records.
65      * Defaults to false.
66      */
67     autoSync: false,
68
69     /**
70      * Sets the updating behavior based on batch synchronization. 'operation' (the default) will update the Store's
71      * internal representation of the data after each operation of the batch has completed, 'complete' will wait until
72      * the entire batch has been completed before updating the Store's data. 'complete' is a good choice for local
73      * storage proxies, 'operation' is better for remote proxies, where there is a comparatively high latency.
74      * @property batchUpdateMode
75      * @type String
76      */
77     batchUpdateMode: 'operation',
78
79     /**
80      * If true, any filters attached to this Store will be run after loading data, before the datachanged event is fired.
81      * Defaults to true, ignored if {@link #remoteFilter} is true
82      * @property filterOnLoad
83      * @type Boolean
84      */
85     filterOnLoad: true,
86
87     /**
88      * If true, any sorters attached to this Store will be run after loading data, before the datachanged event is fired.
89      * Defaults to true, igored if {@link #remoteSort} is true
90      * @property sortOnLoad
91      * @type Boolean
92      */
93     sortOnLoad: true,
94
95     /**
96      * True if a model was created implicitly for this Store. This happens if a fields array is passed to the Store's constructor
97      * instead of a model constructor or name.
98      * @property implicitModel
99      * @type Boolean
100      * @private
101      */
102     implicitModel: false,
103
104     /**
105      * The string type of the Proxy to create if none is specified. This defaults to creating a {@link Ext.data.proxy.Memory memory proxy}.
106      * @property defaultProxyType
107      * @type String
108      */
109     defaultProxyType: 'memory',
110
111     /**
112      * True if the Store has already been destroyed via {@link #destroyStore}. If this is true, the reference to Store should be deleted
113      * as it will not function correctly any more.
114      * @property isDestroyed
115      * @type Boolean
116      */
117     isDestroyed: false,
118
119     isStore: true,
120
121     /**
122      * @cfg {String} storeId Optional unique identifier for this store. If present, this Store will be registered with 
123      * the {@link Ext.data.StoreManager}, making it easy to reuse elsewhere. Defaults to undefined.
124      */
125     
126     /**
127      * @cfg {Array} fields
128      * This may be used in place of specifying a {@link #model} configuration. The fields should be a 
129      * set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model}
130      * with these fields. In general this configuration option should be avoided, it exists for the purposes of
131      * backwards compatibility. For anything more complicated, such as specifying a particular id property or
132      * assocations, a {@link Ext.data.Model} should be defined and specified for the {@link #model} config.
133      */
134
135     sortRoot: 'data',
136     
137     //documented above
138     constructor: function(config) {
139         var me = this,
140             filters;
141         
142         me.addEvents(
143             /**
144              * @event add
145              * Fired when a Model instance has been added to this Store
146              * @param {Ext.data.Store} store The store
147              * @param {Array} records The Model instances that were added
148              * @param {Number} index The index at which the instances were inserted
149              */
150             'add',
151
152             /**
153              * @event remove
154              * Fired when a Model instance has been removed from this Store
155              * @param {Ext.data.Store} store The Store object
156              * @param {Ext.data.Model} record The record that was removed
157              * @param {Number} index The index of the record that was removed
158              */
159             'remove',
160             
161             /**
162              * @event update
163              * Fires when a Record has been updated
164              * @param {Store} this
165              * @param {Ext.data.Model} record The Model instance that was updated
166              * @param {String} operation The update operation being performed. Value may be one of:
167              * <pre><code>
168                Ext.data.Model.EDIT
169                Ext.data.Model.REJECT
170                Ext.data.Model.COMMIT
171              * </code></pre>
172              */
173             'update',
174
175             /**
176              * @event datachanged
177              * Fires whenever the records in the Store have changed in some way - this could include adding or removing records,
178              * or updating the data in existing records
179              * @param {Ext.data.Store} this The data store
180              */
181             'datachanged',
182
183             /**
184              * @event beforeload
185              * Event description
186              * @param {Ext.data.Store} store This Store
187              * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to load the Store
188              */
189             'beforeload',
190
191             /**
192              * @event load
193              * Fires whenever the store reads data from a remote data source.
194              * @param {Ext.data.Store} this
195              * @param {Array} records An array of records
196              * @param {Boolean} successful True if the operation was successful.
197              */
198             'load',
199
200             /**
201              * @event beforesync
202              * Called before a call to {@link #sync} is executed. Return false from any listener to cancel the synv
203              * @param {Object} options Hash of all records to be synchronized, broken down into create, update and destroy
204              */
205             'beforesync',
206             /**
207              * @event clear
208              * Fired after the {@link #removeAll} method is called.
209              * @param {Ext.data.Store} this
210              */
211             'clear'
212         );
213         
214         Ext.apply(me, config);
215         // don't use *config* anymore from here on... use *me* instead...
216
217         /**
218          * Temporary cache in which removed model instances are kept until successfully synchronised with a Proxy,
219          * at which point this is cleared.
220          * @private
221          * @property removed
222          * @type Array
223          */
224         me.removed = [];
225
226         me.mixins.observable.constructor.apply(me, arguments);
227         me.model = Ext.ModelManager.getModel(me.model);
228
229         /**
230          * @property modelDefaults
231          * @type Object
232          * @private
233          * A set of default values to be applied to every model instance added via {@link #insert} or created via {@link #create}.
234          * This is used internally by associations to set foreign keys and other fields. See the Association classes source code
235          * for examples. This should not need to be used by application developers.
236          */
237         Ext.applyIf(me, {
238             modelDefaults: {}
239         });
240
241         //Supports the 3.x style of simply passing an array of fields to the store, implicitly creating a model
242         if (!me.model && me.fields) {
243             me.model = Ext.define('Ext.data.Store.ImplicitModel-' + (me.storeId || Ext.id()), {
244                 extend: 'Ext.data.Model',
245                 fields: me.fields,
246                 proxy: me.proxy || me.defaultProxyType
247             });
248
249             delete me.fields;
250
251             me.implicitModel = true;
252         }
253
254         //ensures that the Proxy is instantiated correctly
255         me.setProxy(me.proxy || me.model.getProxy());
256
257         if (me.id && !me.storeId) {
258             me.storeId = me.id;
259             delete me.id;
260         }
261
262         if (me.storeId) {
263             Ext.data.StoreManager.register(me);
264         }
265         
266         me.mixins.sortable.initSortable.call(me);        
267         
268         /**
269          * The collection of {@link Ext.util.Filter Filters} currently applied to this Store
270          * @property filters
271          * @type Ext.util.MixedCollection
272          */
273         filters = me.decodeFilters(me.filters);
274         me.filters = Ext.create('Ext.util.MixedCollection');
275         me.filters.addAll(filters);
276     },
277
278     /**
279      * Sets the Store's Proxy by string, config object or Proxy instance
280      * @param {String|Object|Ext.data.proxy.Proxy} proxy The new Proxy, which can be either a type string, a configuration object
281      * or an Ext.data.proxy.Proxy instance
282      * @return {Ext.data.proxy.Proxy} The attached Proxy object
283      */
284     setProxy: function(proxy) {
285         var me = this;
286         
287         if (proxy instanceof Ext.data.proxy.Proxy) {
288             proxy.setModel(me.model);
289         } else {
290             if (Ext.isString(proxy)) {
291                 proxy = {
292                     type: proxy    
293                 };
294             }
295             Ext.applyIf(proxy, {
296                 model: me.model
297             });
298             
299             proxy = Ext.createByAlias('proxy.' + proxy.type, proxy);
300         }
301         
302         me.proxy = proxy;
303         
304         return me.proxy;
305     },
306
307     /**
308      * Returns the proxy currently attached to this proxy instance
309      * @return {Ext.data.proxy.Proxy} The Proxy instance
310      */
311     getProxy: function() {
312         return this.proxy;
313     },
314
315     //saves any phantom records
316     create: function(data, options) {
317         var me = this,
318             instance = Ext.ModelManager.create(Ext.applyIf(data, me.modelDefaults), me.model.modelName),
319             operation;
320         
321         options = options || {};
322
323         Ext.applyIf(options, {
324             action : 'create',
325             records: [instance]
326         });
327
328         operation = Ext.create('Ext.data.Operation', options);
329
330         me.proxy.create(operation, me.onProxyWrite, me);
331         
332         return instance;
333     },
334
335     read: function() {
336         return this.load.apply(this, arguments);
337     },
338
339     onProxyRead: Ext.emptyFn,
340
341     update: function(options) {
342         var me = this,
343             operation;
344         options = options || {};
345
346         Ext.applyIf(options, {
347             action : 'update',
348             records: me.getUpdatedRecords()
349         });
350
351         operation = Ext.create('Ext.data.Operation', options);
352
353         return me.proxy.update(operation, me.onProxyWrite, me);
354     },
355
356     /**
357      * @private
358      * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect
359      * the updates provided by the Proxy
360      */
361     onProxyWrite: function(operation) {
362         var me = this,
363             success = operation.wasSuccessful(),
364             records = operation.getRecords();
365
366         switch (operation.action) {
367             case 'create':
368                 me.onCreateRecords(records, operation, success);
369                 break;
370             case 'update':
371                 me.onUpdateRecords(records, operation, success);
372                 break;
373             case 'destroy':
374                 me.onDestroyRecords(records, operation, success);
375                 break;
376         }
377
378         if (success) {
379             me.fireEvent('write', me, operation);
380             me.fireEvent('datachanged', me);
381         }
382         //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional
383         Ext.callback(operation.callback, operation.scope || me, [records, operation, success]);
384     },
385
386
387     //tells the attached proxy to destroy the given records
388     destroy: function(options) {
389         var me = this,
390             operation;
391             
392         options = options || {};
393
394         Ext.applyIf(options, {
395             action : 'destroy',
396             records: me.getRemovedRecords()
397         });
398
399         operation = Ext.create('Ext.data.Operation', options);
400
401         return me.proxy.destroy(operation, me.onProxyWrite, me);
402     },
403
404     /**
405      * @private
406      * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default just calls through
407      * to onProxyWrite.
408      */
409     onBatchOperationComplete: function(batch, operation) {
410         return this.onProxyWrite(operation);
411     },
412
413     /**
414      * @private
415      * Attached as the 'complete' event listener to a proxy's Batch object. Iterates over the batch operations
416      * and updates the Store's internal data MixedCollection.
417      */
418     onBatchComplete: function(batch, operation) {
419         var me = this,
420             operations = batch.operations,
421             length = operations.length,
422             i;
423
424         me.suspendEvents();
425
426         for (i = 0; i < length; i++) {
427             me.onProxyWrite(operations[i]);
428         }
429
430         me.resumeEvents();
431
432         me.fireEvent('datachanged', me);
433     },
434
435     onBatchException: function(batch, operation) {
436         // //decide what to do... could continue with the next operation
437         // batch.start();
438         //
439         // //or retry the last operation
440         // batch.retry();
441     },
442
443     /**
444      * @private
445      * Filter function for new records.
446      */
447     filterNew: function(item) {
448         // only want phantom records that are valid
449         return item.phantom === true && item.isValid();
450     },
451
452     /**
453      * Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not
454      * yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one)
455      * @return {Array} The Model instances
456      */
457     getNewRecords: function() {
458         return [];
459     },
460
461     /**
462      * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy
463      * @return {Array} The updated Model instances
464      */
465     getUpdatedRecords: function() {
466         return [];
467     },
468
469     /**
470      * @private
471      * Filter function for updated records.
472      */
473     filterUpdated: function(item) {
474         // only want dirty records, not phantoms that are valid
475         return item.dirty === true && item.phantom !== true && item.isValid();
476     },
477
478     /**
479      * Returns any records that have been removed from the store but not yet destroyed on the proxy.
480      * @return {Array} The removed Model instances
481      */
482     getRemovedRecords: function() {
483         return this.removed;
484     },
485
486     filter: function(filters, value) {
487
488     },
489
490     /**
491      * @private
492      * Normalizes an array of filter objects, ensuring that they are all Ext.util.Filter instances
493      * @param {Array} filters The filters array
494      * @return {Array} Array of Ext.util.Filter objects
495      */
496     decodeFilters: function(filters) {
497         if (!Ext.isArray(filters)) {
498             if (filters === undefined) {
499                 filters = [];
500             } else {
501                 filters = [filters];
502             }
503         }
504
505         var length = filters.length,
506             Filter = Ext.util.Filter,
507             config, i;
508
509         for (i = 0; i < length; i++) {
510             config = filters[i];
511
512             if (!(config instanceof Filter)) {
513                 Ext.apply(config, {
514                     root: 'data'
515                 });
516
517                 //support for 3.x style filters where a function can be defined as 'fn'
518                 if (config.fn) {
519                     config.filterFn = config.fn;
520                 }
521
522                 //support a function to be passed as a filter definition
523                 if (typeof config == 'function') {
524                     config = {
525                         filterFn: config
526                     };
527                 }
528
529                 filters[i] = new Filter(config);
530             }
531         }
532
533         return filters;
534     },
535
536     clearFilter: function(supressEvent) {
537
538     },
539
540     isFiltered: function() {
541
542     },
543
544     filterBy: function(fn, scope) {
545
546     },
547     
548     /**
549      * Synchronizes the Store with its Proxy. This asks the Proxy to batch together any new, updated
550      * and deleted records in the store, updating the Store's internal representation of the records
551      * as each operation completes.
552      */
553     sync: function() {
554         var me        = this,
555             options   = {},
556             toCreate  = me.getNewRecords(),
557             toUpdate  = me.getUpdatedRecords(),
558             toDestroy = me.getRemovedRecords(),
559             needsSync = false;
560
561         if (toCreate.length > 0) {
562             options.create = toCreate;
563             needsSync = true;
564         }
565
566         if (toUpdate.length > 0) {
567             options.update = toUpdate;
568             needsSync = true;
569         }
570
571         if (toDestroy.length > 0) {
572             options.destroy = toDestroy;
573             needsSync = true;
574         }
575
576         if (needsSync && me.fireEvent('beforesync', options) !== false) {
577             me.proxy.batch(options, me.getBatchListeners());
578         }
579     },
580
581
582     /**
583      * @private
584      * Returns an object which is passed in as the listeners argument to proxy.batch inside this.sync.
585      * This is broken out into a separate function to allow for customisation of the listeners
586      * @return {Object} The listeners object
587      */
588     getBatchListeners: function() {
589         var me = this,
590             listeners = {
591                 scope: me,
592                 exception: me.onBatchException
593             };
594
595         if (me.batchUpdateMode == 'operation') {
596             listeners.operationcomplete = me.onBatchOperationComplete;
597         } else {
598             listeners.complete = me.onBatchComplete;
599         }
600
601         return listeners;
602     },
603
604     //deprecated, will be removed in 5.0
605     save: function() {
606         return this.sync.apply(this, arguments);
607     },
608
609     /**
610      * Loads the Store using its configured {@link #proxy}.
611      * @param {Object} options Optional config object. This is passed into the {@link Ext.data.Operation Operation}
612      * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function
613      */
614     load: function(options) {
615         var me = this,
616             operation;
617
618         options = options || {};
619
620         Ext.applyIf(options, {
621             action : 'read',
622             filters: me.filters.items,
623             sorters: me.getSorters()
624         });
625         
626         operation = Ext.create('Ext.data.Operation', options);
627
628         if (me.fireEvent('beforeload', me, operation) !== false) {
629             me.loading = true;
630             me.proxy.read(operation, me.onProxyLoad, me);
631         }
632         
633         return me;
634     },
635
636     /**
637      * @private
638      * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
639      * @param {Ext.data.Model} record The model instance that was edited
640      */
641     afterEdit : function(record) {
642         var me = this;
643         
644         if (me.autoSync) {
645             me.sync();
646         }
647         
648         me.fireEvent('update', me, record, Ext.data.Model.EDIT);
649     },
650
651     /**
652      * @private
653      * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to..
654      * @param {Ext.data.Model} record The model instance that was edited
655      */
656     afterReject : function(record) {
657         this.fireEvent('update', this, record, Ext.data.Model.REJECT);
658     },
659
660     /**
661      * @private
662      * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
663      * @param {Ext.data.Model} record The model instance that was edited
664      */
665     afterCommit : function(record) {
666         this.fireEvent('update', this, record, Ext.data.Model.COMMIT);
667     },
668
669     clearData: Ext.emptyFn,
670
671     destroyStore: function() {
672         var me = this;
673         
674         if (!me.isDestroyed) {
675             if (me.storeId) {
676                 Ext.data.StoreManager.unregister(me);
677             }
678             me.clearData();
679             me.data = null;
680             me.tree = null;
681             // Ext.destroy(this.proxy);
682             me.reader = me.writer = null;
683             me.clearListeners();
684             me.isDestroyed = true;
685
686             if (me.implicitModel) {
687                 Ext.destroy(me.model);
688             }
689         }
690     },
691     
692     doSort: function(sorterFn) {
693         var me = this;
694         if (me.remoteSort) {
695             //the load function will pick up the new sorters and request the sorted data from the proxy
696             me.load();
697         } else {
698             me.data.sortBy(sorterFn);
699             me.fireEvent('datachanged', me);
700         }
701     },
702
703     getCount: Ext.emptyFn,
704
705     getById: Ext.emptyFn,
706     
707     /**
708      * Removes all records from the store. This method does a "fast remove",
709      * individual remove events are not called. The {@link #clear} event is
710      * fired upon completion.
711      * @method
712      */
713     removeAll: Ext.emptyFn,
714     // individual substores should implement a "fast" remove
715     // and fire a clear event afterwards
716
717     /**
718      * Returns true if the Store is currently performing a load operation
719      * @return {Boolean} True if the Store is currently loading
720      */
721     isLoading: function() {
722         return this.loading;
723      }
724 });