X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/c930e9176a5a85509c5b0230e2bff5c22a591432..2e847cf21b8ab9d15fa167b315ca5b2fa92638fc:/src/data/Store.js diff --git a/src/data/Store.js b/src/data/Store.js index fb368f73..b8447c75 100644 --- a/src/data/Store.js +++ b/src/data/Store.js @@ -1,6 +1,6 @@ /*! - * Ext JS Library 3.0.0 - * Copyright(c) 2006-2009 Ext JS, LLC + * Ext JS Library 3.1.1 + * Copyright(c) 2006-2010 Ext JS, LLC * licensing@extjs.com * http://www.extjs.com/license */ @@ -67,286 +67,16 @@ 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}) * + *

Writing Data

+ *

And new in Ext version 3, use the new {@link Ext.data.DataWriter DataWriter} to create an automated, Writable Store + * along with RESTful features. * @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; - } - if(this.reader.onMetaChange){ - this.reader.onMetaChange = this.onMetaChange.createDelegate(this); - } - if (this.writer) { // writer passed - this.writer.meta = this.reader.meta; - this.pruneModifiedRecords = true; - } - } - - /** - * 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'}
-    ]
-});
-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);
-        }
-    }]
-});
-     * 
- * @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 = []; - - 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' - ); - - 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 - }); - } - - this.sortToggle = {}; - if(this.sortField){ - this.setDefaultSort(this.sortField, this.sortDir); - }else if(this.sortInfo){ - this.setDefaultSort(this.sortInfo.field, this.sortInfo.direction); - } - - Ext.data.Store.superclass.constructor.call(this); - - 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, { +Ext.data.Store = Ext.extend(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 @@ -495,7 +225,7 @@ sortInfo: { * internally be set to false.

*/ restful: false, - + /** * @cfg {Object} paramNames *

An object containing properties which specify the names of the paging and @@ -515,7 +245,7 @@ sortInfo: { * the parameter names to use in its {@link #load requests}. */ paramNames : undefined, - + /** * @cfg {Object} defaultParamNames * Provides the default values for the {@link #paramNames} property. To globally modify the parameters @@ -528,17 +258,349 @@ sortInfo: { dir : 'dir' }, + // private + batchKey : '_ext_batch_', + + constructor : function(config){ + this.data = new Ext.util.MixedCollection(false); + this.data.getKey = function(o){ + return o.id; + }; + + + // temporary removed-records cache + this.removed = []; + + if(config && config.data){ + this.inlineData = config.data; + delete config.data; + } + + Ext.apply(this, config); + + /** + * See the {@link #baseParams corresponding configuration option} + * for a description of this property. + * To modify this property see {@link #setBaseParam}. + * @property + */ + this.baseParams = Ext.isObject(this.baseParams) ? this.baseParams : {}; + + this.paramNames = Ext.applyIf(this.paramNames || {}, this.defaultParamNames); + + if((this.url || this.api) && !this.proxy){ + this.proxy = new Ext.data.HttpProxy({url: this.url, api: this.api}); + } + // 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. + // TODO might want to allow implemention of faux REST where batch is possible using RESTful routes only. + this.batch = false; + Ext.data.Api.restify(this.proxy); + } + + if(this.reader){ // reader passed + if(!this.recordType){ + this.recordType = this.reader.recordType; + } + if(this.reader.onMetaChange){ + this.reader.onMetaChange = this.reader.onMetaChange.createSequence(this.onMetaChange, this); + } + if (this.writer) { // writer passed + if (this.writer instanceof(Ext.data.DataWriter) === false) { // <-- config-object instead of instance. + this.writer = this.buildWriter(this.writer); + } + this.writer.meta = this.reader.meta; + this.pruneModifiedRecords = true; + } + } + + /** + * 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'}
+        ]
+    });
+    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);
+            }
+        }]
+    });
+         * 
+ * @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 = []; + + 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 + * @param {Record[]} The records that were cleared. + */ + '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 {Ext.data.Store} store + * @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 of the action is determined in the result['successProperty']property (NOTE for RESTful stores, + * a simple 20x response is sufficient for the actions "destroy" and "update". The "create" action should should return 200 along with a database pk). + * @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', + /** + * @event beforesave + * Fires before a save action is called. A save encompasses destroying records, updating records and creating records. + * @param {Ext.data.Store} store + * @param {Object} data An object containing the data that is to be saved. The object will contain a key for each appropriate action, + * with an array of records for each action. + */ + 'beforesave', + /** + * @event save + * Fires after a save is completed. A save encompasses destroying records, updating records and creating records. + * @param {Ext.data.Store} store + * @param {Number} batch The identifier for the batch that was saved. + * @param {Object} data An object containing the data that is to be saved. The object will contain a key for each appropriate action, + * with an array of records for each action. + */ + 'save' + + ); + + if(this.proxy){ + // TODO remove deprecated loadexception with ext-3.0.1 + 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, + clear: this.onClear + }); + } + + this.sortToggle = {}; + if(this.sortField){ + this.setDefaultSort(this.sortField, this.sortDir); + }else if(this.sortInfo){ + this.setDefaultSort(this.sortInfo.field, this.sortInfo.direction); + } + + Ext.data.Store.superclass.constructor.call(this); + + 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]); + } + // used internally to uniquely identify a batch + this.batchCounter = 0; + this.batches = {}; + }, + + /** + * builds a DataWriter instance when Store constructor is provided with a writer config-object instead of an instace. + * @param {Object} config Writer configuration + * @return {Ext.data.DataWriter} + * @private + */ + buildWriter : function(config) { + var klass = undefined, + type = (config.format || 'json').toLowerCase(); + switch (type) { + case 'json': + klass = Ext.data.JsonWriter; + break; + case 'xml': + klass = Ext.data.XmlWriter; + break; + default: + klass = Ext.data.JsonWriter; + } + return new klass(config); + }, + /** * Destroys the store. */ destroy : function(){ - if(this.storeId){ - Ext.StoreMgr.unregister(this); + if(!this.isDestroyed){ + if(this.storeId){ + Ext.StoreMgr.unregister(this); + } + this.clearData(); + this.data = null; + Ext.destroy(this.proxy); + this.reader = this.writer = null; + this.purgeListeners(); + this.isDestroyed = true; } - this.data = null; - Ext.destroy(this.proxy); - this.reader = this.writer = null; - this.purgeListeners(); }, /** @@ -575,19 +637,27 @@ sortInfo: { }, /** - * 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. + * Remove Records from the Store and fires the {@link #remove} event. + * @param {Ext.data.Record/Ext.data.Record[]} record The record object or array of records to remove from the cache. */ remove : function(record){ + if(Ext.isArray(record)){ + Ext.each(record, function(r){ + this.remove(r); + }, this); + } var index = this.data.indexOf(record); if(index > -1){ + record.join(null); this.data.removeAt(index); - if(this.pruneModifiedRecords){ - this.modified.remove(record); - } - if(this.snapshot){ - this.snapshot.remove(record); - } + } + if(this.pruneModifiedRecords){ + this.modified.remove(record); + } + if(this.snapshot){ + this.snapshot.remove(record); + } + if(index > -1){ this.fireEvent('remove', this, record, index); } }, @@ -602,16 +672,30 @@ sortInfo: { /** * Remove all Records from the Store and fires the {@link #clear} event. + * @param {Boolean} silent [false] Defaults to false. Set true to not fire clear event. */ - removeAll : function(){ - this.data.clear(); + removeAll : function(silent){ + var items = []; + this.each(function(rec){ + items.push(rec); + }); + this.clearData(); if(this.snapshot){ this.snapshot.clear(); } if(this.pruneModifiedRecords){ this.modified = []; } - this.fireEvent('clear', this); + if (silent !== true) { // <-- prevents write-actions when we just want to clear a store. + this.fireEvent('clear', this, items); + } + }, + + // private + onClear: function(store, records){ + Ext.each(records, function(rec, index){ + this.destroyRecord(this, rec, index); + }, this); }, /** @@ -626,6 +710,9 @@ sortInfo: { this.data.insert(index, records[i]); records[i].join(this); } + if(this.snapshot){ + this.snapshot.addAll(records); + } this.fireEvent('add', this, records, index); }, @@ -653,7 +740,7 @@ sortInfo: { * @return {Ext.data.Record} The Record with the passed id. Returns undefined if not found. */ getById : function(id){ - return this.data.key(id); + return (this.snapshot || this.data).key(id); }, /** @@ -683,6 +770,14 @@ sortInfo: { this.lastOptions = o; }, + // private + clearData: function(){ + this.data.each(function(rec) { + rec.join(null); + }); + this.data.clear(); + }, + /** *

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

*

Notes:

- *
  • callback : Function

    A function to be called after the Records - * have been loaded. The callback is called after the load event and is passed the following arguments:

  • - *
  • scope : Object

    Scope with which to call the callback (defaults + *

  • callback : Function

    A function to be called after the Records + * have been loaded. The callback is called after the load event is fired, and is passed the following arguments:

  • + *
  • scope : Object

    Scope with which to call the callback (defaults * to the Store object)

  • - *
  • add : Boolean

    Indicator to append loaded records rather than + *

  • add : Boolean

    Indicator to append loaded records rather than * replace the current cache. Note: see note for {@link #loadData}

  • * * @return {Boolean} If the developer provided {@link #beforeload} event handler returns @@ -740,7 +835,7 @@ sortInfo: { * @private */ updateRecord : function(store, record, action) { - if (action == Ext.data.Record.EDIT && this.autoSave === true && (!record.phantom || (record.phantom && record.isValid))) { + if (action == Ext.data.Record.EDIT && this.autoSave === true && (!record.phantom || (record.phantom && record.isValid()))) { this.save(); } }, @@ -799,25 +894,29 @@ sortInfo: { * @throws Error * @private */ - execute : function(action, rs, options) { + execute : function(action, rs, options, /* private */ batch) { // 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); } - // make sure options has a params key + // make sure options has a fresh, new params hash options = Ext.applyIf(options||{}, { params: {} }); - + if(batch !== undefined){ + this.addToBatch(batch); + } // 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; if (action === 'read') { doRequest = this.fireEvent('beforeload', this, options); + Ext.applyIf(options.params, this.baseParams); } else { - // if Writer is configured as listful, force single-recoord rs to be [{}} instead of {} + // if Writer is configured as listful, force single-record rs to be [{}] instead of {} + // TODO Move listful rendering into DataWriter where the @cfg is defined. Should be easy now. if (this.writer.listful === true && this.restful !== true) { rs = (Ext.isArray(rs)) ? rs : [rs]; } @@ -827,18 +926,20 @@ sortInfo: { } // Write the action to options.params if ((doRequest = this.fireEvent('beforewrite', this, action, rs, options)) !== false) { - this.writer.write(action, options.params, rs); + this.writer.apply(options.params, this.baseParams, action, rs); } } 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; + options.params.xaction = action; // <-- really old, probaby unecessary. } - // 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); + // 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 user's configured DataProxy#api + // TODO Refactor all Proxies to accept an instance of Ext.data.Request (not yet defined) instead of this looooooong list + // of params. This method is an artifact from Ext2. + this.proxy.request(Ext.data.Api.actions[action], rs, options.params, this.reader, this.createCallback(action, rs, batch), this, options); } return doRequest; }, @@ -855,81 +956,140 @@ sortInfo: { * * @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. + * @return {Number} batch Returns a number to uniquely identify the "batch" of saves occurring. -1 will be returned + * if there are no items to save or the save was cancelled. */ save : function() { if (!this.writer) { throw new Ext.data.Store.Error('writer-undefined'); } + var queue = [], + len, + trans, + batch, + data = {}; // 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); + if(this.removed.length){ + queue.push(['destroy', this.removed]); } // 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; - } - - // 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); + if(rs.length){ + // 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); } - } 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); - } + // If we have valid phantoms, create them... + if(phantoms.length){ + queue.push(['create', phantoms]); + } - // 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); + // UPDATE: And finally, if we're still here after splicing-off phantoms and !isValid real records, update the rest... + if(rs.length){ + queue.push(['update', rs]); + } } - return true; + len = queue.length; + if(len){ + batch = ++this.batchCounter; + for(var i = 0; i < len; ++i){ + trans = queue[i]; + data[trans[0]] = trans[1]; + } + if(this.fireEvent('beforesave', this, data) !== false){ + for(var i = 0; i < len; ++i){ + trans = queue[i]; + this.doTransaction(trans[0], trans[1], batch); + } + return batch; + } + } + return -1; }, // private. Simply wraps call to Store#execute in try/catch. Defers to Store#handleException on error. Loops if batch: false - doTransaction : function(action, rs) { + doTransaction : function(action, rs, batch) { function transaction(records) { - try { - this.execute(action, records); - } catch (e) { + try{ + this.execute(action, records, undefined, batch); + }catch (e){ this.handleException(e); } } - if (this.batch === false) { - for (var i = 0, len = rs.length; i < len; i++) { + if(this.batch === false){ + for(var i = 0, len = rs.length; i < len; i++){ transaction.call(this, rs[i]); } - } else { + }else{ transaction.call(this, rs); } }, + // private + addToBatch : function(batch){ + var b = this.batches, + key = this.batchKey + batch, + o = b[key]; + + if(!o){ + b[key] = o = { + id: batch, + count: 0, + data: {} + } + } + ++o.count; + }, + + removeFromBatch : function(batch, action, data){ + var b = this.batches, + key = this.batchKey + batch, + o = b[key], + data, + arr; + + + if(o){ + arr = o.data[action] || []; + o.data[action] = arr.concat(data); + if(o.count === 1){ + data = o.data; + delete b[key]; + this.fireEvent('save', this, batch, data); + }else{ + --o.count; + } + } + }, + // @private callback-handler for remote CRUD actions // Do not override -- override loadRecords, onCreateRecords, onDestroyRecords and onUpdateRecords instead. - createCallback : function(action, rs) { + createCallback : function(action, rs, batch) { 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); + this['on' + Ext.util.Format.capitalize(action) + 'Records'](success, rs, [].concat(data)); // If success === false here, exception will have been called in DataProxy if (success === true) { this.fireEvent('write', this, action, data, response, rs); } + this.removeFromBatch(batch, action, data); }; }, // 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? + // TODO remove this method? clearModified : function(rs) { if (Ext.isArray(rs)) { for (var n=rs.length-1;n>=0;n--) { @@ -990,7 +1150,7 @@ sortInfo: { // @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; + rs = (rs instanceof Ext.data.Record) ? [rs] : [].concat(rs); for (var i=0,len=rs.length;iReloads the Record cache from the configured Proxy using the configured {@link Ext.data.Reader Reader} and - * the options from the last load operation performed.

    + *

    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). + * @param {Object} options

    (optional) An Object containing + * {@link #load loading options} which may override the {@link #lastOptions options} + * used in the last {@link #load} operation. See {@link #load} for details + * (defaults to null, in which case the {@link #lastOptions} are + * used).

    + *

    To add new params to the existing params:

    
    +lastOptions = myStore.lastOptions;
    +Ext.apply(lastOptions.params, {
    +    myNewParam: true
    +});
    +myStore.reload(lastOptions);
    +     * 
    */ reload : function(options){ this.load(Ext.applyIf(options||{}, this.lastOptions)); @@ -1024,6 +1194,9 @@ sortInfo: { // private // Called as a callback by the Reader during a load operation. loadRecords : function(o, options, success){ + if (this.isDestroyed === true) { + return; + } if(!o || success === false){ if(success !== false){ this.fireEvent('load', this, [], options); @@ -1045,7 +1218,7 @@ sortInfo: { this.data = this.snapshot; delete this.snapshot; } - this.data.clear(); + this.clearData(); this.data.addAll(r); this.totalLength = t; this.applySort(); @@ -1191,7 +1364,8 @@ sortInfo: { * Calls the specified function for each of the {@link Ext.data.Record Records} in the cache. * @param {Function} fn The function to call. The {@link Ext.data.Record Record} is passed as the first parameter. * Returning false aborts and exits the iteration. - * @param {Object} scope (optional) The scope in which to call the function (defaults to the {@link Ext.data.Record Record}). + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. + * Defaults to the current {@link Ext.data.Record Record} in the iteration. */ each : function(fn, scope){ this.data.each(fn, scope); @@ -1262,7 +1436,7 @@ sortInfo: { * to test for filtering. Access field values using {@link Ext.data.Record#get}.

    *
  • id : Object

    The ID of the Record passed.

  • * - * @param {Object} scope (optional) The scope of the function (defaults to this) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. */ filterBy : function(fn, scope){ this.snapshot = this.snapshot || this.data; @@ -1293,7 +1467,7 @@ sortInfo: { * to test for filtering. Access field values using {@link Ext.data.Record#get}.

    *
  • id : Object

    The ID of the Record passed.

  • * - * @param {Object} scope (optional) The scope of the function (defaults to this) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. * @return {MixedCollection} Returns an Ext.util.MixedCollection of the matched records **/ queryBy : function(fn, scope){ @@ -1302,10 +1476,10 @@ sortInfo: { }, /** - * Finds the index of the first matching record in this store by a specific property/value. - * @param {String} property A property on your objects - * @param {String/RegExp} value Either a string that the property value - * should begin with, or a RegExp to test against the property. + * Finds the index of the first matching Record in this store by a specific field value. + * @param {String} fieldName The name of the Record field to test. + * @param {String/RegExp} value Either a string that the field value + * should begin with, or a RegExp to test against the field. * @param {Number} startIndex (optional) The index to start searching at * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning * @param {Boolean} caseSensitive (optional) True for case sensitive comparison @@ -1317,9 +1491,9 @@ sortInfo: { }, /** - * Finds the index of the first matching record in this store by a specific property/value. - * @param {String} property A property on your objects - * @param {String/RegExp} value The value to match against + * Finds the index of the first matching Record in this store by a specific field value. + * @param {String} fieldName The name of the Record field to test. + * @param {Mixed} value The value to match the field against. * @param {Number} startIndex (optional) The index to start searching at * @return {Number} The matched index or -1 */ @@ -1337,7 +1511,7 @@ sortInfo: { * to test for filtering. Access field values using {@link Ext.data.Record#get}.

    *
  • id : Object

    The ID of the Record passed.

  • * - * @param {Object} scope (optional) The scope of the function (defaults to this) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. * @param {Number} startIndex (optional) The index to start searching at * @return {Number} The matched index or -1 */ @@ -1432,18 +1606,27 @@ sortInfo: { for(var i = 0, len = m.length; i < len; i++){ m[i].reject(); } + var m = this.removed.slice(0).reverse(); + this.removed = []; + for(var i = 0, len = m.length; i < len; i++){ + this.insert(m[i].lastIndex||0, m[i]); + m[i].reject(); + } }, // private - onMetaChange : function(meta, rtype, o){ - this.recordType = rtype; - this.fields = rtype.prototype.fields; + onMetaChange : function(meta){ + this.recordType = this.reader.recordType; + this.fields = this.recordType.prototype.fields; delete this.snapshot; - if(meta.sortInfo){ - this.sortInfo = meta.sortInfo; + if(this.reader.meta.sortInfo){ + this.sortInfo = this.reader.meta.sortInfo; }else if(this.sortInfo && !this.fields.get(this.sortInfo.field)){ delete this.sortInfo; } + if(this.writer){ + this.writer.meta = this.reader.meta; + } this.modified = []; this.fireEvent('metachange', this, this.reader.meta); }, @@ -1489,4 +1672,3 @@ Ext.apply(Ext.data.Store.Error.prototype, { 'writer-undefined' : 'Attempted to execute a write-action without a DataWriter installed.' } }); -