X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/c930e9176a5a85509c5b0230e2bff5c22a591432..3789b528d8dd8aad4558e38e22d775bcab1cbd36:/src/data/Store.js diff --git a/src/data/Store.js b/src/data/Store.js index fb368f73..c4d631bd 100644 --- a/src/data/Store.js +++ b/src/data/Store.js @@ -1,1347 +1,1805 @@ -/*! - * Ext JS Library 3.0.0 - * Copyright(c) 2006-2009 Ext JS, LLC - * licensing@extjs.com - * http://www.extjs.com/license - */ /** + * @author Ed Spencer * @class Ext.data.Store - * @extends Ext.util.Observable - *

The Store class encapsulates a client side cache of {@link Ext.data.Record Record} - * objects which provide input data for Components such as the {@link Ext.grid.GridPanel GridPanel}, - * the {@link Ext.form.ComboBox ComboBox}, or the {@link Ext.DataView DataView}.

- *

Retrieving Data

- *

A Store object may access a data object using:

- *

Reading Data

- *

A Store object has no inherent knowledge of the format of the data object (it could be - * an Array, XML, or JSON). A Store object uses an appropriate {@link #reader configured implementation} - * of a {@link Ext.data.DataReader DataReader} to create {@link Ext.data.Record Record} instances from the data - * object.

- *

Store Types

- *

There are several implementations of Store available which are customized for use with - * a specific DataReader implementation. Here is an example using an ArrayStore which implicitly - * creates a reader commensurate to an Array data object.

- *

-var myStore = new Ext.data.ArrayStore({
-    fields: ['fullname', 'first'],
-    idIndex: 0 // id for each record will be the first element
+ * @extends Ext.data.AbstractStore
+ *
+ * 

The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load + * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting}, + * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.

+ * + *

Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:

+ * +

+// Set up a {@link Ext.data.Model model} to use in our Store
+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'firstName', type: 'string'},
+        {name: 'lastName',  type: 'string'},
+        {name: 'age',       type: 'int'},
+        {name: 'eyeColor',  type: 'string'}
+    ]
 });
- * 
- *

For custom implementations create a basic {@link Ext.data.Store} configured as needed:

- *

-// create a {@link Ext.data.Record Record} constructor:
-var rt = Ext.data.Record.create([
-    {name: 'fullname'},
-    {name: 'first'}
-]);
+
 var myStore = new Ext.data.Store({
-    // explicitly create reader
-    reader: new Ext.data.ArrayReader(
-        {
-            idIndex: 0  // id for each record will be the first element
-        },
-        rt // recordType
-    )
+    model: 'User',
+    proxy: {
+        type: 'ajax',
+        url : '/users.json',
+        reader: {
+            type: 'json',
+            root: 'users'
+        }
+    },
+    autoLoad: true
 });
- * 
- *

Load some data into store (note the data object is an array which corresponds to the reader):

- *

-var myData = [
-    [1, 'Fred Flintstone', 'Fred'],  // note that id for the record is the first element
-    [2, 'Barney Rubble', 'Barney']
-];
-myStore.loadData(myData);
- * 
- *

Records are cached and made available through accessor functions. An example of adding - * a record to the store:

- *

-var defaultData = {
-    fullname: 'Full Name',
-    first: 'First Name'
-};
-var recId = 100; // provide unique id for the record
-var r = new myStore.recordType(defaultData, ++recId); // create new record
-myStore.{@link #insert}(0, r); // insert a new record into the store (also see {@link #add})
- * 
- * @constructor - * Creates a new Store. - * @param {Object} config A config object containing the objects needed for the Store to access data, - * and read the data into Records. - * @xtype store - */ -Ext.data.Store = function(config){ - this.data = new Ext.util.MixedCollection(false); - this.data.getKey = function(o){ - return o.id; - }; - /** - * See the {@link #baseParams corresponding configuration option} - * for a description of this property. - * To modify this property see {@link #setBaseParam}. - * @property - */ - this.baseParams = {}; - - // temporary removed-records cache - this.removed = []; - - if(config && config.data){ - this.inlineData = config.data; - delete config.data; - } - - Ext.apply(this, config); - - this.paramNames = Ext.applyIf(this.paramNames || {}, this.defaultParamNames); - - if(this.url && !this.proxy){ - this.proxy = new Ext.data.HttpProxy({url: this.url}); - } - // If Store is RESTful, so too is the DataProxy - if (this.restful === true && this.proxy) { - // When operating RESTfully, a unique transaction is generated for each record. - this.batch = false; - Ext.data.Api.restify(this.proxy); - } +
- if(this.reader){ // reader passed - if(!this.recordType){ - this.recordType = this.reader.recordType; + *

In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy + * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object - + * {@link Ext.data.reader.Json see the docs on JsonReader} for details.

+ * + *

Inline data

+ * + *

Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #data} + * into Model instances:

+ * +

+new Ext.data.Store({
+    model: 'User',
+    data : [
+        {firstName: 'Ed',    lastName: 'Spencer'},
+        {firstName: 'Tommy', lastName: 'Maintz'},
+        {firstName: 'Aaron', lastName: 'Conran'},
+        {firstName: 'Jamie', lastName: 'Avins'}
+    ]
+});
+
+ * + *

Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need + * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure, + * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).

+ * + *

Additional data can also be loaded locally using {@link #add}.

+ * + *

Loading Nested Data

+ * + *

Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders. + * 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 + * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro + * docs for a full explanation:

+ * +

+var store = new Ext.data.Store({
+    autoLoad: true,
+    model: "User",
+    proxy: {
+        type: 'ajax',
+        url : 'users.json',
+        reader: {
+            type: 'json',
+            root: 'users'
         }
-        if(this.reader.onMetaChange){
-            this.reader.onMetaChange = this.onMetaChange.createDelegate(this);
+    }
+});
+
+ * + *

Which would consume a response like this:

+ * +

+{
+    "users": [
+        {
+            "id": 1,
+            "name": "Ed",
+            "orders": [
+                {
+                    "id": 10,
+                    "total": 10.76,
+                    "status": "invoiced"
+                },
+                {
+                    "id": 11,
+                    "total": 13.45,
+                    "status": "shipped"
+                }
+            ]
         }
-        if (this.writer) { // writer passed
-            this.writer.meta = this.reader.meta;
-            this.pruneModifiedRecords = true;
+    ]
+}
+
+ * + *

See the {@link Ext.data.reader.Reader} intro docs for a full explanation.

+ * + *

Filtering and Sorting

+ * + *

Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are + * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to + * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}: + * +


+var store = new Ext.data.Store({
+    model: 'User',
+    sorters: [
+        {
+            property : 'age',
+            direction: 'DESC'
+        },
+        {
+            property : 'firstName',
+            direction: 'ASC'
         }
-    }
+    ],
 
-    /**
-     * The {@link Ext.data.Record Record} constructor as supplied to (or created by) the
-     * {@link Ext.data.DataReader Reader}. Read-only.
-     * 

If the Reader was constructed by passing in an Array of {@link Ext.data.Field} definition objects, - * instead of a Record constructor, it will implicitly create a Record constructor from that Array (see - * {@link Ext.data.Record}.{@link Ext.data.Record#create create} for additional details).

- *

This property may be used to create new Records of the type held in this Store, for example:


-// create the data store
-var store = new Ext.data.ArrayStore({
-    autoDestroy: true,
-    fields: [
-       {name: 'company'},
-       {name: 'price', type: 'float'},
-       {name: 'change', type: 'float'},
-       {name: 'pctChange', type: 'float'},
-       {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
+    filters: [
+        {
+            property: 'firstName',
+            value   : /Ed/
+        }
     ]
 });
-store.loadData(myData);
-
-// create the Grid
-var grid = new Ext.grid.EditorGridPanel({
-    store: store,
-    colModel: new Ext.grid.ColumnModel({
-        columns: [
-            {id:'company', header: 'Company', width: 160, dataIndex: 'company'},
-            {header: 'Price', renderer: 'usMoney', dataIndex: 'price'},
-            {header: 'Change', renderer: change, dataIndex: 'change'},
-            {header: '% Change', renderer: pctChange, dataIndex: 'pctChange'},
-            {header: 'Last Updated', width: 85,
-                renderer: Ext.util.Format.dateRenderer('m/d/Y'),
-                dataIndex: 'lastChange'}
-        ],
-        defaults: {
-            sortable: true,
-            width: 75
-        }
-    }),
-    autoExpandColumn: 'company', // match the id specified in the column model
-    height:350,
-    width:600,
-    title:'Array Grid',
-    tbar: [{
-        text: 'Add Record',
-        handler : function(){
-            var defaultData = {
-                change: 0,
-                company: 'New Company',
-                lastChange: (new Date()).clearTime(),
-                pctChange: 0,
-                price: 10
-            };
-            var recId = 3; // provide unique id
-            var p = new store.recordType(defaultData, recId); // create new record
-            grid.stopEditing();
-            store.{@link #insert}(0, p); // insert a new record into the store (also see {@link #add})
-            grid.startEditing(0, 0);
-        }
-    }]
+
+ * + *

The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting + * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to + * perform these operations instead.

+ * + *

Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store + * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by + * default {@link #sortOnFilter} is set to true, which means that your sorters are automatically reapplied if using local sorting.

+ * +

+store.filter('eyeColor', 'Brown');
+
+ * + *

Change the sorting at any time by calling {@link #sort}:

+ * +

+store.sort('height', 'ASC');
+
+ * + *

Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments, + * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them + * to the MixedCollection:

+ * +

+store.sorters.add(new Ext.util.Sorter({
+    property : 'shoeSize',
+    direction: 'ASC'
+}));
+
+store.sort();
+
+ * + *

Registering with StoreManager

+ * + *

Any Store that is instantiated with a {@link #storeId} will automatically be registed with the {@link Ext.data.StoreManager StoreManager}. + * This makes it easy to reuse the same store in multiple views:

+ * +

+//this store can be used several times
+new Ext.data.Store({
+    model: 'User',
+    storeId: 'usersStore'
 });
-     * 
- * @property recordType - * @type Function - */ - if(this.recordType){ - /** - * A {@link Ext.util.MixedCollection MixedCollection} containing the defined {@link Ext.data.Field Field}s - * for the {@link Ext.data.Record Records} stored in this Store. Read-only. - * @property fields - * @type Ext.util.MixedCollection - */ - this.fields = this.recordType.prototype.fields; - } - this.modified = []; +new Ext.List({ + store: 'usersStore', - this.addEvents( - /** - * @event datachanged - * Fires when the data cache has changed in a bulk manner (e.g., it has been sorted, filtered, etc.) and a - * widget that is using this Store as a Record cache should refresh its view. - * @param {Store} this - */ - 'datachanged', - /** - * @event metachange - * Fires when this store's reader provides new metadata (fields). This is currently only supported for JsonReaders. - * @param {Store} this - * @param {Object} meta The JSON metadata - */ - 'metachange', - /** - * @event add - * Fires when Records have been {@link #add}ed to the Store - * @param {Store} this - * @param {Ext.data.Record[]} records The array of Records added - * @param {Number} index The index at which the record(s) were added - */ - 'add', - /** - * @event remove - * Fires when a Record has been {@link #remove}d from the Store - * @param {Store} this - * @param {Ext.data.Record} record The Record that was removed - * @param {Number} index The index at which the record was removed - */ - 'remove', - /** - * @event update - * Fires when a Record has been updated - * @param {Store} this - * @param {Ext.data.Record} record The Record that was updated - * @param {String} operation The update operation being performed. Value may be one of: - *

- Ext.data.Record.EDIT
- Ext.data.Record.REJECT
- Ext.data.Record.COMMIT
-         * 
- */ - 'update', - /** - * @event clear - * Fires when the data cache has been cleared. - * @param {Store} this - */ - 'clear', - /** - * @event exception - *

Fires if an exception occurs in the Proxy during a remote request. - * This event is relayed through the corresponding {@link Ext.data.DataProxy}. - * See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} - * for additional details. - * @param {misc} misc See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} - * for description. - */ - 'exception', - /** - * @event beforeload - * Fires before a request is made for a new data object. If the beforeload handler returns - * false the {@link #load} action will be canceled. - * @param {Store} this - * @param {Object} options The loading options that were specified (see {@link #load} for details) - */ - 'beforeload', - /** - * @event load - * Fires after a new set of Records has been loaded. - * @param {Store} this - * @param {Ext.data.Record[]} records The Records that were loaded - * @param {Object} options The loading options that were specified (see {@link #load} for details) - */ - 'load', - /** - * @event loadexception - *

This event is deprecated in favor of the catch-all {@link #exception} - * event instead.

- *

This event is relayed through the corresponding {@link Ext.data.DataProxy}. - * See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#loadexception loadexception} - * for additional details. - * @param {misc} misc See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#loadexception loadexception} - * for description. - */ - 'loadexception', - /** - * @event beforewrite - * @param {DataProxy} this - * @param {String} action [Ext.data.Api.actions.create|update|destroy] - * @param {Record/Array[Record]} rs - * @param {Object} options The loading options that were specified. Edit options.params to add Http parameters to the request. (see {@link #save} for details) - * @param {Object} arg The callback's arg object passed to the {@link #request} function - */ - 'beforewrite', - /** - * @event write - * Fires if the server returns 200 after an Ext.data.Api.actions CRUD action. - * Success or failure of the action is available in the result['successProperty'] property. - * The server-code might set the successProperty to false if a database validation - * failed, for example. - * @param {Ext.data.Store} store - * @param {String} action [Ext.data.Api.actions.create|update|destroy] - * @param {Object} result The 'data' picked-out out of the response for convenience. - * @param {Ext.Direct.Transaction} res - * @param {Record/Record[]} rs Store's records, the subject(s) of the write-action - */ - 'write' - ); + //other config goes here +}); - if(this.proxy){ - this.relayEvents(this.proxy, ['loadexception', 'exception']); - } - // With a writer set for the Store, we want to listen to add/remove events to remotely create/destroy records. - if (this.writer) { - this.on({ - scope: this, - add: this.createRecords, - remove: this.destroyRecord, - update: this.updateRecord - }); - } +new Ext.view.View({ + store: 'usersStore', - this.sortToggle = {}; - if(this.sortField){ - this.setDefaultSort(this.sortField, this.sortDir); - }else if(this.sortInfo){ - this.setDefaultSort(this.sortInfo.field, this.sortInfo.direction); - } + //other config goes here +}); +

+ * + *

Further Reading

+ * + *

Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these + * pieces and how they fit together, see:

+ * + * + * + * @constructor + * @param {Object} config Optional config object + */ +Ext.define('Ext.data.Store', { + extend: 'Ext.data.AbstractStore', - Ext.data.Store.superclass.constructor.call(this); + alias: 'store.store', + + requires: ['Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'], + uses: ['Ext.data.proxy.Memory'], - if(this.id){ - this.storeId = this.id; - delete this.id; - } - if(this.storeId){ - Ext.StoreMgr.register(this); - } - if(this.inlineData){ - this.loadData(this.inlineData); - delete this.inlineData; - }else if(this.autoLoad){ - this.load.defer(10, this, [ - typeof this.autoLoad == 'object' ? - this.autoLoad : undefined]); - } -}; -Ext.extend(Ext.data.Store, Ext.util.Observable, { /** - * @cfg {String} storeId If passed, the id to use to register with the {@link Ext.StoreMgr StoreMgr}. - *

Note: if a (deprecated) {@link #id} is specified it will supersede the storeId - * assignment.

+ * @cfg {Boolean} remoteSort + * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to false. */ + remoteSort: false, + /** - * @cfg {String} url If a {@link #proxy} is not specified the url will be used to - * implicitly configure a {@link Ext.data.HttpProxy HttpProxy} if an url is specified. - * Typically this option, or the {@link #data} option will be specified. + * @cfg {Boolean} remoteFilter + * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to false. */ + remoteFilter: false, + /** - * @cfg {Boolean/Object} autoLoad If {@link #data} is not specified, and if autoLoad - * is true or an Object, this store's {@link #load} method is automatically called - * after creation. If the value of autoLoad is an Object, this Object will - * be passed to the store's {@link #load} method. + * @cfg {Boolean} remoteGroup + * True if the grouping should apply on the server side, false if it is local only (defaults to false). If the + * grouping is local, it can be applied immediately to the data. If it is remote, then it will simply act as a + * helper, automatically sending the grouping information to the server. */ + remoteGroup : false, + /** - * @cfg {Ext.data.DataProxy} proxy The {@link Ext.data.DataProxy DataProxy} object which provides - * access to a data object. See {@link #url}. + * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config + * object or a Proxy instance - see {@link #setProxy} for details. */ + /** - * @cfg {Array} data An inline data object readable by the {@link #reader}. - * Typically this option, or the {@link #url} option will be specified. + * @cfg {Array} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details. */ + /** - * @cfg {Ext.data.DataReader} reader The {@link Ext.data.DataReader Reader} object which processes the - * data object and returns an Array of {@link Ext.data.Record} objects which are cached keyed by their - * {@link Ext.data.Record#id id} property. + * @cfg {String} model The {@link Ext.data.Model} associated with this store */ - /** - * @cfg {Ext.data.DataWriter} writer - *

The {@link Ext.data.DataWriter Writer} object which processes a record object for being written - * to the server-side database.

- *

When a writer is installed into a Store the {@link #add}, {@link #remove}, and {@link #update} - * events on the store are monitored in order to remotely {@link #createRecords create records}, - * {@link #destroyRecord destroy records}, or {@link #updateRecord update records}.

- *

The proxy for this store will relay any {@link #writexception} events to this store.

- *

Sample implementation: - *


-var writer = new {@link Ext.data.JsonWriter}({
-    encode: true,
-    writeAllFields: true // write all fields, not just those that changed
-});
 
-// Typical Store collecting the Proxy, Reader and Writer together.
-var store = new Ext.data.Store({
-    storeId: 'user',
-    root: 'records',
-    proxy: proxy,
-    reader: reader,
-    writer: writer,     // <-- plug a DataWriter into the store just as you would a Reader
-    paramsAsHash: true,
-    autoSave: false    // <-- false to delay executing create, update, destroy requests
-                        //     until specifically told to do so.
-});
-     * 

- */ - writer : undefined, - /** - * @cfg {Object} baseParams - *

An object containing properties which are to be sent as parameters - * for every HTTP request.

- *

Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.

- *

Note: baseParams may be superseded by any params - * specified in a {@link #load} request, see {@link #load} - * for more details.

- * This property may be modified after creation using the {@link #setBaseParam} - * method. - * @property - */ - /** - * @cfg {Object} sortInfo A config object to specify the sort order in the request of a Store's - * {@link #load} operation. Note that for local sorting, the direction property is - * case-sensitive. See also {@link #remoteSort} and {@link #paramNames}. - * For example:

-sortInfo: {
-    field: 'fieldName',
-    direction: 'ASC' // or 'DESC' (case sensitive for local sorting)
-}
-
- */ /** - * @cfg {boolean} remoteSort true if sorting is to be handled by requesting the {@link #proxy Proxy} - * to provide a refreshed version of the data object in sorted order, as opposed to sorting the Record cache - * in place (defaults to false). - *

If remoteSort is true, then clicking on a {@link Ext.grid.Column Grid Column}'s - * {@link Ext.grid.Column#header header} causes the current page to be requested from the server appending - * the following two parameters to the {@link #load params}:

+ * The (optional) field by which to group data in the store. Internally, grouping is very similar to sorting - the + * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single + * level of grouping, and groups can be fetched via the {@link #getGroups} method. + * @property groupField + * @type String */ - remoteSort : false, + groupField: undefined, /** - * @cfg {Boolean} autoDestroy true to destroy the store when the component the store is bound - * to is destroyed (defaults to false). - *

Note: this should be set to true when using stores that are bound to only 1 component.

+ * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC" + * @property groupDir + * @type String */ - autoDestroy : false, + groupDir: "ASC", /** - * @cfg {Boolean} pruneModifiedRecords true to clear all modified record information each time - * the store is loaded or when a record is removed (defaults to false). See {@link #getModifiedRecords} - * for the accessor method to retrieve the modified records. + * @cfg {Number} pageSize + * The number of records considered to form a 'page'. This is used to power the built-in + * paging using the nextPage and previousPage functions. Defaults to 25. */ - pruneModifiedRecords : false, + pageSize: 25, /** - * Contains the last options object used as the parameter to the {@link #load} method. See {@link #load} - * for the details of what this may contain. This may be useful for accessing any params which were used - * to load the current Record cache. - * @property + * The page that the Store has most recently loaded (see {@link #loadPage}) + * @property currentPage + * @type Number */ - lastOptions : null, + currentPage: 1, /** - * @cfg {Boolean} autoSave - *

Defaults to true causing the store to automatically {@link #save} records to - * the server when a record is modified (ie: becomes 'dirty'). Specify false to manually call {@link #save} - * to send all modifiedRecords to the server.

- *

Note: each CRUD action will be sent as a separate request.

+ * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage}, + * {@link #nextPage} or {@link #previousPage} (defaults to true). Setting to false keeps existing records, allowing + * large data sets to be loaded one page at a time but rendered all together. */ - autoSave : true, + clearOnPageLoad: true, /** - * @cfg {Boolean} batch - *

Defaults to true (unless {@link #restful}:true). Multiple - * requests for each CRUD action (CREATE, READ, UPDATE and DESTROY) will be combined - * and sent as one transaction. Only applies when {@link #autoSave} is set - * to false.

- *

If Store is RESTful, the DataProxy is also RESTful, and a unique transaction is - * generated for each record.

+ * True if the Store is currently loading via its Proxy + * @property loading + * @type Boolean + * @private */ - batch : true, + loading: false, /** - * @cfg {Boolean} restful - * Defaults to false. Set to true to have the Store and the set - * Proxy operate in a RESTful manner. The store will automatically generate GET, POST, - * PUT and DELETE requests to the server. The HTTP method used for any given CRUD - * action is described in {@link Ext.data.Api#restActions}. For additional information - * see {@link Ext.data.DataProxy#restful}. - *

Note: if {@link #restful}:true batch will - * internally be set to false.

+ * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called, + * causing the sorters to be reapplied after filtering. Defaults to true */ - restful: false, + sortOnFilter: true, /** - * @cfg {Object} paramNames - *

An object containing properties which specify the names of the paging and - * sorting parameters passed to remote servers when loading blocks of data. By default, this - * object takes the following form:


-{
-    start : 'start',  // The parameter name which specifies the start row
-    limit : 'limit',  // The parameter name which specifies number of rows to return
-    sort : 'sort',    // The parameter name which specifies the column to sort on
-    dir : 'dir'       // The parameter name which specifies the sort direction
-}
-
- *

The server must produce the requested data block upon receipt of these parameter names. - * If different parameter names are required, this property can be overriden using a configuration - * property.

- *

A {@link Ext.PagingToolbar PagingToolbar} bound to this Store uses this property to determine - * the parameter names to use in its {@link #load requests}. + * @cfg {Boolean} buffered + * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will + * tell the store to pre-fetch records ahead of a time. */ - paramNames : undefined, + buffered: false, /** - * @cfg {Object} defaultParamNames - * Provides the default values for the {@link #paramNames} property. To globally modify the parameters - * for all stores, this object should be changed on the store prototype. - */ - defaultParamNames : { - start : 'start', - limit : 'limit', - sort : 'sort', - dir : 'dir' - }, - - /** - * Destroys the store. + * @cfg {Number} purgePageCount + * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data. + * This option is only relevant when the {@link #buffered} option is set to true. */ - destroy : function(){ - if(this.storeId){ - Ext.StoreMgr.unregister(this); + purgePageCount: 5, + + isStore: true, + + //documented above + constructor: function(config) { + config = config || {}; + + var me = this, + groupers = config.groupers || me.groupers, + groupField = config.groupField || me.groupField, + proxy, + data; + + if (config.buffered || me.buffered) { + me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) { + return record.index; + }); + me.pendingRequests = []; + me.pagesRequested = []; + + me.sortOnLoad = false; + me.filterOnLoad = false; } - this.data = null; - Ext.destroy(this.proxy); - this.reader = this.writer = null; - this.purgeListeners(); - }, + + me.addEvents( + /** + * @event beforeprefetch + * Fires before a prefetch occurs. Return false to cancel. + * @param {Ext.data.store} this + * @param {Ext.data.Operation} operation The associated operation + */ + 'beforeprefetch', + /** + * @event groupchange + * Fired whenever the grouping in the grid changes + * @param {Ext.data.Store} store The store + * @param {Array} groupers The array of grouper objects + */ + 'groupchange', + /** + * @event load + * Fires whenever records have been prefetched + * @param {Ext.data.store} this + * @param {Array} records An array of records + * @param {Boolean} successful True if the operation was successful. + * @param {Ext.data.Operation} operation The associated operation + */ + 'prefetch' + ); + data = config.data || me.data; - /** - * Add Records to the Store and fires the {@link #add} event. To add Records - * to the store from a remote source use {@link #load}({add:true}). - * See also {@link #recordType} and {@link #insert}. - * @param {Ext.data.Record[]} records An Array of Ext.data.Record objects - * to add to the cache. See {@link #recordType}. - */ - add : function(records){ - records = [].concat(records); - if(records.length < 1){ - return; + /** + * The MixedCollection that holds this store's local cache of records + * @property data + * @type Ext.util.MixedCollection + */ + me.data = Ext.create('Ext.util.MixedCollection', false, function(record) { + return record.internalId; + }); + + if (data) { + me.inlineData = data; + delete config.data; } - for(var i = 0, len = records.length; i < len; i++){ - records[i].join(this); + + if (!groupers && groupField) { + groupers = [{ + property : groupField, + direction: config.groupDir || me.groupDir + }]; } - var index = this.data.length; - this.data.addAll(records); - if(this.snapshot){ - this.snapshot.addAll(records); + delete config.groupers; + + /** + * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store + * @property groupers + * @type Ext.util.MixedCollection + */ + me.groupers = Ext.create('Ext.util.MixedCollection'); + me.groupers.addAll(me.decodeGroupers(groupers)); + + this.callParent([config]); + // don't use *config* anymore from here on... use *me* instead... + + if (me.groupers.items.length) { + me.sort(me.groupers.items, 'prepend', false); } - this.fireEvent('add', this, records, index); - }, - /** - * (Local sort only) Inserts the passed Record into the Store at the index where it - * should go based on the current sort information. - * @param {Ext.data.Record} record - */ - addSorted : function(record){ - var index = this.findInsertIndex(record); - this.insert(index, record); - }, + proxy = me.proxy; + data = me.inlineData; + if (data) { + if (proxy instanceof Ext.data.proxy.Memory) { + proxy.data = data; + me.read(); + } else { + me.add.apply(me, data); + } + + me.sort(); + delete me.inlineData; + } else if (me.autoLoad) { + Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]); + // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here. + // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined); + } + }, + + onBeforeSort: function() { + this.sort(this.groupers.items, 'prepend', false); + }, + /** - * Remove a Record from the Store and fires the {@link #remove} event. - * @param {Ext.data.Record} record The Ext.data.Record object to remove from the cache. + * @private + * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances + * @param {Array} groupers The groupers array + * @return {Array} Array of Ext.util.Grouper objects */ - remove : function(record){ - var index = this.data.indexOf(record); - if(index > -1){ - this.data.removeAt(index); - if(this.pruneModifiedRecords){ - this.modified.remove(record); + decodeGroupers: function(groupers) { + if (!Ext.isArray(groupers)) { + if (groupers === undefined) { + groupers = []; + } else { + groupers = [groupers]; } - if(this.snapshot){ - this.snapshot.remove(record); + } + + var length = groupers.length, + Grouper = Ext.util.Grouper, + config, i; + + for (i = 0; i < length; i++) { + config = groupers[i]; + + if (!(config instanceof Grouper)) { + if (Ext.isString(config)) { + config = { + property: config + }; + } + + Ext.applyIf(config, { + root : 'data', + direction: "ASC" + }); + + //support for 3.x style sorters where a function can be defined as 'fn' + if (config.fn) { + config.sorterFn = config.fn; + } + + //support a function to be passed as a sorter definition + if (typeof config == 'function') { + config = { + sorterFn: config + }; + } + + groupers[i] = new Grouper(config); } - this.fireEvent('remove', this, record, index); } - }, - /** - * Remove a Record from the Store at the specified index. Fires the {@link #remove} event. - * @param {Number} index The index of the record to remove. - */ - removeAt : function(index){ - this.remove(this.getAt(index)); + return groupers; }, - + /** - * Remove all Records from the Store and fires the {@link #clear} event. + * Group data in the store + * @param {String|Array} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model}, + * or an Array of grouper configurations. + * @param {String} direction The overall direction to group the data by. Defaults to "ASC". */ - removeAll : function(){ - this.data.clear(); - if(this.snapshot){ - this.snapshot.clear(); + group: function(groupers, direction) { + var me = this, + grouper, + newGroupers; + + if (Ext.isArray(groupers)) { + newGroupers = groupers; + } else if (Ext.isObject(groupers)) { + newGroupers = [groupers]; + } else if (Ext.isString(groupers)) { + grouper = me.groupers.get(groupers); + + if (!grouper) { + grouper = { + property : groupers, + direction: direction + }; + newGroupers = [grouper]; + } else if (direction === undefined) { + grouper.toggle(); + } else { + grouper.setDirection(direction); + } } - if(this.pruneModifiedRecords){ - this.modified = []; + + if (newGroupers && newGroupers.length) { + newGroupers = me.decodeGroupers(newGroupers); + me.groupers.clear(); + me.groupers.addAll(newGroupers); + } + + if (me.remoteGroup) { + me.load({ + scope: me, + callback: me.fireGroupChange + }); + } else { + me.sort(); + me.fireEvent('groupchange', me, me.groupers); } - this.fireEvent('clear', this); }, - + /** - * Inserts Records into the Store at the given index and fires the {@link #add} event. - * See also {@link #add} and {@link #addSorted}. - * @param {Number} index The start index at which to insert the passed Records. - * @param {Ext.data.Record[]} records An Array of Ext.data.Record objects to add to the cache. + * Clear any groupers in the store */ - insert : function(index, records){ - records = [].concat(records); - for(var i = 0, len = records.length; i < len; i++){ - this.data.insert(index, records[i]); - records[i].join(this); + clearGrouping: function(){ + var me = this; + // Clear any groupers we pushed on to the sorters + me.groupers.each(function(grouper){ + me.sorters.remove(grouper); + }); + me.groupers.clear(); + if (me.remoteGroup) { + me.load({ + scope: me, + callback: me.fireGroupChange + }); + } else { + me.sort(); + me.fireEvent('groupchange', me, me.groupers); } - this.fireEvent('add', this, records, index); }, - + /** - * Get the index within the cache of the passed Record. - * @param {Ext.data.Record} record The Ext.data.Record object to find. - * @return {Number} The index of the passed Record. Returns -1 if not found. + * Checks if the store is currently grouped + * @return {Boolean} True if the store is grouped. */ - indexOf : function(record){ - return this.data.indexOf(record); + isGrouped: function() { + return this.groupers.getCount() > 0; }, - + /** - * Get the index within the cache of the Record with the passed id. - * @param {String} id The id of the Record to find. - * @return {Number} The index of the Record. Returns -1 if not found. + * Fires the groupchange event. Abstracted out so we can use it + * as a callback + * @private */ - indexOfId : function(id){ - return this.data.indexOfKey(id); + fireGroupChange: function(){ + this.fireEvent('groupchange', this, this.groupers); }, /** - * Get the Record with the specified id. - * @param {String} id The id of the Record to find. - * @return {Ext.data.Record} The Record with the passed id. Returns undefined if not found. - */ - getById : function(id){ - return this.data.key(id); - }, + * Returns an object containing the result of applying grouping to the records in this store. See {@link #groupField}, + * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field: +


+var myStore = new Ext.data.Store({
+    groupField: 'color',
+    groupDir  : 'DESC'
+});
 
-    /**
-     * Get the Record at the specified index.
-     * @param {Number} index The index of the Record to find.
-     * @return {Ext.data.Record} The Record at the passed index. Returns undefined if not found.
-     */
-    getAt : function(index){
-        return this.data.itemAt(index);
+myStore.getGroups(); //returns:
+[
+    {
+        name: 'yellow',
+        children: [
+            //all records where the color field is 'yellow'
+        ]
     },
-
-    /**
-     * Returns a range of Records between specified indices.
-     * @param {Number} startIndex (optional) The starting index (defaults to 0)
-     * @param {Number} endIndex (optional) The ending index (defaults to the last Record in the Store)
-     * @return {Ext.data.Record[]} An array of Records
+    {
+        name: 'red',
+        children: [
+            //all records where the color field is 'red'
+        ]
+    }
+]
+
+ * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString} + * @return {Array} The grouped data */ - getRange : function(start, end){ - return this.data.getRange(start, end); - }, + getGroups: function(requestGroupString) { + var records = this.data.items, + length = records.length, + groups = [], + pointers = {}, + record, + groupStr, + group, + i; + + for (i = 0; i < length; i++) { + record = records[i]; + groupStr = this.getGroupString(record); + group = pointers[groupStr]; + + if (group === undefined) { + group = { + name: groupStr, + children: [] + }; + + groups.push(group); + pointers[groupStr] = group; + } - // private - storeOptions : function(o){ - o = Ext.apply({}, o); - delete o.callback; - delete o.scope; - this.lastOptions = o; - }, - - /** - *

Loads the Record cache from the configured {@link #proxy} using the configured {@link #reader}.

- *

Notes:

- * @param {Object} options An object containing properties which control loading options: - * @return {Boolean} If the developer provided {@link #beforeload} event handler returns - * false, the load call will abort and will return false; otherwise will return true. - */ - load : function(options) { - options = options || {}; - this.storeOptions(options); - if(this.sortInfo && this.remoteSort){ - var pn = this.paramNames; - options.params = options.params || {}; - options.params[pn.sort] = this.sortInfo.field; - options.params[pn.dir] = this.sortInfo.direction; - } - try { - return this.execute('read', null, options); // <-- null represents rs. No rs for load actions. - } catch(e) { - this.handleException(e); - return false; + group.children.push(record); } + + return requestGroupString ? pointers[requestGroupString] : groups; }, /** - * updateRecord Should not be used directly. This method will be called automatically if a Writer is set. - * Listens to 'update' event. - * @param {Object} store - * @param {Object} record - * @param {Object} action * @private + * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records + * matching a certain group. */ - updateRecord : function(store, record, action) { - if (action == Ext.data.Record.EDIT && this.autoSave === true && (!record.phantom || (record.phantom && record.isValid))) { - this.save(); + getGroupsForGrouper: function(records, grouper) { + var length = records.length, + groups = [], + oldValue, + newValue, + record, + group, + i; + + for (i = 0; i < length; i++) { + record = records[i]; + newValue = grouper.getGroupString(record); + + if (newValue !== oldValue) { + group = { + name: newValue, + grouper: grouper, + records: [] + }; + groups.push(group); + } + + group.records.push(record); + + oldValue = newValue; } + + return groups; }, /** - * Should not be used directly. Store#add will call this automatically if a Writer is set - * @param {Object} store - * @param {Object} rs - * @param {Object} index * @private + * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for + * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by + * {@link #getGroupsForGrouper} - this function largely just handles the recursion. + * @param {Array} records The set or subset of records to group + * @param {Number} grouperIndex The grouper index to retrieve + * @return {Array} The grouped records */ - createRecords : function(store, rs, index) { - for (var i = 0, len = rs.length; i < len; i++) { - if (rs[i].phantom && rs[i].isValid()) { - rs[i].markDirty(); // <-- Mark new records dirty - this.modified.push(rs[i]); // <-- add to modified + getGroupsForGrouperIndex: function(records, grouperIndex) { + var me = this, + groupers = me.groupers, + grouper = groupers.getAt(grouperIndex), + groups = me.getGroupsForGrouper(records, grouper), + length = groups.length, + i; + + if (grouperIndex + 1 < groupers.length) { + for (i = 0; i < length; i++) { + groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1); } } - if (this.autoSave === true) { - this.save(); + + for (i = 0; i < length; i++) { + groups[i].depth = grouperIndex; } + + return groups; }, /** - * Destroys a record or records. Should not be used directly. It's called by Store#remove if a Writer is set. - * @param {Store} this - * @param {Ext.data.Record/Ext.data.Record[]} - * @param {Number} index * @private + *

Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in + * this case grouping by genre and then author in a fictional books dataset):

+

+[
+    {
+        name: 'Fantasy',
+        depth: 0,
+        records: [
+            //book1, book2, book3, book4
+        ],
+        children: [
+            {
+                name: 'Rowling',
+                depth: 1,
+                records: [
+                    //book1, book2
+                ]
+            },
+            {
+                name: 'Tolkein',
+                depth: 1,
+                records: [
+                    //book3, book4
+                ]
+            }
+        ]
+    }
+]
+
+ * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping + * function correctly so this should only be set to false if the Store is known to already be sorted correctly + * (defaults to true) + * @return {Array} The group data */ - destroyRecord : function(store, record, index) { - if (this.modified.indexOf(record) != -1) { // <-- handled already if @cfg pruneModifiedRecords == true - this.modified.remove(record); + getGroupData: function(sort) { + var me = this; + if (sort !== false) { + me.sort(); } - if (!record.phantom) { - this.removed.push(record); - // since the record has already been removed from the store but the server request has not yet been executed, - // must keep track of the last known index this record existed. If a server error occurs, the record can be - // put back into the store. @see Store#createCallback where the record is returned when response status === false - record.lastIndex = index; - - if (this.autoSave === true) { - this.save(); - } - } + return me.getGroupsForGrouperIndex(me.data.items, 0); }, /** - * This method should generally not be used directly. This method is called internally - * by {@link #load}, or if a Writer is set will be called automatically when {@link #add}, - * {@link #remove}, or {@link #update} events fire. - * @param {String} action Action name ('read', 'create', 'update', or 'destroy') - * @param {Record/Record[]} rs - * @param {Object} options - * @throws Error - * @private + *

Returns the string to group on for a given model instance. The default implementation of this method returns + * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to + * group by the first letter of a model's 'name' field, use the following code:

+

+new Ext.data.Store({
+    groupDir: 'ASC',
+    getGroupString: function(instance) {
+        return instance.get('name')[0];
+    }
+});
+
+ * @param {Ext.data.Model} instance The model instance + * @return {String} The string to compare when forming groups */ - execute : function(action, rs, options) { - // blow up if action not Ext.data.CREATE, READ, UPDATE, DESTROY - if (!Ext.data.Api.isAction(action)) { - throw new Ext.data.Api.Error('execute', action); + getGroupString: function(instance) { + var group = this.groupers.first(); + if (group) { + return instance.get(group.property); } - // make sure options has a params key - options = Ext.applyIf(options||{}, { - params: {} - }); - - // have to separate before-events since load has a different signature than create,destroy and save events since load does not - // include the rs (record resultset) parameter. Capture return values from the beforeaction into doRequest flag. - var doRequest = true; + return ''; + }, + /** + * Inserts Model instances into the Store at the given index and fires the {@link #add} event. + * See also {@link #add}. + * @param {Number} index The start index at which to insert the passed Records. + * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache. + */ + insert: function(index, records) { + var me = this, + sync = false, + i, + record, + len; - if (action === 'read') { - doRequest = this.fireEvent('beforeload', this, options); + records = [].concat(records); + for (i = 0, len = records.length; i < len; i++) { + record = me.createModel(records[i]); + record.set(me.modelDefaults); + // reassign the model in the array in case it wasn't created yet + records[i] = record; + + me.data.insert(index + i, record); + record.join(me); + + sync = sync || record.phantom === true; } - else { - // if Writer is configured as listful, force single-recoord rs to be [{}} instead of {} - if (this.writer.listful === true && this.restful !== true) { - rs = (Ext.isArray(rs)) ? rs : [rs]; - } - // if rs has just a single record, shift it off so that Writer writes data as '{}' rather than '[{}]' - else if (Ext.isArray(rs) && rs.length == 1) { - rs = rs.shift(); - } - // Write the action to options.params - if ((doRequest = this.fireEvent('beforewrite', this, action, rs, options)) !== false) { - this.writer.write(action, options.params, rs); - } + + if (me.snapshot) { + me.snapshot.addAll(records); } - if (doRequest !== false) { - // Send request to proxy. - var params = Ext.apply({}, options.params, this.baseParams); - if (this.writer && this.proxy.url && !this.proxy.restful && !Ext.data.Api.hasUniqueUrl(this.proxy, action)) { - params.xaction = action; - } - // Note: Up until this point we've been dealing with 'action' as a key from Ext.data.Api.actions. We'll flip it now - // and send the value into DataProxy#request, since it's the value which maps to the DataProxy#api - this.proxy.request(Ext.data.Api.actions[action], rs, params, this.reader, this.createCallback(action, rs), this, options); + + me.fireEvent('add', me, records, index); + me.fireEvent('datachanged', me); + if (me.autoSync && sync) { + me.sync(); } - return doRequest; }, /** - * Saves all pending changes to the store. If the commensurate Ext.data.Api.actions action is not configured, then - * the configured {@link #url} will be used. - *
-     * change            url
-     * ---------------   --------------------
-     * removed records   Ext.data.Api.actions.destroy
-     * phantom records   Ext.data.Api.actions.create
-     * {@link #getModifiedRecords modified records}  Ext.data.Api.actions.update
-     * 
- * @TODO: Create extensions of Error class and send associated Record with thrown exceptions. - * e.g.: Ext.data.DataReader.Error or Ext.data.Error or Ext.data.DataProxy.Error, etc. + * Adds Model instances to the Store by instantiating them based on a JavaScript object. When adding already- + * instantiated Models, use {@link #insert} instead. The instances will be added at the end of the existing collection. + * This method accepts either a single argument array of Model instances or any number of model instance arguments. + * Sample usage: + * +

+myStore.add({some: 'data'}, {some: 'other data'});
+
+ * + * @param {Object} data The data for each model + * @return {Array} The array of newly created model instances */ - save : function() { - if (!this.writer) { - throw new Ext.data.Store.Error('writer-undefined'); + add: function(records) { + //accept both a single-argument array of records, or any number of record arguments + if (!Ext.isArray(records)) { + records = Array.prototype.slice.apply(arguments); } - // DESTROY: First check for removed records. Records in this.removed are guaranteed non-phantoms. @see Store#remove - if (this.removed.length) { - this.doTransaction('destroy', this.removed); - } + var me = this, + i = 0, + length = records.length, + record; - // Check for modified records. Use a copy so Store#rejectChanges will work if server returns error. - var rs = [].concat(this.getModifiedRecords()); - if (!rs.length) { // Bail-out if empty... - return true; + for (; i < length; i++) { + record = me.createModel(records[i]); + // reassign the model in the array in case it wasn't created yet + records[i] = record; } - // CREATE: Next check for phantoms within rs. splice-off and execute create. - var phantoms = []; - for (var i = rs.length-1; i >= 0; i--) { - if (rs[i].phantom === true) { - var rec = rs.splice(i, 1).shift(); - if (rec.isValid()) { - phantoms.push(rec); - } - } else if (!rs[i].isValid()) { // <-- while we're here, splice-off any !isValid real records - rs.splice(i,1); - } - } - // If we have valid phantoms, create them... - if (phantoms.length) { - this.doTransaction('create', phantoms); - } + me.insert(me.data.length, records); - // UPDATE: And finally, if we're still here after splicing-off phantoms and !isValid real records, update the rest... - if (rs.length) { - this.doTransaction('update', rs); - } - return true; + return records; }, - // private. Simply wraps call to Store#execute in try/catch. Defers to Store#handleException on error. Loops if batch: false - doTransaction : function(action, rs) { - function transaction(records) { - try { - this.execute(action, records); - } catch (e) { - this.handleException(e); - } - } - if (this.batch === false) { - for (var i = 0, len = rs.length; i < len; i++) { - transaction.call(this, rs[i]); - } - } else { - transaction.call(this, rs); + /** + * Converts a literal to a model, if it's not a model already + * @private + * @param record {Ext.data.Model/Object} The record to create + * @return {Ext.data.Model} + */ + createModel: function(record) { + if (!record.isModel) { + record = Ext.ModelManager.create(record, this.model); } + + return record; }, - // @private callback-handler for remote CRUD actions - // Do not override -- override loadRecords, onCreateRecords, onDestroyRecords and onUpdateRecords instead. - createCallback : function(action, rs) { - var actions = Ext.data.Api.actions; - return (action == 'read') ? this.loadRecords : function(data, response, success) { - // calls: onCreateRecords | onUpdateRecords | onDestroyRecords - this['on' + Ext.util.Format.capitalize(action) + 'Records'](success, rs, data); - // If success === false here, exception will have been called in DataProxy - if (success === true) { - this.fireEvent('write', this, action, data, response, rs); - } - }; + /** + * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache. + * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed as the first parameter. + * Returning false aborts and exits the iteration. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. + * Defaults to the current {@link Ext.data.Model Record} in the iteration. + */ + each: function(fn, scope) { + this.data.each(fn, scope); }, - // Clears records from modified array after an exception event. - // NOTE: records are left marked dirty. Do we want to commit them even though they were not updated/realized? - clearModified : function(rs) { - if (Ext.isArray(rs)) { - for (var n=rs.length-1;n>=0;n--) { - this.modified.splice(this.modified.indexOf(rs[n]), 1); - } - } else { - this.modified.splice(this.modified.indexOf(rs), 1); + /** + * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single + * 'datachanged' event after removal. + * @param {Ext.data.Model/Array} records The Ext.data.Model instance or array of instances to remove + */ + remove: function(records, /* private */ isMove) { + if (!Ext.isArray(records)) { + records = [records]; } - }, - // remap record ids in MixedCollection after records have been realized. @see Store#onCreateRecords, @see DataReader#realize - reMap : function(record) { - if (Ext.isArray(record)) { - for (var i = 0, len = record.length; i < len; i++) { - this.reMap(record[i]); + /* + * Pass the isMove parameter if we know we're going to be re-inserting this record + */ + isMove = isMove === true; + var me = this, + sync = false, + i = 0, + length = records.length, + isPhantom, + index, + record; + + for (; i < length; i++) { + record = records[i]; + index = me.data.indexOf(record); + + if (me.snapshot) { + me.snapshot.remove(record); + } + + if (index > -1) { + isPhantom = record.phantom === true; + if (!isMove && !isPhantom) { + // don't push phantom records onto removed + me.removed.push(record); + } + + record.unjoin(me); + me.data.remove(record); + sync = sync || !isPhantom; + + me.fireEvent('remove', me, record, index); } - } else { - delete this.data.map[record._phid]; - this.data.map[record.id] = record; - var index = this.data.keys.indexOf(record._phid); - this.data.keys.splice(index, 1, record.id); - delete record._phid; + } + + me.fireEvent('datachanged', me); + if (!isMove && me.autoSync && sync) { + me.sync(); } }, - // @protected onCreateRecord proxy callback for create action - onCreateRecords : function(success, rs, data) { - if (success === true) { - try { - this.reader.realize(rs, data); - this.reMap(rs); - } - catch (e) { - this.handleException(e); - if (Ext.isArray(rs)) { - // Recurse to run back into the try {}. DataReader#realize splices-off the rs until empty. - this.onCreateRecords(success, rs, data); + /** + * Removes the model instance at the given index + * @param {Number} index The record index + */ + removeAt: function(index) { + var record = this.getAt(index); + + if (record) { + this.remove(record); + } + }, + + /** + *

Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an + * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved + * instances into the Store and calling an optional callback if required. Example usage:

+ * +

+store.load({
+    scope   : this,
+    callback: function(records, operation, success) {
+        //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
+        console.log(records);
+    }
+});
+
+ * + *

If the callback scope does not need to be set, a function can simply be passed:

+ * +

+store.load(function(records, operation, success) {
+    console.log('loaded records');
+});
+
+ * + * @param {Object/Function} options Optional config object, passed into the Ext.data.Operation object before loading. + */ + load: function(options) { + var me = this; + + options = options || {}; + + if (Ext.isFunction(options)) { + options = { + callback: options + }; + } + + Ext.applyIf(options, { + groupers: me.groupers.items, + page: me.currentPage, + start: (me.currentPage - 1) * me.pageSize, + limit: me.pageSize, + addRecords: false + }); + + return me.callParent([options]); + }, + + /** + * @private + * Called internally when a Proxy has completed a load request + */ + onProxyLoad: function(operation) { + var me = this, + resultSet = operation.getResultSet(), + records = operation.getRecords(), + successful = operation.wasSuccessful(); + + if (resultSet) { + me.totalCount = resultSet.total; + } + + if (successful) { + me.loadRecords(records, operation); + } + + me.loading = false; + me.fireEvent('load', me, records, successful); + + //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not. + //People are definitely using this so can't deprecate safely until 2.x + me.fireEvent('read', me, records, operation.wasSuccessful()); + + //this is a callback that would have been passed to the 'read' function and is optional + Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]); + }, + + /** + * Create any new records when a write is returned from the server. + * @private + * @param {Array} records The array of new records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onCreateRecords: function(records, operation, success) { + if (success) { + var i = 0, + data = this.data, + snapshot = this.snapshot, + length = records.length, + originalRecords = operation.records, + record, + original, + index; + + /* + * Loop over each record returned from the server. Assume they are + * returned in order of how they were sent. If we find a matching + * record, replace it with the newly created one. + */ + for (; i < length; ++i) { + record = records[i]; + original = originalRecords[i]; + if (original) { + index = data.indexOf(original); + if (index > -1) { + data.removeAt(index); + data.insert(index, record); + } + if (snapshot) { + index = snapshot.indexOf(original); + if (index > -1) { + snapshot.removeAt(index); + snapshot.insert(index, record); + } + } + record.phantom = false; + record.join(this); } } } }, - // @protected, onUpdateRecords proxy callback for update action - onUpdateRecords : function(success, rs, data) { - if (success === true) { - try { - this.reader.update(rs, data); - } catch (e) { - this.handleException(e); - if (Ext.isArray(rs)) { - // Recurse to run back into the try {}. DataReader#update splices-off the rs until empty. - this.onUpdateRecords(success, rs, data); + /** + * Update any records when a write is returned from the server. + * @private + * @param {Array} records The array of updated records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onUpdateRecords: function(records, operation, success){ + if (success) { + var i = 0, + length = records.length, + data = this.data, + snapshot = this.snapshot, + record; + + for (; i < length; ++i) { + record = records[i]; + data.replace(record); + if (snapshot) { + snapshot.replace(record); } + record.join(this); } } }, - // @protected onDestroyRecords proxy callback for destroy action - onDestroyRecords : function(success, rs, data) { - // splice each rec out of this.removed - rs = (rs instanceof Ext.data.Record) ? [rs] : rs; - for (var i=0,len=rs.length;i=0;i--) { - this.insert(rs[i].lastIndex, rs[i]); // <-- lastIndex set in Store#destroyRecord + /** + * Remove any records when a write is returned from the server. + * @private + * @param {Array} records The array of removed records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onDestroyRecords: function(records, operation, success){ + if (success) { + var me = this, + i = 0, + length = records.length, + data = me.data, + snapshot = me.snapshot, + record; + + for (; i < length; ++i) { + record = records[i]; + record.unjoin(me); + data.remove(record); + if (snapshot) { + snapshot.remove(record); + } } + me.removed = []; } }, - // protected handleException. Possibly temporary until Ext framework has an exception-handler. - handleException : function(e) { - // @see core/Error.js - Ext.handleError(e); + //inherit docs + getNewRecords: function() { + return this.data.filterBy(this.filterNew).items; + }, + + //inherit docs + getUpdatedRecords: function() { + return this.data.filterBy(this.filterUpdated).items; }, /** - *

Reloads the Record cache from the configured Proxy using the configured {@link Ext.data.Reader Reader} and - * the options from the last load operation performed.

- *

Note: see the Important note in {@link #load}.

- * @param {Object} options (optional) An Object containing {@link #load loading options} which may - * override the options used in the last {@link #load} operation. See {@link #load} for details (defaults to - * null, in which case the {@link #lastOptions} are used). + * Filters the loaded set of records by a given set of filters. + * @param {Mixed} filters The set of filters to apply to the data. These are stored internally on the store, + * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See + * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively, + * pass in a property string + * @param {String} value Optional value to filter by (only if using a property string as the first argument) */ - reload : function(options){ - this.load(Ext.applyIf(options||{}, this.lastOptions)); - }, + filter: function(filters, value) { + if (Ext.isString(filters)) { + filters = { + property: filters, + value: value + }; + } - // private - // Called as a callback by the Reader during a load operation. - loadRecords : function(o, options, success){ - if(!o || success === false){ - if(success !== false){ - this.fireEvent('load', this, [], options); - } - if(options.callback){ - options.callback.call(options.scope || this, [], options, false, o); - } - return; + var me = this, + decoded = me.decodeFilters(filters), + i = 0, + doLocalSort = me.sortOnFilter && !me.remoteSort, + length = decoded.length; + + for (; i < length; i++) { + me.filters.replace(decoded[i]); } - var r = o.records, t = o.totalRecords || r.length; - if(!options || options.add !== true){ - if(this.pruneModifiedRecords){ - this.modified = []; - } - for(var i = 0, len = r.length; i < len; i++){ - r[i].join(this); - } - if(this.snapshot){ - this.data = this.snapshot; - delete this.snapshot; + + if (me.remoteFilter) { + //the load function will pick up the new filters and request the filtered data from the proxy + me.load(); + } else { + /** + * A pristine (unfiltered) collection of the records in this store. This is used to reinstate + * records when a filter is removed or changed + * @property snapshot + * @type Ext.util.MixedCollection + */ + if (me.filters.getCount()) { + me.snapshot = me.snapshot || me.data.clone(); + me.data = me.data.filter(me.filters.items); + + if (doLocalSort) { + me.sort(); + } + // fire datachanged event if it hasn't already been fired by doSort + if (!doLocalSort || me.sorters.length < 1) { + me.fireEvent('datachanged', me); + } } - this.data.clear(); - this.data.addAll(r); - this.totalLength = t; - this.applySort(); - this.fireEvent('datachanged', this); - }else{ - this.totalLength = Math.max(t, this.data.length+r.length); - this.add(r); - } - this.fireEvent('load', this, r, options); - if(options.callback){ - options.callback.call(options.scope || this, r, options, true); } }, /** - * Loads data from a passed data block and fires the {@link #load} event. A {@link Ext.data.Reader Reader} - * which understands the format of the data must have been configured in the constructor. - * @param {Object} data The data block from which to read the Records. The format of the data expected - * is dependent on the type of {@link Ext.data.Reader Reader} that is configured and should correspond to - * that {@link Ext.data.Reader Reader}'s {@link Ext.data.Reader#readRecords} parameter. - * @param {Boolean} append (Optional) true to append the new Records rather the default to replace - * the existing cache. - * Note: that Records in a Store are keyed by their {@link Ext.data.Record#id id}, so added Records - * with ids which are already present in the Store will replace existing Records. Only Records with - * new, unique ids will be added. + * Revert to a view of the Record cache with no filtering applied. + * @param {Boolean} suppressEvent If true the filter is cleared silently without firing the + * {@link #datachanged} event. */ - loadData : function(o, append){ - var r = this.reader.readRecords(o); - this.loadRecords(r, {add: append}, true); - }, + clearFilter: function(suppressEvent) { + var me = this; - /** - * Gets the number of cached records. - *

If using paging, this may not be the total size of the dataset. If the data object - * used by the Reader contains the dataset size, then the {@link #getTotalCount} function returns - * the dataset size. Note: see the Important note in {@link #load}.

- * @return {Number} The number of Records in the Store's cache. - */ - getCount : function(){ - return this.data.length || 0; + me.filters.clear(); + + if (me.remoteFilter) { + me.load(); + } else if (me.isFiltered()) { + me.data = me.snapshot.clone(); + delete me.snapshot; + + if (suppressEvent !== true) { + me.fireEvent('datachanged', me); + } + } }, /** - * Gets the total number of records in the dataset as returned by the server. - *

If using paging, for this to be accurate, the data object used by the {@link #reader Reader} - * must contain the dataset size. For remote data sources, the value for this property - * (totalProperty for {@link Ext.data.JsonReader JsonReader}, - * totalRecords for {@link Ext.data.XmlReader XmlReader}) shall be returned by a query on the server. - * Note: see the Important note in {@link #load}.

- * @return {Number} The number of Records as specified in the data object passed to the Reader - * by the Proxy. - *

Note: this value is not updated when changing the contents of the Store locally.

+ * Returns true if this store is currently filtered + * @return {Boolean} */ - getTotalCount : function(){ - return this.totalLength || 0; + isFiltered: function() { + var snapshot = this.snapshot; + return !! snapshot && snapshot !== this.data; }, /** - * Returns an object describing the current sort state of this Store. - * @return {Object} The sort state of the Store. An object with two properties: