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