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