3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
17 * @class Ext.data.Store
18 * @extends Ext.data.AbstractStore
20 * <p>The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load
21 * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting},
22 * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.</p>
24 * <p>Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:</p>
27 // Set up a {@link Ext.data.Model model} to use in our Store
29 extend: 'Ext.data.Model',
31 {name: 'firstName', type: 'string'},
32 {name: 'lastName', type: 'string'},
33 {name: 'age', type: 'int'},
34 {name: 'eyeColor', type: 'string'}
38 var myStore = Ext.create('Ext.data.Store', {
52 * <p>In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy
53 * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object -
54 * {@link Ext.data.reader.Json see the docs on JsonReader} for details.</p>
56 * <p><u>Inline data</u></p>
58 * <p>Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #data}
59 * into Model instances:</p>
62 Ext.create('Ext.data.Store', {
65 {firstName: 'Ed', lastName: 'Spencer'},
66 {firstName: 'Tommy', lastName: 'Maintz'},
67 {firstName: 'Aaron', lastName: 'Conran'},
68 {firstName: 'Jamie', lastName: 'Avins'}
73 * <p>Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need
74 * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure,
75 * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).</p>
77 * <p>Additional data can also be loaded locally using {@link #add}.</p>
79 * <p><u>Loading Nested Data</u></p>
81 * <p>Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders.
82 * Instead of issuing an AJAX request for the User and a series of additional AJAX requests for each Order, we can load a nested dataset
83 * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro
84 * docs for a full explanation:</p>
87 var store = Ext.create('Ext.data.Store', {
101 * <p>Which would consume a response like this:</p>
126 * <p>See the {@link Ext.data.reader.Reader} intro docs for a full explanation.</p>
128 * <p><u>Filtering and Sorting</u></p>
130 * <p>Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are
131 * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to
132 * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}:
135 var store = Ext.create('Ext.data.Store', {
143 property : 'firstName',
150 property: 'firstName',
157 * <p>The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting
158 * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to
159 * perform these operations instead.</p>
161 * <p>Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store
162 * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by
163 * default {@link #sortOnFilter} is set to true, which means that your sorters are automatically reapplied if using local sorting.</p>
166 store.filter('eyeColor', 'Brown');
169 * <p>Change the sorting at any time by calling {@link #sort}:</p>
172 store.sort('height', 'ASC');
175 * <p>Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments,
176 * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them
177 * to the MixedCollection:</p>
180 store.sorters.add(new Ext.util.Sorter({
181 property : 'shoeSize',
188 * <p><u>Registering with StoreManager</u></p>
190 * <p>Any Store that is instantiated with a {@link #storeId} will automatically be registed with the {@link Ext.data.StoreManager StoreManager}.
191 * This makes it easy to reuse the same store in multiple views:</p>
194 //this store can be used several times
195 Ext.create('Ext.data.Store', {
197 storeId: 'usersStore'
203 //other config goes here
209 //other config goes here
213 * <p><u>Further Reading</u></p>
215 * <p>Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these
216 * pieces and how they fit together, see:</p>
218 * <ul style="list-style-type: disc; padding-left: 25px">
219 * <li>{@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used</li>
220 * <li>{@link Ext.data.Model Model} - the core class in the data package</li>
221 * <li>{@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response</li>
225 Ext.define('Ext.data.Store', {
226 extend: 'Ext.data.AbstractStore',
228 alias: 'store.store',
230 requires: ['Ext.data.StoreManager', 'Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'],
231 uses: ['Ext.data.proxy.Memory'],
234 * @cfg {Boolean} remoteSort
235 * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to <tt>false</tt>.
240 * @cfg {Boolean} remoteFilter
241 * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to <tt>false</tt>.
246 * @cfg {Boolean} remoteGroup
247 * True if the grouping should apply on the server side, false if it is local only. If the
248 * grouping is local, it can be applied immediately to the data. If it is remote, then it will simply act as a
249 * helper, automatically sending the grouping information to the server.
254 * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
255 * object or a Proxy instance - see {@link #setProxy} for details.
259 * @cfg {Object[]/Ext.data.Model[]} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details.
263 * @property {String} groupField
264 * The field by which to group data in the store. Internally, grouping is very similar to sorting - the
265 * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
266 * level of grouping, and groups can be fetched via the {@link #getGroups} method.
268 groupField: undefined,
271 * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC"
278 * @cfg {Number} pageSize
279 * The number of records considered to form a 'page'. This is used to power the built-in
280 * paging using the nextPage and previousPage functions. Defaults to 25.
285 * The page that the Store has most recently loaded (see {@link #loadPage})
286 * @property currentPage
292 * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage},
293 * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records, allowing
294 * large data sets to be loaded one page at a time but rendered all together.
296 clearOnPageLoad: true,
299 * @property {Boolean} loading
300 * True if the Store is currently loading via its Proxy
306 * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called,
307 * causing the sorters to be reapplied after filtering. Defaults to true
312 * @cfg {Boolean} buffered
313 * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will
314 * tell the store to pre-fetch records ahead of a time.
319 * @cfg {Number} purgePageCount
320 * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data.
321 * This option is only relevant when the {@link #buffered} option is set to true.
327 onClassExtended: function(cls, data) {
328 var model = data.model;
330 if (typeof model == 'string') {
331 var onBeforeClassCreated = data.onBeforeClassCreated;
333 data.onBeforeClassCreated = function(cls, data) {
336 Ext.require(model, function() {
337 onBeforeClassCreated.call(me, cls, data);
345 * @param {Object} config (optional) Config object
347 constructor: function(config) {
348 // Clone the config so we don't modify the original config object
349 config = Ext.Object.merge({}, config);
352 groupers = config.groupers || me.groupers,
353 groupField = config.groupField || me.groupField,
357 if (config.buffered || me.buffered) {
358 me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) {
361 me.pendingRequests = [];
362 me.pagesRequested = [];
364 me.sortOnLoad = false;
365 me.filterOnLoad = false;
370 * @event beforeprefetch
371 * Fires before a prefetch occurs. Return false to cancel.
372 * @param {Ext.data.Store} this
373 * @param {Ext.data.Operation} operation The associated operation
378 * Fired whenever the grouping in the grid changes
379 * @param {Ext.data.Store} store The store
380 * @param {Ext.util.Grouper[]} groupers The array of grouper objects
385 * Fires whenever records have been prefetched
386 * @param {Ext.data.Store} this
387 * @param {Ext.util.Grouper[]} records An array of records
388 * @param {Boolean} successful True if the operation was successful.
389 * @param {Ext.data.Operation} operation The associated operation
393 data = config.data || me.data;
396 * The MixedCollection that holds this store's local cache of records
398 * @type Ext.util.MixedCollection
400 me.data = Ext.create('Ext.util.MixedCollection', false, function(record) {
401 return record.internalId;
405 me.inlineData = data;
409 if (!groupers && groupField) {
411 property : groupField,
412 direction: config.groupDir || me.groupDir
415 delete config.groupers;
418 * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store
420 * @type Ext.util.MixedCollection
422 me.groupers = Ext.create('Ext.util.MixedCollection');
423 me.groupers.addAll(me.decodeGroupers(groupers));
425 this.callParent([config]);
426 // don't use *config* anymore from here on... use *me* instead...
428 if (me.groupers.items.length) {
429 me.sort(me.groupers.items, 'prepend', false);
433 data = me.inlineData;
436 if (proxy instanceof Ext.data.proxy.Memory) {
440 me.add.apply(me, data);
444 delete me.inlineData;
445 } else if (me.autoLoad) {
446 Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]);
447 // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here.
448 // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined);
452 onBeforeSort: function() {
453 var groupers = this.groupers;
454 if (groupers.getCount() > 0) {
455 this.sort(groupers.items, 'prepend', false);
461 * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances
462 * @param {Object[]} groupers The groupers array
463 * @return {Ext.util.Grouper[]} Array of Ext.util.Grouper objects
465 decodeGroupers: function(groupers) {
466 if (!Ext.isArray(groupers)) {
467 if (groupers === undefined) {
470 groupers = [groupers];
474 var length = groupers.length,
475 Grouper = Ext.util.Grouper,
478 for (i = 0; i < length; i++) {
479 config = groupers[i];
481 if (!(config instanceof Grouper)) {
482 if (Ext.isString(config)) {
488 Ext.applyIf(config, {
493 //support for 3.x style sorters where a function can be defined as 'fn'
495 config.sorterFn = config.fn;
498 //support a function to be passed as a sorter definition
499 if (typeof config == 'function') {
505 groupers[i] = new Grouper(config);
513 * Group data in the store
514 * @param {String/Object[]} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model},
515 * or an Array of grouper configurations.
516 * @param {String} direction The overall direction to group the data by. Defaults to "ASC".
518 group: function(groupers, direction) {
524 if (Ext.isArray(groupers)) {
525 newGroupers = groupers;
526 } else if (Ext.isObject(groupers)) {
527 newGroupers = [groupers];
528 } else if (Ext.isString(groupers)) {
529 grouper = me.groupers.get(groupers);
536 newGroupers = [grouper];
537 } else if (direction === undefined) {
540 grouper.setDirection(direction);
544 if (newGroupers && newGroupers.length) {
546 newGroupers = me.decodeGroupers(newGroupers);
548 me.groupers.addAll(newGroupers);
551 if (me.remoteGroup) {
554 callback: me.fireGroupChange
557 // need to explicitly force a sort if we have groupers
558 me.sort(null, null, null, hasNew);
559 me.fireGroupChange();
564 * Clear any groupers in the store
566 clearGrouping: function(){
568 // Clear any groupers we pushed on to the sorters
569 me.groupers.each(function(grouper){
570 me.sorters.remove(grouper);
573 if (me.remoteGroup) {
576 callback: me.fireGroupChange
580 me.fireEvent('groupchange', me, me.groupers);
585 * Checks if the store is currently grouped
586 * @return {Boolean} True if the store is grouped.
588 isGrouped: function() {
589 return this.groupers.getCount() > 0;
593 * Fires the groupchange event. Abstracted out so we can use it
597 fireGroupChange: function(){
598 this.fireEvent('groupchange', this, this.groupers);
602 * Returns an array containing the result of applying grouping to the records in this store. See {@link #groupField},
603 * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field:
605 var myStore = Ext.create('Ext.data.Store', {
610 myStore.getGroups(); //returns:
615 //all records where the color field is 'yellow'
621 //all records where the color field is 'red'
626 * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString}
627 * @return {Object/Object[]} The grouped data
629 getGroups: function(requestGroupString) {
630 var records = this.data.items,
631 length = records.length,
639 for (i = 0; i < length; i++) {
641 groupStr = this.getGroupString(record);
642 group = pointers[groupStr];
644 if (group === undefined) {
651 pointers[groupStr] = group;
654 group.children.push(record);
657 return requestGroupString ? pointers[requestGroupString] : groups;
662 * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records
663 * matching a certain group.
665 getGroupsForGrouper: function(records, grouper) {
666 var length = records.length,
674 for (i = 0; i < length; i++) {
676 newValue = grouper.getGroupString(record);
678 if (newValue !== oldValue) {
687 group.records.push(record);
697 * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for
698 * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by
699 * {@link #getGroupsForGrouper} - this function largely just handles the recursion.
700 * @param {Ext.data.Model[]} records The set or subset of records to group
701 * @param {Number} grouperIndex The grouper index to retrieve
702 * @return {Object[]} The grouped records
704 getGroupsForGrouperIndex: function(records, grouperIndex) {
706 groupers = me.groupers,
707 grouper = groupers.getAt(grouperIndex),
708 groups = me.getGroupsForGrouper(records, grouper),
709 length = groups.length,
712 if (grouperIndex + 1 < groupers.length) {
713 for (i = 0; i < length; i++) {
714 groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1);
718 for (i = 0; i < length; i++) {
719 groups[i].depth = grouperIndex;
727 * <p>Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in
728 * this case grouping by genre and then author in a fictional books dataset):</p>
735 //book1, book2, book3, book4
756 * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping
757 * function correctly so this should only be set to false if the Store is known to already be sorted correctly
759 * @return {Object[]} The group data
761 getGroupData: function(sort) {
763 if (sort !== false) {
767 return me.getGroupsForGrouperIndex(me.data.items, 0);
771 * <p>Returns the string to group on for a given model instance. The default implementation of this method returns
772 * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to
773 * group by the first letter of a model's 'name' field, use the following code:</p>
775 Ext.create('Ext.data.Store', {
777 getGroupString: function(instance) {
778 return instance.get('name')[0];
782 * @param {Ext.data.Model} instance The model instance
783 * @return {String} The string to compare when forming groups
785 getGroupString: function(instance) {
786 var group = this.groupers.first();
788 return instance.get(group.property);
793 * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
794 * See also <code>{@link #add}</code>.
795 * @param {Number} index The start index at which to insert the passed Records.
796 * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
798 insert: function(index, records) {
805 records = [].concat(records);
806 for (i = 0, len = records.length; i < len; i++) {
807 record = me.createModel(records[i]);
808 record.set(me.modelDefaults);
809 // reassign the model in the array in case it wasn't created yet
812 me.data.insert(index + i, record);
815 sync = sync || record.phantom === true;
819 me.snapshot.addAll(records);
822 me.fireEvent('add', me, records, index);
823 me.fireEvent('datachanged', me);
824 if (me.autoSync && sync) {
830 * Adds Model instance to the Store. This method accepts either:
832 * - An array of Model instances or Model configuration objects.
833 * - Any number of Model instance or Model configuration object arguments.
835 * The new Model instances will be added at the end of the existing collection.
839 * myStore.add({some: 'data'}, {some: 'other data'});
841 * @param {Ext.data.Model[]/Ext.data.Model...} model An array of Model instances
842 * or Model configuration objects, or variable number of Model instance or config arguments.
843 * @return {Ext.data.Model[]} The model instances that were added
845 add: function(records) {
846 //accept both a single-argument array of records, or any number of record arguments
847 if (!Ext.isArray(records)) {
848 records = Array.prototype.slice.apply(arguments);
853 length = records.length,
856 for (; i < length; i++) {
857 record = me.createModel(records[i]);
858 // reassign the model in the array in case it wasn't created yet
862 me.insert(me.data.length, records);
868 * Converts a literal to a model, if it's not a model already
870 * @param record {Ext.data.Model/Object} The record to create
871 * @return {Ext.data.Model}
873 createModel: function(record) {
874 if (!record.isModel) {
875 record = Ext.ModelManager.create(record, this.model);
882 * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
883 * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed as the first parameter.
884 * Returning <tt>false</tt> aborts and exits the iteration.
885 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed.
886 * Defaults to the current {@link Ext.data.Model Record} in the iteration.
888 each: function(fn, scope) {
889 this.data.each(fn, scope);
893 * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single
894 * 'datachanged' event after removal.
895 * @param {Ext.data.Model/Ext.data.Model[]} records The Ext.data.Model instance or array of instances to remove
897 remove: function(records, /* private */ isMove) {
898 if (!Ext.isArray(records)) {
903 * Pass the isMove parameter if we know we're going to be re-inserting this record
905 isMove = isMove === true;
909 length = records.length,
914 for (; i < length; i++) {
916 index = me.data.indexOf(record);
919 me.snapshot.remove(record);
923 isPhantom = record.phantom === true;
924 if (!isMove && !isPhantom) {
925 // don't push phantom records onto removed
926 me.removed.push(record);
930 me.data.remove(record);
931 sync = sync || !isPhantom;
933 me.fireEvent('remove', me, record, index);
937 me.fireEvent('datachanged', me);
938 if (!isMove && me.autoSync && sync) {
944 * Removes the model instance at the given index
945 * @param {Number} index The record index
947 removeAt: function(index) {
948 var record = this.getAt(index);
956 * <p>Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
957 * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
958 * instances into the Store and calling an optional callback if required. Example usage:</p>
963 callback: function(records, operation, success) {
964 //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
965 console.log(records);
970 * <p>If the callback scope does not need to be set, a function can simply be passed:</p>
973 store.load(function(records, operation, success) {
974 console.log('loaded records');
978 * @param {Object/Function} options (Optional) config object, passed into the Ext.data.Operation object before loading.
980 load: function(options) {
983 options = options || {};
985 if (Ext.isFunction(options)) {
991 Ext.applyIf(options, {
992 groupers: me.groupers.items,
993 page: me.currentPage,
994 start: (me.currentPage - 1) * me.pageSize,
999 return me.callParent([options]);
1004 * Called internally when a Proxy has completed a load request
1006 onProxyLoad: function(operation) {
1008 resultSet = operation.getResultSet(),
1009 records = operation.getRecords(),
1010 successful = operation.wasSuccessful();
1013 me.totalCount = resultSet.total;
1017 me.loadRecords(records, operation);
1021 me.fireEvent('load', me, records, successful);
1023 //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not.
1024 //People are definitely using this so can't deprecate safely until 2.x
1025 me.fireEvent('read', me, records, operation.wasSuccessful());
1027 //this is a callback that would have been passed to the 'read' function and is optional
1028 Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
1032 * Create any new records when a write is returned from the server.
1034 * @param {Ext.data.Model[]} records The array of new records
1035 * @param {Ext.data.Operation} operation The operation that just completed
1036 * @param {Boolean} success True if the operation was successful
1038 onCreateRecords: function(records, operation, success) {
1042 snapshot = this.snapshot,
1043 length = records.length,
1044 originalRecords = operation.records,
1050 * Loop over each record returned from the server. Assume they are
1051 * returned in order of how they were sent. If we find a matching
1052 * record, replace it with the newly created one.
1054 for (; i < length; ++i) {
1055 record = records[i];
1056 original = originalRecords[i];
1058 index = data.indexOf(original);
1060 data.removeAt(index);
1061 data.insert(index, record);
1064 index = snapshot.indexOf(original);
1066 snapshot.removeAt(index);
1067 snapshot.insert(index, record);
1070 record.phantom = false;
1078 * Update any records when a write is returned from the server.
1080 * @param {Ext.data.Model[]} records The array of updated records
1081 * @param {Ext.data.Operation} operation The operation that just completed
1082 * @param {Boolean} success True if the operation was successful
1084 onUpdateRecords: function(records, operation, success){
1087 length = records.length,
1089 snapshot = this.snapshot,
1092 for (; i < length; ++i) {
1093 record = records[i];
1094 data.replace(record);
1096 snapshot.replace(record);
1104 * Remove any records when a write is returned from the server.
1106 * @param {Ext.data.Model[]} records The array of removed records
1107 * @param {Ext.data.Operation} operation The operation that just completed
1108 * @param {Boolean} success True if the operation was successful
1110 onDestroyRecords: function(records, operation, success){
1114 length = records.length,
1116 snapshot = me.snapshot,
1119 for (; i < length; ++i) {
1120 record = records[i];
1122 data.remove(record);
1124 snapshot.remove(record);
1132 getNewRecords: function() {
1133 return this.data.filterBy(this.filterNew).items;
1137 getUpdatedRecords: function() {
1138 return this.data.filterBy(this.filterUpdated).items;
1142 * Filters the loaded set of records by a given set of filters.
1144 * Filtering by single field:
1146 * store.filter("email", /\.com$/);
1148 * Using multiple filters:
1151 * {property: "email", value: /\.com$/},
1152 * {filterFn: function(item) { return item.get("age") > 10; }}
1155 * Using Ext.util.Filter instances instead of config objects
1156 * (note that we need to specify the {@link Ext.util.Filter#root root} config option in this case):
1159 * Ext.create('Ext.util.Filter', {property: "email", value: /\.com$/, root: 'data'}),
1160 * Ext.create('Ext.util.Filter', {filterFn: function(item) { return item.get("age") > 10; }, root: 'data'})
1163 * @param {Object[]/Ext.util.Filter[]/String} filters The set of filters to apply to the data. These are stored internally on the store,
1164 * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See
1165 * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively,
1166 * pass in a property string
1167 * @param {String} value (optional) value to filter by (only if using a property string as the first argument)
1169 filter: function(filters, value) {
1170 if (Ext.isString(filters)) {
1178 decoded = me.decodeFilters(filters),
1180 doLocalSort = me.sortOnFilter && !me.remoteSort,
1181 length = decoded.length;
1183 for (; i < length; i++) {
1184 me.filters.replace(decoded[i]);
1187 if (me.remoteFilter) {
1188 //the load function will pick up the new filters and request the filtered data from the proxy
1192 * A pristine (unfiltered) collection of the records in this store. This is used to reinstate
1193 * records when a filter is removed or changed
1194 * @property snapshot
1195 * @type Ext.util.MixedCollection
1197 if (me.filters.getCount()) {
1198 me.snapshot = me.snapshot || me.data.clone();
1199 me.data = me.data.filter(me.filters.items);
1204 // fire datachanged event if it hasn't already been fired by doSort
1205 if (!doLocalSort || me.sorters.length < 1) {
1206 me.fireEvent('datachanged', me);
1213 * Revert to a view of the Record cache with no filtering applied.
1214 * @param {Boolean} suppressEvent If <tt>true</tt> the filter is cleared silently without firing the
1215 * {@link #datachanged} event.
1217 clearFilter: function(suppressEvent) {
1222 if (me.remoteFilter) {
1224 } else if (me.isFiltered()) {
1225 me.data = me.snapshot.clone();
1228 if (suppressEvent !== true) {
1229 me.fireEvent('datachanged', me);
1235 * Returns true if this store is currently filtered
1238 isFiltered: function() {
1239 var snapshot = this.snapshot;
1240 return !! snapshot && snapshot !== this.data;
1244 * Filter by a function. The specified function will be called for each
1245 * Record in this Store. If the function returns <tt>true</tt> the Record is included,
1246 * otherwise it is filtered out.
1247 * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1248 * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1249 * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1250 * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1252 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1254 filterBy: function(fn, scope) {
1257 me.snapshot = me.snapshot || me.data.clone();
1258 me.data = me.queryBy(fn, scope || me);
1259 me.fireEvent('datachanged', me);
1263 * Query the cached records in this Store using a filtering function. The specified function
1264 * will be called with each record in this Store. If the function returns <tt>true</tt> the record is
1265 * included in the results.
1266 * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1267 * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1268 * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1269 * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1271 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1272 * @return {Ext.util.MixedCollection} Returns an Ext.util.MixedCollection of the matched records
1274 queryBy: function(fn, scope) {
1276 data = me.snapshot || me.data;
1277 return data.filterBy(fn, scope || me);
1281 * Loads an array of data straight into the Store.
1283 * Using this method is great if the data is in the correct format already (e.g. it doesn't need to be
1284 * processed by a reader). If your data requires processing to decode the data structure, use a
1285 * {@link Ext.data.proxy.Memory MemoryProxy} instead.
1287 * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will be cast
1288 * into model instances.
1289 * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
1290 * to remove the old ones first.
1292 loadData: function(data, append) {
1293 var model = this.model,
1294 length = data.length,
1299 //make sure each data element is an Ext.data.Model instance
1300 for (i = 0; i < length; i++) {
1303 if (!(record instanceof Ext.data.Model)) {
1304 record = Ext.ModelManager.create(record, model);
1306 newData.push(record);
1309 this.loadRecords(newData, {addRecords: append});
1314 * Loads data via the bound Proxy's reader
1316 * Use this method if you are attempting to load data and want to utilize the configured data reader.
1318 * @param {Object[]} data The full JSON object you'd like to load into the Data store.
1319 * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
1320 * to remove the old ones first.
1322 loadRawData : function(data, append) {
1324 result = me.proxy.reader.read(data),
1325 records = result.records;
1327 if (result.success) {
1328 me.loadRecords(records, { addRecords: append });
1329 me.fireEvent('load', me, records, true);
1335 * Loads an array of {@link Ext.data.Model model} instances into the store, fires the datachanged event. This should only usually
1336 * be called internally when loading from the {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #add} instead
1337 * @param {Ext.data.Model[]} records The array of records to load
1338 * @param {Object} options {addRecords: true} to add these records to the existing records, false to remove the Store's existing records first
1340 loadRecords: function(records, options) {
1343 length = records.length;
1345 options = options || {};
1348 if (!options.addRecords) {
1353 me.data.addAll(records);
1355 //FIXME: this is not a good solution. Ed Spencer is totally responsible for this and should be forced to fix it immediately.
1356 for (; i < length; i++) {
1357 if (options.start !== undefined) {
1358 records[i].index = options.start + i;
1361 records[i].join(me);
1365 * this rather inelegant suspension and resumption of events is required because both the filter and sort functions
1366 * fire an additional datachanged event, which is not wanted. Ideally we would do this a different way. The first
1367 * datachanged event is fired by the call to this.add, above.
1371 if (me.filterOnLoad && !me.remoteFilter) {
1375 if (me.sortOnLoad && !me.remoteSort) {
1380 me.fireEvent('datachanged', me, records);
1385 * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
1386 * load operation, passing in calculated 'start' and 'limit' params
1387 * @param {Number} page The number of the page to load
1388 * @param {Object} options See options for {@link #load}
1390 loadPage: function(page, options) {
1392 options = Ext.apply({}, options);
1394 me.currentPage = page;
1396 me.read(Ext.applyIf(options, {
1398 start: (page - 1) * me.pageSize,
1400 addRecords: !me.clearOnPageLoad
1405 * Loads the next 'page' in the current data set
1406 * @param {Object} options See options for {@link #load}
1408 nextPage: function(options) {
1409 this.loadPage(this.currentPage + 1, options);
1413 * Loads the previous 'page' in the current data set
1414 * @param {Object} options See options for {@link #load}
1416 previousPage: function(options) {
1417 this.loadPage(this.currentPage - 1, options);
1421 clearData: function() {
1423 me.data.each(function(record) {
1432 * Prefetches data into the store using its configured {@link #proxy}.
1433 * @param {Object} options (Optional) config object, passed into the Ext.data.Operation object before loading.
1436 prefetch: function(options) {
1439 requestId = me.getRequestId();
1441 options = options || {};
1443 Ext.applyIf(options, {
1445 filters: me.filters.items,
1446 sorters: me.sorters.items,
1447 requestId: requestId
1449 me.pendingRequests.push(requestId);
1451 operation = Ext.create('Ext.data.Operation', options);
1453 // HACK to implement loadMask support.
1454 //if (operation.blocking) {
1455 // me.fireEvent('beforeload', me, operation);
1457 if (me.fireEvent('beforeprefetch', me, operation) !== false) {
1459 me.proxy.read(operation, me.onProxyPrefetch, me);
1466 * Prefetches a page of data.
1467 * @param {Number} page The page to prefetch
1468 * @param {Object} options (Optional) config object, passed into the Ext.data.Operation object before loading.
1471 prefetchPage: function(page, options) {
1473 pageSize = me.pageSize,
1474 start = (page - 1) * me.pageSize,
1475 end = start + pageSize;
1477 // Currently not requesting this page and range isn't already satisified
1478 if (Ext.Array.indexOf(me.pagesRequested, page) === -1 && !me.rangeSatisfied(start, end)) {
1479 options = options || {};
1480 me.pagesRequested.push(page);
1481 Ext.applyIf(options, {
1485 callback: me.onWaitForGuarantee,
1489 me.prefetch(options);
1495 * Returns a unique requestId to track requests.
1498 getRequestId: function() {
1499 this.requestSeed = this.requestSeed || 1;
1500 return this.requestSeed++;
1504 * Called after the configured proxy completes a prefetch operation.
1506 * @param {Ext.data.Operation} operation The operation that completed
1508 onProxyPrefetch: function(operation) {
1510 resultSet = operation.getResultSet(),
1511 records = operation.getRecords(),
1513 successful = operation.wasSuccessful();
1516 me.totalCount = resultSet.total;
1517 me.fireEvent('totalcountchange', me.totalCount);
1521 me.cacheRecords(records, operation);
1523 Ext.Array.remove(me.pendingRequests, operation.requestId);
1524 if (operation.page) {
1525 Ext.Array.remove(me.pagesRequested, operation.page);
1529 me.fireEvent('prefetch', me, records, successful, operation);
1531 // HACK to support loadMask
1532 if (operation.blocking) {
1533 me.fireEvent('load', me, records, successful);
1536 //this is a callback that would have been passed to the 'read' function and is optional
1537 Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
1541 * Caches the records in the prefetch and stripes them with their server-side
1544 * @param {Ext.data.Model[]} records The records to cache
1545 * @param {Ext.data.Operation} The associated operation
1547 cacheRecords: function(records, operation) {
1550 length = records.length,
1551 start = operation ? operation.start : 0;
1553 if (!Ext.isDefined(me.totalCount)) {
1554 me.totalCount = records.length;
1555 me.fireEvent('totalcountchange', me.totalCount);
1558 for (; i < length; i++) {
1559 // this is the true index, not the viewIndex
1560 records[i].index = start + i;
1563 me.prefetchData.addAll(records);
1564 if (me.purgePageCount) {
1572 * Purge the least recently used records in the prefetch if the purgeCount
1573 * has been exceeded.
1575 purgeRecords: function() {
1577 prefetchCount = me.prefetchData.getCount(),
1578 purgeCount = me.purgePageCount * me.pageSize,
1579 numRecordsToPurge = prefetchCount - purgeCount - 1,
1582 for (; i <= numRecordsToPurge; i++) {
1583 me.prefetchData.removeAt(0);
1588 * Determines if the range has already been satisfied in the prefetchData.
1590 * @param {Number} start The start index
1591 * @param {Number} end The end index in the range
1593 rangeSatisfied: function(start, end) {
1598 for (; i < end; i++) {
1599 if (!me.prefetchData.getByKey(i)) {
1602 if (end - i > me.pageSize) {
1603 Ext.Error.raise("A single page prefetch could never satisfy this request.");
1613 * Determines the page from a record index
1614 * @param {Number} index The record index
1615 * @return {Number} The page the record belongs to
1617 getPageFromRecordIndex: function(index) {
1618 return Math.floor(index / this.pageSize) + 1;
1622 * Handles a guaranteed range being loaded
1625 onGuaranteedRange: function() {
1627 totalCount = me.getTotalCount(),
1628 start = me.requestStart,
1629 end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd,
1634 end = Math.max(0, end);
1640 msg: 'Start (' + start + ') was greater than end (' + end +
1641 ') for the range of records requested (' + me.requestStart + '-' +
1642 me.requestEnd + ')' + (this.storeId ? ' from store "' + this.storeId + '"' : '')
1647 if (start !== me.guaranteedStart && end !== me.guaranteedEnd) {
1648 me.guaranteedStart = start;
1649 me.guaranteedEnd = end;
1651 for (; i <= end; i++) {
1652 record = me.prefetchData.getByKey(i);
1655 // Ext.log('Record with key "' + i + '" was not found and store said it was guaranteed');
1662 me.fireEvent('guaranteedrange', range, start, end);
1664 me.cb.call(me.scope || me, range);
1671 // hack to support loadmask
1674 this.fireEvent('beforeload');
1677 // hack to support loadmask
1678 unmask: function() {
1680 this.fireEvent('load');
1685 * Returns the number of pending requests out.
1687 hasPendingRequests: function() {
1688 return this.pendingRequests.length;
1692 // wait until all requests finish, until guaranteeing the range.
1693 onWaitForGuarantee: function() {
1694 if (!this.hasPendingRequests()) {
1695 this.onGuaranteedRange();
1700 * Guarantee a specific range, this will load the store with a range (that
1701 * must be the pageSize or smaller) and take care of any loading that may
1704 guaranteeRange: function(start, end, cb, scope) {
1707 if (end - start > this.pageSize) {
1711 pageSize: this.pageSize,
1712 msg: "Requested a bigger range than the specified pageSize"
1718 end = (end > this.totalCount) ? this.totalCount - 1 : end;
1722 prefetchData = me.prefetchData,
1724 startLoaded = !!prefetchData.getByKey(start),
1725 endLoaded = !!prefetchData.getByKey(end),
1726 startPage = me.getPageFromRecordIndex(start),
1727 endPage = me.getPageFromRecordIndex(end);
1732 me.requestStart = start;
1733 me.requestEnd = end;
1734 // neither beginning or end are loaded
1735 if (!startLoaded || !endLoaded) {
1736 // same page, lets load it
1737 if (startPage === endPage) {
1739 me.prefetchPage(startPage, {
1741 callback: me.onWaitForGuarantee,
1744 // need to load two pages
1747 me.prefetchPage(startPage, {
1749 callback: me.onWaitForGuarantee,
1752 me.prefetchPage(endPage, {
1754 callback: me.onWaitForGuarantee,
1758 // Request was already satisfied via the prefetch
1760 me.onGuaranteedRange();
1764 // because prefetchData is stored by index
1765 // this invalidates all of the prefetchedData
1768 prefetchData = me.prefetchData,
1775 if (me.remoteSort) {
1776 prefetchData.clear();
1777 me.callParent(arguments);
1779 sorters = me.getSorters();
1780 start = me.guaranteedStart;
1781 end = me.guaranteedEnd;
1783 if (sorters.length) {
1784 prefetchData.sort(sorters);
1785 range = prefetchData.getRange();
1786 prefetchData.clear();
1787 me.cacheRecords(range);
1788 delete me.guaranteedStart;
1789 delete me.guaranteedEnd;
1790 me.guaranteeRange(start, end);
1792 me.callParent(arguments);
1795 me.callParent(arguments);
1799 // overriden to provide striping of the indexes as sorting occurs.
1800 // this cannot be done inside of sort because datachanged has already
1801 // fired and will trigger a repaint of the bound view.
1802 doSort: function(sorterFn) {
1804 if (me.remoteSort) {
1805 //the load function will pick up the new sorters and request the sorted data from the proxy
1808 me.data.sortBy(sorterFn);
1810 var range = me.getRange(),
1813 for (; i < ln; i++) {
1817 me.fireEvent('datachanged', me);
1822 * Finds the index of the first matching Record in this store by a specific field value.
1823 * @param {String} fieldName The name of the Record field to test.
1824 * @param {String/RegExp} value Either a string that the field value
1825 * should begin with, or a RegExp to test against the field.
1826 * @param {Number} startIndex (optional) The index to start searching at
1827 * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
1828 * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
1829 * @param {Boolean} exactMatch (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
1830 * @return {Number} The matched index or -1
1832 find: function(property, value, start, anyMatch, caseSensitive, exactMatch) {
1833 var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch);
1834 return fn ? this.data.findIndexBy(fn, null, start) : -1;
1838 * Finds the first matching Record in this store by a specific field value.
1839 * @param {String} fieldName The name of the Record field to test.
1840 * @param {String/RegExp} value Either a string that the field value
1841 * should begin with, or a RegExp to test against the field.
1842 * @param {Number} startIndex (optional) The index to start searching at
1843 * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
1844 * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
1845 * @param {Boolean} exactMatch (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
1846 * @return {Ext.data.Model} The matched record or null
1848 findRecord: function() {
1850 index = me.find.apply(me, arguments);
1851 return index !== -1 ? me.getAt(index) : null;
1856 * Returns a filter function used to test a the given property's value. Defers most of the work to
1857 * Ext.util.MixedCollection's createValueMatcher function
1858 * @param {String} property The property to create the filter function for
1859 * @param {String/RegExp} value The string/regex to compare the property value to
1860 * @param {Boolean} [anyMatch=false] True if we don't care if the filter value is not the full value.
1861 * @param {Boolean} [caseSensitive=false] True to create a case-sensitive regex.
1862 * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters added to the regex).
1863 * Ignored if anyMatch is true.
1865 createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) {
1866 if (Ext.isEmpty(value)) {
1869 value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch);
1870 return function(r) {
1871 return value.test(r.data[property]);
1876 * Finds the index of the first matching Record in this store by a specific field value.
1877 * @param {String} fieldName The name of the Record field to test.
1878 * @param {Object} value The value to match the field against.
1879 * @param {Number} startIndex (optional) The index to start searching at
1880 * @return {Number} The matched index or -1
1882 findExact: function(property, value, start) {
1883 return this.data.findIndexBy(function(rec) {
1884 return rec.get(property) == value;
1890 * Find the index of the first matching Record in this Store by a function.
1891 * If the function returns <tt>true</tt> it is considered a match.
1892 * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1893 * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1894 * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1895 * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1897 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1898 * @param {Number} startIndex (optional) The index to start searching at
1899 * @return {Number} The matched index or -1
1901 findBy: function(fn, scope, start) {
1902 return this.data.findIndexBy(fn, scope, start);
1906 * Collects unique values for a particular dataIndex from this store.
1907 * @param {String} dataIndex The property to collect
1908 * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string values
1909 * @param {Boolean} bypassFilter (optional) Pass true to collect from all records, even ones which are filtered
1910 * @return {Object[]} An array of the unique values
1912 collect: function(dataIndex, allowNull, bypassFilter) {
1914 data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data;
1916 return data.collect(dataIndex, 'data', allowNull);
1920 * Gets the number of cached records.
1921 * <p>If using paging, this may not be the total size of the dataset. If the data object
1922 * used by the Reader contains the dataset size, then the {@link #getTotalCount} function returns
1923 * the dataset size. <b>Note</b>: see the Important note in {@link #load}.</p>
1924 * @return {Number} The number of Records in the Store's cache.
1926 getCount: function() {
1927 return this.data.length || 0;
1931 * Returns the total number of {@link Ext.data.Model Model} instances that the {@link Ext.data.proxy.Proxy Proxy}
1932 * indicates exist. This will usually differ from {@link #getCount} when using paging - getCount returns the
1933 * number of records loaded into the Store at the moment, getTotalCount returns the number of records that
1934 * could be loaded into the Store if the Store contained all data
1935 * @return {Number} The total number of Model instances available via the Proxy
1937 getTotalCount: function() {
1938 return this.totalCount;
1942 * Get the Record at the specified index.
1943 * @param {Number} index The index of the Record to find.
1944 * @return {Ext.data.Model} The Record at the passed index. Returns undefined if not found.
1946 getAt: function(index) {
1947 return this.data.getAt(index);
1951 * Returns a range of Records between specified indices.
1952 * @param {Number} [startIndex=0] The starting index
1953 * @param {Number} [endIndex] The ending index. Defaults to the last Record in the Store.
1954 * @return {Ext.data.Model[]} An array of Records
1956 getRange: function(start, end) {
1957 return this.data.getRange(start, end);
1961 * Get the Record with the specified id.
1962 * @param {String} id The id of the Record to find.
1963 * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
1965 getById: function(id) {
1966 return (this.snapshot || this.data).findBy(function(record) {
1967 return record.getId() === id;
1972 * Get the index within the cache of the passed Record.
1973 * @param {Ext.data.Model} record The Ext.data.Model object to find.
1974 * @return {Number} The index of the passed Record. Returns -1 if not found.
1976 indexOf: function(record) {
1977 return this.data.indexOf(record);
1982 * Get the index within the entire dataset. From 0 to the totalCount.
1983 * @param {Ext.data.Model} record The Ext.data.Model object to find.
1984 * @return {Number} The index of the passed Record. Returns -1 if not found.
1986 indexOfTotal: function(record) {
1987 var index = record.index;
1988 if (index || index === 0) {
1991 return this.indexOf(record);
1995 * Get the index within the cache of the Record with the passed id.
1996 * @param {String} id The id of the Record to find.
1997 * @return {Number} The index of the Record. Returns -1 if not found.
1999 indexOfId: function(id) {
2000 return this.indexOf(this.getById(id));
2004 * Remove all items from the store.
2005 * @param {Boolean} silent Prevent the `clear` event from being fired.
2007 removeAll: function(silent) {
2012 me.snapshot.clear();
2014 if (silent !== true) {
2015 me.fireEvent('clear', me);
2020 * Aggregation methods
2024 * Convenience function for getting the first model instance in the store
2025 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2026 * in the store. The value returned will be an object literal with the key being the group
2027 * name and the first record being the value. The grouped parameter is only honored if
2028 * the store has a groupField.
2029 * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
2031 first: function(grouped) {
2034 if (grouped && me.isGrouped()) {
2035 return me.aggregate(function(records) {
2036 return records.length ? records[0] : undefined;
2039 return me.data.first();
2044 * Convenience function for getting the last model instance in the store
2045 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2046 * in the store. The value returned will be an object literal with the key being the group
2047 * name and the last record being the value. The grouped parameter is only honored if
2048 * the store has a groupField.
2049 * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
2051 last: function(grouped) {
2054 if (grouped && me.isGrouped()) {
2055 return me.aggregate(function(records) {
2056 var len = records.length;
2057 return len ? records[len - 1] : undefined;
2060 return me.data.last();
2065 * Sums the value of <tt>property</tt> for each {@link Ext.data.Model record} between <tt>start</tt>
2066 * and <tt>end</tt> and returns the result.
2067 * @param {String} field A field in each record
2068 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2069 * in the store. The value returned will be an object literal with the key being the group
2070 * name and the sum for that group being the value. The grouped parameter is only honored if
2071 * the store has a groupField.
2072 * @return {Number} The sum
2074 sum: function(field, grouped) {
2077 if (grouped && me.isGrouped()) {
2078 return me.aggregate(me.getSum, me, true, [field]);
2080 return me.getSum(me.data.items, field);
2084 // @private, see sum
2085 getSum: function(records, field) {
2088 len = records.length;
2090 for (; i < len; ++i) {
2091 total += records[i].get(field);
2098 * Gets the count of items in the store.
2099 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2100 * in the store. The value returned will be an object literal with the key being the group
2101 * name and the count for each group being the value. The grouped parameter is only honored if
2102 * the store has a groupField.
2103 * @return {Number} the count
2105 count: function(grouped) {
2108 if (grouped && me.isGrouped()) {
2109 return me.aggregate(function(records) {
2110 return records.length;
2113 return me.getCount();
2118 * Gets the minimum value in the store.
2119 * @param {String} field The field in each record
2120 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2121 * in the store. The value returned will be an object literal with the key being the group
2122 * name and the minimum in the group being the value. The grouped parameter is only honored if
2123 * the store has a groupField.
2124 * @return {Object} The minimum value, if no items exist, undefined.
2126 min: function(field, grouped) {
2129 if (grouped && me.isGrouped()) {
2130 return me.aggregate(me.getMin, me, true, [field]);
2132 return me.getMin(me.data.items, field);
2136 // @private, see min
2137 getMin: function(records, field){
2139 len = records.length,
2143 min = records[0].get(field);
2146 for (; i < len; ++i) {
2147 value = records[i].get(field);
2156 * Gets the maximum value in the store.
2157 * @param {String} field The field in each record
2158 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2159 * in the store. The value returned will be an object literal with the key being the group
2160 * name and the maximum in the group being the value. The grouped parameter is only honored if
2161 * the store has a groupField.
2162 * @return {Object} The maximum value, if no items exist, undefined.
2164 max: function(field, grouped) {
2167 if (grouped && me.isGrouped()) {
2168 return me.aggregate(me.getMax, me, true, [field]);
2170 return me.getMax(me.data.items, field);
2174 // @private, see max
2175 getMax: function(records, field) {
2177 len = records.length,
2182 max = records[0].get(field);
2185 for (; i < len; ++i) {
2186 value = records[i].get(field);
2195 * Gets the average value in the store.
2196 * @param {String} field The field in each record
2197 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2198 * in the store. The value returned will be an object literal with the key being the group
2199 * name and the group average being the value. The grouped parameter is only honored if
2200 * the store has a groupField.
2201 * @return {Object} The average value, if no items exist, 0.
2203 average: function(field, grouped) {
2205 if (grouped && me.isGrouped()) {
2206 return me.aggregate(me.getAverage, me, true, [field]);
2208 return me.getAverage(me.data.items, field);
2212 // @private, see average
2213 getAverage: function(records, field) {
2215 len = records.length,
2218 if (records.length > 0) {
2219 for (; i < len; ++i) {
2220 sum += records[i].get(field);
2228 * Runs the aggregate function for all the records in the store.
2229 * @param {Function} fn The function to execute. The function is called with a single parameter,
2230 * an array of records for that group.
2231 * @param {Object} scope (optional) The scope to execute the function in. Defaults to the store.
2232 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2233 * in the store. The value returned will be an object literal with the key being the group
2234 * name and the group average being the value. The grouped parameter is only honored if
2235 * the store has a groupField.
2236 * @param {Array} args (optional) Any arguments to append to the function call
2237 * @return {Object} An object literal with the group names and their appropriate values.
2239 aggregate: function(fn, scope, grouped, args) {
2241 if (grouped && this.isGrouped()) {
2242 var groups = this.getGroups(),
2244 len = groups.length,
2248 for (; i < len; ++i) {
2250 out[group.name] = fn.apply(scope || this, [group.children].concat(args));
2254 return fn.apply(scope || this, [this.data.items].concat(args));
2258 // A dummy empty store with a fieldless Model defined in it.
2259 // Just for binding to Views which are instantiated with no Store defined.
2260 // They will be able to run and render fine, and be bound to a generated Store later.
2261 Ext.regStore('ext-empty-store', {fields: [], proxy: 'proxy'});