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 = new 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>
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 = new 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 = new 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
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.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 (defaults to false). 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 {Array} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details.
263 * @cfg {String} model The {@link Ext.data.Model} associated with this store
267 * The (optional) field by which to group data in the store. Internally, grouping is very similar to sorting - the
268 * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
269 * level of grouping, and groups can be fetched via the {@link #getGroups} method.
270 * @property groupField
273 groupField: undefined,
276 * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC"
283 * @cfg {Number} pageSize
284 * The number of records considered to form a 'page'. This is used to power the built-in
285 * paging using the nextPage and previousPage functions. Defaults to 25.
290 * The page that the Store has most recently loaded (see {@link #loadPage})
291 * @property currentPage
297 * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage},
298 * {@link #nextPage} or {@link #previousPage} (defaults to true). Setting to false keeps existing records, allowing
299 * large data sets to be loaded one page at a time but rendered all together.
301 clearOnPageLoad: true,
304 * True if the Store is currently loading via its Proxy
312 * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called,
313 * causing the sorters to be reapplied after filtering. Defaults to true
318 * @cfg {Boolean} buffered
319 * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will
320 * tell the store to pre-fetch records ahead of a time.
325 * @cfg {Number} purgePageCount
326 * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data.
327 * This option is only relevant when the {@link #buffered} option is set to true.
335 * @param {Object} config (optional) Config object
337 constructor: function(config) {
338 config = config || {};
341 groupers = config.groupers || me.groupers,
342 groupField = config.groupField || me.groupField,
346 if (config.buffered || me.buffered) {
347 me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) {
350 me.pendingRequests = [];
351 me.pagesRequested = [];
353 me.sortOnLoad = false;
354 me.filterOnLoad = false;
359 * @event beforeprefetch
360 * Fires before a prefetch occurs. Return false to cancel.
361 * @param {Ext.data.store} this
362 * @param {Ext.data.Operation} operation The associated operation
367 * Fired whenever the grouping in the grid changes
368 * @param {Ext.data.Store} store The store
369 * @param {Array} groupers The array of grouper objects
374 * Fires whenever records have been prefetched
375 * @param {Ext.data.store} this
376 * @param {Array} records An array of records
377 * @param {Boolean} successful True if the operation was successful.
378 * @param {Ext.data.Operation} operation The associated operation
382 data = config.data || me.data;
385 * The MixedCollection that holds this store's local cache of records
387 * @type Ext.util.MixedCollection
389 me.data = Ext.create('Ext.util.MixedCollection', false, function(record) {
390 return record.internalId;
394 me.inlineData = data;
398 if (!groupers && groupField) {
400 property : groupField,
401 direction: config.groupDir || me.groupDir
404 delete config.groupers;
407 * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store
409 * @type Ext.util.MixedCollection
411 me.groupers = Ext.create('Ext.util.MixedCollection');
412 me.groupers.addAll(me.decodeGroupers(groupers));
414 this.callParent([config]);
415 // don't use *config* anymore from here on... use *me* instead...
417 if (me.groupers.items.length) {
418 me.sort(me.groupers.items, 'prepend', false);
422 data = me.inlineData;
425 if (proxy instanceof Ext.data.proxy.Memory) {
429 me.add.apply(me, data);
433 delete me.inlineData;
434 } else if (me.autoLoad) {
435 Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]);
436 // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here.
437 // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined);
441 onBeforeSort: function() {
442 this.sort(this.groupers.items, 'prepend', false);
447 * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances
448 * @param {Array} groupers The groupers array
449 * @return {Array} Array of Ext.util.Grouper objects
451 decodeGroupers: function(groupers) {
452 if (!Ext.isArray(groupers)) {
453 if (groupers === undefined) {
456 groupers = [groupers];
460 var length = groupers.length,
461 Grouper = Ext.util.Grouper,
464 for (i = 0; i < length; i++) {
465 config = groupers[i];
467 if (!(config instanceof Grouper)) {
468 if (Ext.isString(config)) {
474 Ext.applyIf(config, {
479 //support for 3.x style sorters where a function can be defined as 'fn'
481 config.sorterFn = config.fn;
484 //support a function to be passed as a sorter definition
485 if (typeof config == 'function') {
491 groupers[i] = new Grouper(config);
499 * Group data in the store
500 * @param {String|Array} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model},
501 * or an Array of grouper configurations.
502 * @param {String} direction The overall direction to group the data by. Defaults to "ASC".
504 group: function(groupers, direction) {
509 if (Ext.isArray(groupers)) {
510 newGroupers = groupers;
511 } else if (Ext.isObject(groupers)) {
512 newGroupers = [groupers];
513 } else if (Ext.isString(groupers)) {
514 grouper = me.groupers.get(groupers);
521 newGroupers = [grouper];
522 } else if (direction === undefined) {
525 grouper.setDirection(direction);
529 if (newGroupers && newGroupers.length) {
530 newGroupers = me.decodeGroupers(newGroupers);
532 me.groupers.addAll(newGroupers);
535 if (me.remoteGroup) {
538 callback: me.fireGroupChange
542 me.fireEvent('groupchange', me, me.groupers);
547 * Clear any groupers in the store
549 clearGrouping: function(){
551 // Clear any groupers we pushed on to the sorters
552 me.groupers.each(function(grouper){
553 me.sorters.remove(grouper);
556 if (me.remoteGroup) {
559 callback: me.fireGroupChange
563 me.fireEvent('groupchange', me, me.groupers);
568 * Checks if the store is currently grouped
569 * @return {Boolean} True if the store is grouped.
571 isGrouped: function() {
572 return this.groupers.getCount() > 0;
576 * Fires the groupchange event. Abstracted out so we can use it
580 fireGroupChange: function(){
581 this.fireEvent('groupchange', this, this.groupers);
585 * Returns an object containing the result of applying grouping to the records in this store. See {@link #groupField},
586 * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field:
588 var myStore = new Ext.data.Store({
593 myStore.getGroups(); //returns:
598 //all records where the color field is 'yellow'
604 //all records where the color field is 'red'
609 * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString}
610 * @return {Array} The grouped data
612 getGroups: function(requestGroupString) {
613 var records = this.data.items,
614 length = records.length,
622 for (i = 0; i < length; i++) {
624 groupStr = this.getGroupString(record);
625 group = pointers[groupStr];
627 if (group === undefined) {
634 pointers[groupStr] = group;
637 group.children.push(record);
640 return requestGroupString ? pointers[requestGroupString] : groups;
645 * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records
646 * matching a certain group.
648 getGroupsForGrouper: function(records, grouper) {
649 var length = records.length,
657 for (i = 0; i < length; i++) {
659 newValue = grouper.getGroupString(record);
661 if (newValue !== oldValue) {
670 group.records.push(record);
680 * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for
681 * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by
682 * {@link #getGroupsForGrouper} - this function largely just handles the recursion.
683 * @param {Array} records The set or subset of records to group
684 * @param {Number} grouperIndex The grouper index to retrieve
685 * @return {Array} The grouped records
687 getGroupsForGrouperIndex: function(records, grouperIndex) {
689 groupers = me.groupers,
690 grouper = groupers.getAt(grouperIndex),
691 groups = me.getGroupsForGrouper(records, grouper),
692 length = groups.length,
695 if (grouperIndex + 1 < groupers.length) {
696 for (i = 0; i < length; i++) {
697 groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1);
701 for (i = 0; i < length; i++) {
702 groups[i].depth = grouperIndex;
710 * <p>Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in
711 * this case grouping by genre and then author in a fictional books dataset):</p>
718 //book1, book2, book3, book4
739 * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping
740 * function correctly so this should only be set to false if the Store is known to already be sorted correctly
742 * @return {Array} The group data
744 getGroupData: function(sort) {
746 if (sort !== false) {
750 return me.getGroupsForGrouperIndex(me.data.items, 0);
754 * <p>Returns the string to group on for a given model instance. The default implementation of this method returns
755 * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to
756 * group by the first letter of a model's 'name' field, use the following code:</p>
760 getGroupString: function(instance) {
761 return instance.get('name')[0];
765 * @param {Ext.data.Model} instance The model instance
766 * @return {String} The string to compare when forming groups
768 getGroupString: function(instance) {
769 var group = this.groupers.first();
771 return instance.get(group.property);
776 * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
777 * See also <code>{@link #add}</code>.
778 * @param {Number} index The start index at which to insert the passed Records.
779 * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
781 insert: function(index, records) {
788 records = [].concat(records);
789 for (i = 0, len = records.length; i < len; i++) {
790 record = me.createModel(records[i]);
791 record.set(me.modelDefaults);
792 // reassign the model in the array in case it wasn't created yet
795 me.data.insert(index + i, record);
798 sync = sync || record.phantom === true;
802 me.snapshot.addAll(records);
805 me.fireEvent('add', me, records, index);
806 me.fireEvent('datachanged', me);
807 if (me.autoSync && sync) {
813 * Adds Model instances to the Store by instantiating them based on a JavaScript object. When adding already-
814 * instantiated Models, use {@link #insert} instead. The instances will be added at the end of the existing collection.
815 * This method accepts either a single argument array of Model instances or any number of model instance arguments.
819 myStore.add({some: 'data'}, {some: 'other data'});
822 * @param {Object} data The data for each model
823 * @return {Array} The array of newly created model instances
825 add: function(records) {
826 //accept both a single-argument array of records, or any number of record arguments
827 if (!Ext.isArray(records)) {
828 records = Array.prototype.slice.apply(arguments);
833 length = records.length,
836 for (; i < length; i++) {
837 record = me.createModel(records[i]);
838 // reassign the model in the array in case it wasn't created yet
842 me.insert(me.data.length, records);
848 * Converts a literal to a model, if it's not a model already
850 * @param record {Ext.data.Model/Object} The record to create
851 * @return {Ext.data.Model}
853 createModel: function(record) {
854 if (!record.isModel) {
855 record = Ext.ModelManager.create(record, this.model);
862 * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
863 * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed as the first parameter.
864 * Returning <tt>false</tt> aborts and exits the iteration.
865 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed.
866 * Defaults to the current {@link Ext.data.Model Record} in the iteration.
868 each: function(fn, scope) {
869 this.data.each(fn, scope);
873 * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single
874 * 'datachanged' event after removal.
875 * @param {Ext.data.Model/Array} records The Ext.data.Model instance or array of instances to remove
877 remove: function(records, /* private */ isMove) {
878 if (!Ext.isArray(records)) {
883 * Pass the isMove parameter if we know we're going to be re-inserting this record
885 isMove = isMove === true;
889 length = records.length,
894 for (; i < length; i++) {
896 index = me.data.indexOf(record);
899 me.snapshot.remove(record);
903 isPhantom = record.phantom === true;
904 if (!isMove && !isPhantom) {
905 // don't push phantom records onto removed
906 me.removed.push(record);
910 me.data.remove(record);
911 sync = sync || !isPhantom;
913 me.fireEvent('remove', me, record, index);
917 me.fireEvent('datachanged', me);
918 if (!isMove && me.autoSync && sync) {
924 * Removes the model instance at the given index
925 * @param {Number} index The record index
927 removeAt: function(index) {
928 var record = this.getAt(index);
936 * <p>Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
937 * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
938 * instances into the Store and calling an optional callback if required. Example usage:</p>
943 callback: function(records, operation, success) {
944 //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
945 console.log(records);
950 * <p>If the callback scope does not need to be set, a function can simply be passed:</p>
953 store.load(function(records, operation, success) {
954 console.log('loaded records');
958 * @param {Object/Function} options Optional config object, passed into the Ext.data.Operation object before loading.
960 load: function(options) {
963 options = options || {};
965 if (Ext.isFunction(options)) {
971 Ext.applyIf(options, {
972 groupers: me.groupers.items,
973 page: me.currentPage,
974 start: (me.currentPage - 1) * me.pageSize,
979 return me.callParent([options]);
984 * Called internally when a Proxy has completed a load request
986 onProxyLoad: function(operation) {
988 resultSet = operation.getResultSet(),
989 records = operation.getRecords(),
990 successful = operation.wasSuccessful();
993 me.totalCount = resultSet.total;
997 me.loadRecords(records, operation);
1001 me.fireEvent('load', me, records, successful);
1003 //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not.
1004 //People are definitely using this so can't deprecate safely until 2.x
1005 me.fireEvent('read', me, records, operation.wasSuccessful());
1007 //this is a callback that would have been passed to the 'read' function and is optional
1008 Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
1012 * Create any new records when a write is returned from the server.
1014 * @param {Array} records The array of new records
1015 * @param {Ext.data.Operation} operation The operation that just completed
1016 * @param {Boolean} success True if the operation was successful
1018 onCreateRecords: function(records, operation, success) {
1022 snapshot = this.snapshot,
1023 length = records.length,
1024 originalRecords = operation.records,
1030 * Loop over each record returned from the server. Assume they are
1031 * returned in order of how they were sent. If we find a matching
1032 * record, replace it with the newly created one.
1034 for (; i < length; ++i) {
1035 record = records[i];
1036 original = originalRecords[i];
1038 index = data.indexOf(original);
1040 data.removeAt(index);
1041 data.insert(index, record);
1044 index = snapshot.indexOf(original);
1046 snapshot.removeAt(index);
1047 snapshot.insert(index, record);
1050 record.phantom = false;
1058 * Update any records when a write is returned from the server.
1060 * @param {Array} records The array of updated records
1061 * @param {Ext.data.Operation} operation The operation that just completed
1062 * @param {Boolean} success True if the operation was successful
1064 onUpdateRecords: function(records, operation, success){
1067 length = records.length,
1069 snapshot = this.snapshot,
1072 for (; i < length; ++i) {
1073 record = records[i];
1074 data.replace(record);
1076 snapshot.replace(record);
1084 * Remove any records when a write is returned from the server.
1086 * @param {Array} records The array of removed records
1087 * @param {Ext.data.Operation} operation The operation that just completed
1088 * @param {Boolean} success True if the operation was successful
1090 onDestroyRecords: function(records, operation, success){
1094 length = records.length,
1096 snapshot = me.snapshot,
1099 for (; i < length; ++i) {
1100 record = records[i];
1102 data.remove(record);
1104 snapshot.remove(record);
1112 getNewRecords: function() {
1113 return this.data.filterBy(this.filterNew).items;
1117 getUpdatedRecords: function() {
1118 return this.data.filterBy(this.filterUpdated).items;
1122 * Filters the loaded set of records by a given set of filters.
1123 * @param {Mixed} filters The set of filters to apply to the data. These are stored internally on the store,
1124 * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See
1125 * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively,
1126 * pass in a property string
1127 * @param {String} value Optional value to filter by (only if using a property string as the first argument)
1129 filter: function(filters, value) {
1130 if (Ext.isString(filters)) {
1138 decoded = me.decodeFilters(filters),
1140 doLocalSort = me.sortOnFilter && !me.remoteSort,
1141 length = decoded.length;
1143 for (; i < length; i++) {
1144 me.filters.replace(decoded[i]);
1147 if (me.remoteFilter) {
1148 //the load function will pick up the new filters and request the filtered data from the proxy
1152 * A pristine (unfiltered) collection of the records in this store. This is used to reinstate
1153 * records when a filter is removed or changed
1154 * @property snapshot
1155 * @type Ext.util.MixedCollection
1157 if (me.filters.getCount()) {
1158 me.snapshot = me.snapshot || me.data.clone();
1159 me.data = me.data.filter(me.filters.items);
1164 // fire datachanged event if it hasn't already been fired by doSort
1165 if (!doLocalSort || me.sorters.length < 1) {
1166 me.fireEvent('datachanged', me);
1173 * Revert to a view of the Record cache with no filtering applied.
1174 * @param {Boolean} suppressEvent If <tt>true</tt> the filter is cleared silently without firing the
1175 * {@link #datachanged} event.
1177 clearFilter: function(suppressEvent) {
1182 if (me.remoteFilter) {
1184 } else if (me.isFiltered()) {
1185 me.data = me.snapshot.clone();
1188 if (suppressEvent !== true) {
1189 me.fireEvent('datachanged', me);
1195 * Returns true if this store is currently filtered
1198 isFiltered: function() {
1199 var snapshot = this.snapshot;
1200 return !! snapshot && snapshot !== this.data;
1204 * Filter by a function. The specified function will be called for each
1205 * Record in this Store. If the function returns <tt>true</tt> the Record is included,
1206 * otherwise it is filtered out.
1207 * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1208 * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1209 * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1210 * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1212 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1214 filterBy: function(fn, scope) {
1217 me.snapshot = me.snapshot || me.data.clone();
1218 me.data = me.queryBy(fn, scope || me);
1219 me.fireEvent('datachanged', me);
1223 * Query the cached records in this Store using a filtering function. The specified function
1224 * will be called with each record in this Store. If the function returns <tt>true</tt> the record is
1225 * included in the results.
1226 * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1227 * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1228 * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1229 * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1231 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1232 * @return {MixedCollection} Returns an Ext.util.MixedCollection of the matched records
1234 queryBy: function(fn, scope) {
1236 data = me.snapshot || me.data;
1237 return data.filterBy(fn, scope || me);
1241 * Loads an array of data straight into the Store
1242 * @param {Array} data Array of data to load. Any non-model instances will be cast into model instances first
1243 * @param {Boolean} append True to add the records to the existing records in the store, false to remove the old ones first
1245 loadData: function(data, append) {
1246 var model = this.model,
1247 length = data.length,
1251 //make sure each data element is an Ext.data.Model instance
1252 for (i = 0; i < length; i++) {
1255 if (! (record instanceof Ext.data.Model)) {
1256 data[i] = Ext.ModelManager.create(record, model);
1260 this.loadRecords(data, {addRecords: append});
1264 * Loads an array of {@Ext.data.Model model} instances into the store, fires the datachanged event. This should only usually
1265 * be called internally when loading from the {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #add} instead
1266 * @param {Array} records The array of records to load
1267 * @param {Object} options {addRecords: true} to add these records to the existing records, false to remove the Store's existing records first
1269 loadRecords: function(records, options) {
1272 length = records.length;
1274 options = options || {};
1277 if (!options.addRecords) {
1282 me.data.addAll(records);
1284 //FIXME: this is not a good solution. Ed Spencer is totally responsible for this and should be forced to fix it immediately.
1285 for (; i < length; i++) {
1286 if (options.start !== undefined) {
1287 records[i].index = options.start + i;
1290 records[i].join(me);
1294 * this rather inelegant suspension and resumption of events is required because both the filter and sort functions
1295 * fire an additional datachanged event, which is not wanted. Ideally we would do this a different way. The first
1296 * datachanged event is fired by the call to this.add, above.
1300 if (me.filterOnLoad && !me.remoteFilter) {
1304 if (me.sortOnLoad && !me.remoteSort) {
1309 me.fireEvent('datachanged', me, records);
1314 * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
1315 * load operation, passing in calculated 'start' and 'limit' params
1316 * @param {Number} page The number of the page to load
1318 loadPage: function(page) {
1321 me.currentPage = page;
1325 start: (page - 1) * me.pageSize,
1327 addRecords: !me.clearOnPageLoad
1332 * Loads the next 'page' in the current data set
1334 nextPage: function() {
1335 this.loadPage(this.currentPage + 1);
1339 * Loads the previous 'page' in the current data set
1341 previousPage: function() {
1342 this.loadPage(this.currentPage - 1);
1346 clearData: function() {
1347 this.data.each(function(record) {
1356 * Prefetches data the Store using its configured {@link #proxy}.
1357 * @param {Object} options Optional config object, passed into the Ext.data.Operation object before loading.
1360 prefetch: function(options) {
1363 requestId = me.getRequestId();
1365 options = options || {};
1367 Ext.applyIf(options, {
1369 filters: me.filters.items,
1370 sorters: me.sorters.items,
1371 requestId: requestId
1373 me.pendingRequests.push(requestId);
1375 operation = Ext.create('Ext.data.Operation', options);
1377 // HACK to implement loadMask support.
1378 //if (operation.blocking) {
1379 // me.fireEvent('beforeload', me, operation);
1381 if (me.fireEvent('beforeprefetch', me, operation) !== false) {
1383 me.proxy.read(operation, me.onProxyPrefetch, me);
1390 * Prefetches a page of data.
1391 * @param {Number} page The page to prefetch
1392 * @param {Object} options Optional config object, passed into the Ext.data.Operation object before loading.
1396 prefetchPage: function(page, options) {
1398 pageSize = me.pageSize,
1399 start = (page - 1) * me.pageSize,
1400 end = start + pageSize;
1402 // Currently not requesting this page and range isn't already satisified
1403 if (Ext.Array.indexOf(me.pagesRequested, page) === -1 && !me.rangeSatisfied(start, end)) {
1404 options = options || {};
1405 me.pagesRequested.push(page);
1406 Ext.applyIf(options, {
1410 callback: me.onWaitForGuarantee,
1414 me.prefetch(options);
1420 * Returns a unique requestId to track requests.
1423 getRequestId: function() {
1424 this.requestSeed = this.requestSeed || 1;
1425 return this.requestSeed++;
1429 * Handles a success pre-fetch
1431 * @param {Ext.data.Operation} operation The operation that completed
1433 onProxyPrefetch: function(operation) {
1435 resultSet = operation.getResultSet(),
1436 records = operation.getRecords(),
1438 successful = operation.wasSuccessful();
1441 me.totalCount = resultSet.total;
1442 me.fireEvent('totalcountchange', me.totalCount);
1446 me.cacheRecords(records, operation);
1448 Ext.Array.remove(me.pendingRequests, operation.requestId);
1449 if (operation.page) {
1450 Ext.Array.remove(me.pagesRequested, operation.page);
1454 me.fireEvent('prefetch', me, records, successful, operation);
1456 // HACK to support loadMask
1457 if (operation.blocking) {
1458 me.fireEvent('load', me, records, successful);
1461 //this is a callback that would have been passed to the 'read' function and is optional
1462 Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
1466 * Caches the records in the prefetch and stripes them with their server-side
1469 * @param {Array} records The records to cache
1470 * @param {Ext.data.Operation} The associated operation
1472 cacheRecords: function(records, operation) {
1475 length = records.length,
1476 start = operation ? operation.start : 0;
1478 if (!Ext.isDefined(me.totalCount)) {
1479 me.totalCount = records.length;
1480 me.fireEvent('totalcountchange', me.totalCount);
1483 for (; i < length; i++) {
1484 // this is the true index, not the viewIndex
1485 records[i].index = start + i;
1488 me.prefetchData.addAll(records);
1489 if (me.purgePageCount) {
1497 * Purge the least recently used records in the prefetch if the purgeCount
1498 * has been exceeded.
1500 purgeRecords: function() {
1502 prefetchCount = me.prefetchData.getCount(),
1503 purgeCount = me.purgePageCount * me.pageSize,
1504 numRecordsToPurge = prefetchCount - purgeCount - 1,
1507 for (; i <= numRecordsToPurge; i++) {
1508 me.prefetchData.removeAt(0);
1513 * Determines if the range has already been satisfied in the prefetchData.
1515 * @param {Number} start The start index
1516 * @param {Number} end The end index in the range
1518 rangeSatisfied: function(start, end) {
1523 for (; i < end; i++) {
1524 if (!me.prefetchData.getByKey(i)) {
1527 if (end - i > me.pageSize) {
1528 Ext.Error.raise("A single page prefetch could never satisfy this request.");
1538 * Determines the page from a record index
1539 * @param {Number} index The record index
1540 * @return {Number} The page the record belongs to
1542 getPageFromRecordIndex: function(index) {
1543 return Math.floor(index / this.pageSize) + 1;
1547 * Handles a guaranteed range being loaded
1550 onGuaranteedRange: function() {
1552 totalCount = me.getTotalCount(),
1553 start = me.requestStart,
1554 end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd,
1561 Ext.Error.raise("Start (" + start + ") was greater than end (" + end + ")");
1565 if (start !== me.guaranteedStart && end !== me.guaranteedEnd) {
1566 me.guaranteedStart = start;
1567 me.guaranteedEnd = end;
1569 for (; i <= end; i++) {
1570 record = me.prefetchData.getByKey(i);
1573 Ext.Error.raise("Record was not found and store said it was guaranteed");
1578 me.fireEvent('guaranteedrange', range, start, end);
1580 me.cb.call(me.scope || me, range);
1587 // hack to support loadmask
1590 this.fireEvent('beforeload');
1593 // hack to support loadmask
1594 unmask: function() {
1596 this.fireEvent('load');
1601 * Returns the number of pending requests out.
1603 hasPendingRequests: function() {
1604 return this.pendingRequests.length;
1608 // wait until all requests finish, until guaranteeing the range.
1609 onWaitForGuarantee: function() {
1610 if (!this.hasPendingRequests()) {
1611 this.onGuaranteedRange();
1616 * Guarantee a specific range, this will load the store with a range (that
1617 * must be the pageSize or smaller) and take care of any loading that may
1620 guaranteeRange: function(start, end, cb, scope) {
1623 if (end - start > this.pageSize) {
1627 pageSize: this.pageSize,
1628 msg: "Requested a bigger range than the specified pageSize"
1634 end = (end > this.totalCount) ? this.totalCount - 1 : end;
1638 prefetchData = me.prefetchData,
1640 startLoaded = !!prefetchData.getByKey(start),
1641 endLoaded = !!prefetchData.getByKey(end),
1642 startPage = me.getPageFromRecordIndex(start),
1643 endPage = me.getPageFromRecordIndex(end);
1648 me.requestStart = start;
1649 me.requestEnd = end;
1650 // neither beginning or end are loaded
1651 if (!startLoaded || !endLoaded) {
1652 // same page, lets load it
1653 if (startPage === endPage) {
1655 me.prefetchPage(startPage, {
1657 callback: me.onWaitForGuarantee,
1660 // need to load two pages
1663 me.prefetchPage(startPage, {
1665 callback: me.onWaitForGuarantee,
1668 me.prefetchPage(endPage, {
1670 callback: me.onWaitForGuarantee,
1674 // Request was already satisfied via the prefetch
1676 me.onGuaranteedRange();
1680 // because prefetchData is stored by index
1681 // this invalidates all of the prefetchedData
1684 prefetchData = me.prefetchData,
1691 if (me.remoteSort) {
1692 prefetchData.clear();
1693 me.callParent(arguments);
1695 sorters = me.getSorters();
1696 start = me.guaranteedStart;
1697 end = me.guaranteedEnd;
1699 if (sorters.length) {
1700 prefetchData.sort(sorters);
1701 range = prefetchData.getRange();
1702 prefetchData.clear();
1703 me.cacheRecords(range);
1704 delete me.guaranteedStart;
1705 delete me.guaranteedEnd;
1706 me.guaranteeRange(start, end);
1708 me.callParent(arguments);
1711 me.callParent(arguments);
1715 // overriden to provide striping of the indexes as sorting occurs.
1716 // this cannot be done inside of sort because datachanged has already
1717 // fired and will trigger a repaint of the bound view.
1718 doSort: function(sorterFn) {
1720 if (me.remoteSort) {
1721 //the load function will pick up the new sorters and request the sorted data from the proxy
1724 me.data.sortBy(sorterFn);
1726 var range = me.getRange(),
1729 for (; i < ln; i++) {
1733 me.fireEvent('datachanged', me);
1738 * Finds the index of the first matching Record in this store by a specific field value.
1739 * @param {String} fieldName The name of the Record field to test.
1740 * @param {String/RegExp} value Either a string that the field value
1741 * should begin with, or a RegExp to test against the field.
1742 * @param {Number} startIndex (optional) The index to start searching at
1743 * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
1744 * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
1745 * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false.
1746 * @return {Number} The matched index or -1
1748 find: function(property, value, start, anyMatch, caseSensitive, exactMatch) {
1749 var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch);
1750 return fn ? this.data.findIndexBy(fn, null, start) : -1;
1754 * Finds the first matching Record in this store by a specific field value.
1755 * @param {String} fieldName The name of the Record field to test.
1756 * @param {String/RegExp} value Either a string that the field value
1757 * should begin with, or a RegExp to test against the field.
1758 * @param {Number} startIndex (optional) The index to start searching at
1759 * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
1760 * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
1761 * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false.
1762 * @return {Ext.data.Model} The matched record or null
1764 findRecord: function() {
1766 index = me.find.apply(me, arguments);
1767 return index !== -1 ? me.getAt(index) : null;
1772 * Returns a filter function used to test a the given property's value. Defers most of the work to
1773 * Ext.util.MixedCollection's createValueMatcher function
1774 * @param {String} property The property to create the filter function for
1775 * @param {String/RegExp} value The string/regex to compare the property value to
1776 * @param {Boolean} anyMatch True if we don't care if the filter value is not the full value (defaults to false)
1777 * @param {Boolean} caseSensitive True to create a case-sensitive regex (defaults to false)
1778 * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false.
1779 * Ignored if anyMatch is true.
1781 createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) {
1782 if (Ext.isEmpty(value)) {
1785 value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch);
1786 return function(r) {
1787 return value.test(r.data[property]);
1792 * Finds the index of the first matching Record in this store by a specific field value.
1793 * @param {String} fieldName The name of the Record field to test.
1794 * @param {Mixed} value The value to match the field against.
1795 * @param {Number} startIndex (optional) The index to start searching at
1796 * @return {Number} The matched index or -1
1798 findExact: function(property, value, start) {
1799 return this.data.findIndexBy(function(rec) {
1800 return rec.get(property) === value;
1806 * Find the index of the first matching Record in this Store by a function.
1807 * If the function returns <tt>true</tt> it is considered a match.
1808 * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
1809 * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
1810 * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
1811 * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
1813 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
1814 * @param {Number} startIndex (optional) The index to start searching at
1815 * @return {Number} The matched index or -1
1817 findBy: function(fn, scope, start) {
1818 return this.data.findIndexBy(fn, scope, start);
1822 * Collects unique values for a particular dataIndex from this store.
1823 * @param {String} dataIndex The property to collect
1824 * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string values
1825 * @param {Boolean} bypassFilter (optional) Pass true to collect from all records, even ones which are filtered
1826 * @return {Array} An array of the unique values
1828 collect: function(dataIndex, allowNull, bypassFilter) {
1830 data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data;
1832 return data.collect(dataIndex, 'data', allowNull);
1836 * Gets the number of cached records.
1837 * <p>If using paging, this may not be the total size of the dataset. If the data object
1838 * used by the Reader contains the dataset size, then the {@link #getTotalCount} function returns
1839 * the dataset size. <b>Note</b>: see the Important note in {@link #load}.</p>
1840 * @return {Number} The number of Records in the Store's cache.
1842 getCount: function() {
1843 return this.data.length || 0;
1847 * Returns the total number of {@link Ext.data.Model Model} instances that the {@link Ext.data.proxy.Proxy Proxy}
1848 * indicates exist. This will usually differ from {@link #getCount} when using paging - getCount returns the
1849 * number of records loaded into the Store at the moment, getTotalCount returns the number of records that
1850 * could be loaded into the Store if the Store contained all data
1851 * @return {Number} The total number of Model instances available via the Proxy
1853 getTotalCount: function() {
1854 return this.totalCount;
1858 * Get the Record at the specified index.
1859 * @param {Number} index The index of the Record to find.
1860 * @return {Ext.data.Model} The Record at the passed index. Returns undefined if not found.
1862 getAt: function(index) {
1863 return this.data.getAt(index);
1867 * Returns a range of Records between specified indices.
1868 * @param {Number} startIndex (optional) The starting index (defaults to 0)
1869 * @param {Number} endIndex (optional) The ending index (defaults to the last Record in the Store)
1870 * @return {Ext.data.Model[]} An array of Records
1872 getRange: function(start, end) {
1873 return this.data.getRange(start, end);
1877 * Get the Record with the specified id.
1878 * @param {String} id The id of the Record to find.
1879 * @return {Ext.data.Model} The Record with the passed id. Returns undefined if not found.
1881 getById: function(id) {
1882 return (this.snapshot || this.data).findBy(function(record) {
1883 return record.getId() === id;
1888 * Get the index within the cache of the passed Record.
1889 * @param {Ext.data.Model} record The Ext.data.Model object to find.
1890 * @return {Number} The index of the passed Record. Returns -1 if not found.
1892 indexOf: function(record) {
1893 return this.data.indexOf(record);
1898 * Get the index within the entire dataset. From 0 to the totalCount.
1899 * @param {Ext.data.Model} record The Ext.data.Model object to find.
1900 * @return {Number} The index of the passed Record. Returns -1 if not found.
1902 indexOfTotal: function(record) {
1903 return record.index || this.indexOf(record);
1907 * Get the index within the cache of the Record with the passed id.
1908 * @param {String} id The id of the Record to find.
1909 * @return {Number} The index of the Record. Returns -1 if not found.
1911 indexOfId: function(id) {
1912 return this.data.indexOfKey(id);
1916 * Remove all items from the store.
1917 * @param {Boolean} silent Prevent the `clear` event from being fired.
1919 removeAll: function(silent) {
1924 me.snapshot.clear();
1926 if (silent !== true) {
1927 me.fireEvent('clear', me);
1932 * Aggregation methods
1936 * Convenience function for getting the first model instance in the store
1937 * @param {Boolean} grouped (Optional) True to perform the operation for each group
1938 * in the store. The value returned will be an object literal with the key being the group
1939 * name and the first record being the value. The grouped parameter is only honored if
1940 * the store has a groupField.
1941 * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
1943 first: function(grouped) {
1946 if (grouped && me.isGrouped()) {
1947 return me.aggregate(function(records) {
1948 return records.length ? records[0] : undefined;
1951 return me.data.first();
1956 * Convenience function for getting the last model instance in the store
1957 * @param {Boolean} grouped (Optional) True to perform the operation for each group
1958 * in the store. The value returned will be an object literal with the key being the group
1959 * name and the last record being the value. The grouped parameter is only honored if
1960 * the store has a groupField.
1961 * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
1963 last: function(grouped) {
1966 if (grouped && me.isGrouped()) {
1967 return me.aggregate(function(records) {
1968 var len = records.length;
1969 return len ? records[len - 1] : undefined;
1972 return me.data.last();
1977 * Sums the value of <tt>property</tt> for each {@link Ext.data.Model record} between <tt>start</tt>
1978 * and <tt>end</tt> and returns the result.
1979 * @param {String} field A field in each record
1980 * @param {Boolean} grouped (Optional) True to perform the operation for each group
1981 * in the store. The value returned will be an object literal with the key being the group
1982 * name and the sum for that group being the value. The grouped parameter is only honored if
1983 * the store has a groupField.
1984 * @return {Number} The sum
1986 sum: function(field, grouped) {
1989 if (grouped && me.isGrouped()) {
1990 return me.aggregate(me.getSum, me, true, [field]);
1992 return me.getSum(me.data.items, field);
1996 // @private, see sum
1997 getSum: function(records, field) {
2000 len = records.length;
2002 for (; i < len; ++i) {
2003 total += records[i].get(field);
2010 * Gets the count of items in the store.
2011 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2012 * in the store. The value returned will be an object literal with the key being the group
2013 * name and the count for each group being the value. The grouped parameter is only honored if
2014 * the store has a groupField.
2015 * @return {Number} the count
2017 count: function(grouped) {
2020 if (grouped && me.isGrouped()) {
2021 return me.aggregate(function(records) {
2022 return records.length;
2025 return me.getCount();
2030 * Gets the minimum value in the store.
2031 * @param {String} field The field in each record
2032 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2033 * in the store. The value returned will be an object literal with the key being the group
2034 * name and the minimum in the group being the value. The grouped parameter is only honored if
2035 * the store has a groupField.
2036 * @return {Mixed/undefined} The minimum value, if no items exist, undefined.
2038 min: function(field, grouped) {
2041 if (grouped && me.isGrouped()) {
2042 return me.aggregate(me.getMin, me, true, [field]);
2044 return me.getMin(me.data.items, field);
2048 // @private, see min
2049 getMin: function(records, field){
2051 len = records.length,
2055 min = records[0].get(field);
2058 for (; i < len; ++i) {
2059 value = records[i].get(field);
2068 * Gets the maximum value in the store.
2069 * @param {String} field The field in each record
2070 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2071 * in the store. The value returned will be an object literal with the key being the group
2072 * name and the maximum in the group being the value. The grouped parameter is only honored if
2073 * the store has a groupField.
2074 * @return {Mixed/undefined} The maximum value, if no items exist, undefined.
2076 max: function(field, grouped) {
2079 if (grouped && me.isGrouped()) {
2080 return me.aggregate(me.getMax, me, true, [field]);
2082 return me.getMax(me.data.items, field);
2086 // @private, see max
2087 getMax: function(records, field) {
2089 len = records.length,
2094 max = records[0].get(field);
2097 for (; i < len; ++i) {
2098 value = records[i].get(field);
2107 * Gets the average value in the store.
2108 * @param {String} field The field in each record
2109 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2110 * in the store. The value returned will be an object literal with the key being the group
2111 * name and the group average being the value. The grouped parameter is only honored if
2112 * the store has a groupField.
2113 * @return {Mixed/undefined} The average value, if no items exist, 0.
2115 average: function(field, grouped) {
2117 if (grouped && me.isGrouped()) {
2118 return me.aggregate(me.getAverage, me, true, [field]);
2120 return me.getAverage(me.data.items, field);
2124 // @private, see average
2125 getAverage: function(records, field) {
2127 len = records.length,
2130 if (records.length > 0) {
2131 for (; i < len; ++i) {
2132 sum += records[i].get(field);
2140 * Runs the aggregate function for all the records in the store.
2141 * @param {Function} fn The function to execute. The function is called with a single parameter,
2142 * an array of records for that group.
2143 * @param {Object} scope (optional) The scope to execute the function in. Defaults to the store.
2144 * @param {Boolean} grouped (Optional) True to perform the operation for each group
2145 * in the store. The value returned will be an object literal with the key being the group
2146 * name and the group average being the value. The grouped parameter is only honored if
2147 * the store has a groupField.
2148 * @param {Array} args (optional) Any arguments to append to the function call
2149 * @return {Object} An object literal with the group names and their appropriate values.
2151 aggregate: function(fn, scope, grouped, args) {
2153 if (grouped && this.isGrouped()) {
2154 var groups = this.getGroups(),
2156 len = groups.length,
2160 for (; i < len; ++i) {
2162 out[group.name] = fn.apply(scope || this, [group.children].concat(args));
2166 return fn.apply(scope || this, [this.data.items].concat(args));