/*!
- * 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
*/
+
/**
* @class Ext.data.Api
* @extends Object
proxy.api[action] = proxy.api[action] || proxy.url || proxy.directFn;
if (typeof(proxy.api[action]) == 'string') {
proxy.api[action] = {
- url: proxy.api[action]
+ url: proxy.api[action],
+ method: (proxy.restful === true) ? Ext.data.Api.restActions[action] : undefined
};
}
}
restify : function(proxy) {
proxy.restful = true;
for (var verb in this.restActions) {
- proxy.api[this.actions[verb]].method = this.restActions[verb];
+ proxy.api[this.actions[verb]].method ||
+ (proxy.api[this.actions[verb]].method = this.restActions[verb]);
}
+ // TODO: perhaps move this interceptor elsewhere? like into DataProxy, perhaps? Placed here
+ // to satisfy initial 3.0 final release of REST features.
+ proxy.onWrite = proxy.onWrite.createInterceptor(function(action, o, response, rs) {
+ var reader = o.reader;
+ var res = new Ext.data.Response({
+ action: action,
+ raw: response
+ });
+
+ switch (response.status) {
+ case 200: // standard 200 response, send control back to HttpProxy#onWrite by returning true from this intercepted #onWrite
+ return true;
+ break;
+ case 201: // entity created but no response returned
+ if (Ext.isEmpty(res.raw.responseText)) {
+ res.success = true;
+ } else {
+ //if the response contains data, treat it like a 200
+ return true;
+ }
+ break;
+ case 204: // no-content. Create a fake response.
+ res.success = true;
+ res.data = null;
+ break;
+ default:
+ return true;
+ break;
+ }
+ if (res.success === true) {
+ this.fireEvent("write", this, action, res.data, res, rs, o.request.arg);
+ } else {
+ this.fireEvent('exception', this, 'remote', action, o, res, rs);
+ }
+ o.request.callback.call(o.request.scope, res.data, res, res.success);
+
+ return false; // <-- false to prevent intercepted function from running.
+ }, proxy);
}
};
})();
+/**
+ * Ext.data.Response
+ * Experimental. Do not use directly.
+ */
+Ext.data.Response = function(params, response) {
+ Ext.apply(this, params, {
+ raw: response
+ });
+};
+Ext.data.Response.prototype = {
+ message : null,
+ success : false,
+ status : null,
+ root : null,
+ raw : null,
+
+ getMessage : function() {
+ return this.message;
+ },
+ getSuccess : function() {
+ return this.success;
+ },
+ getStatus : function() {
+ return this.status;
+ },
+ getRoot : function() {
+ return this.root;
+ },
+ getRawResponse : function() {
+ return this.raw;
+ }
+};
+
/**
* @class Ext.data.Api.Error
* @extends Ext.Error
'execute': 'Attempted to execute an unknown action. Valid API actions are defined in Ext.data.Api.actions"'
}
});
+
+
\r
/**\r
* @class Ext.data.SortTypes\r
* <code>{@link #data}</code> and <code>{@link #id}</code> properties.</p>
* <p>Record objects generated by this constructor inherit all the methods of Ext.data.Record listed below.</p>
* @constructor
- * This constructor should not be used to create Record objects. Instead, use {@link #create} to
- * generate a subclass of Ext.data.Record configured with information about its constituent fields.
+ * <p>This constructor should not be used to create Record objects. Instead, use {@link #create} to
+ * generate a subclass of Ext.data.Record configured with information about its constituent fields.<p>
+ * <p><b>The generated constructor has the same signature as this constructor.</b></p>
* @param {Object} data (Optional) An object, the properties of which provide values for the new Record's
* fields. If not specified the <code>{@link Ext.data.Field#defaultValue defaultValue}</code>
* for each field will be assigned.
- * @param {Object} id (Optional) The id of the Record. This id should be unique, and is used by the
- * {@link Ext.data.Store} object which owns the Record to index its collection of Records. If
- * an <code>id</code> is not specified a <b><code>{@link #phantom}</code></b> Record will be created
- * with an {@link #Record.id automatically generated id}.
+ * @param {Object} id (Optional) The id of the Record. The id is used by the
+ * {@link Ext.data.Store} object which owns the Record to index its collection
+ * of Records (therefore this id should be unique within each store). If an
+ * <code>id</code> is not specified a <b><code>{@link #phantom}</code></b>
+ * Record will be created with an {@link #Record.id automatically generated id}.
*/
Ext.data.Record = function(data, id){
// if no id, call the auto id method
</code></pre>
* @method create
* @return {function} A constructor which is used to create new Records according
- * to the definition. The constructor has the same signature as {@link #Ext.data.Record}.
+ * to the definition. The constructor has the same signature as {@link #Record}.
* @static
*/
Ext.data.Record.create = function(o){
* @property id
* @type {Object}
*/
+ /**
+ * <p><b>Only present if this Record was created by an {@link Ext.data.XmlReader XmlReader}</b>.</p>
+ * <p>The XML element which was the source of the data for this Record.</p>
+ * @property node
+ * @type {XMLElement}
+ */
+ /**
+ * <p><b>Only present if this Record was created by an {@link Ext.data.ArrayReader ArrayReader} or a {@link Ext.data.JsonReader JsonReader}</b>.</p>
+ * <p>The Array or object which was the source of the data for this Record.</p>
+ * @property json
+ * @type {Array|Object}
+ */
/**
* Readonly flag - true if this Record has been modified.
* @type Boolean
*/
dirty : false,
editing : false,
- error: null,
+ error : null,
/**
* This object contains a key and value storing the original values of all modified
* fields or is null if no fields have been modified.
* @property modified
* @type {Object}
*/
- modified: null,
+ modified : null,
/**
- * <tt>false</tt> when the record does not yet exist in a server-side database (see
+ * <tt>true</tt> when the record does not yet exist in a server-side database (see
* {@link #markDirty}). Any record which has a real database pk set as its id property
* is NOT a phantom -- it's real.
* @property phantom
// update the record in the store, bypass setting dirty flag,
// and do not store the change in the {@link Ext.data.Store#getModifiedRecords modified records}
-rec.{@link #data}['firstname'] = 'Wilma'); // updates record, but not the view
+rec.{@link #data}['firstname'] = 'Wilma'; // updates record, but not the view
rec.{@link #commit}(); // updates the view
* </code></pre>
* <b>Notes</b>:<div class="mdetail-params"><ul>
* event fire.</li>
* </ul></div>
* @param {String} name The {@link Ext.data.Field#name name of the field} to set.
- * @param {Object} value The value to set the field to.
+ * @param {String/Object/Array} value The value to set the field to.
*/
set : function(name, value){
- var isObj = (typeof value === 'object');
- if(!isObj && String(this.data[name]) === String(value)){
+ var encode = Ext.isPrimitive(value) ? String : Ext.encode;
+ if(encode(this.data[name]) == encode(value)) {
return;
- } else if (isObj && Ext.encode(this.data[name]) === Ext.encode(value)) {
- return;
- }
+ }
this.dirty = true;
if(!this.modified){
this.modified = {};
}
- if(typeof this.modified[name] == 'undefined'){
+ if(this.modified[name] === undefined){
this.modified[name] = this.data[name];
}
this.data[name] = value;
},
// private
- afterEdit: function(){
+ afterEdit : function(){
if(this.store){
this.store.afterEdit(this);
}
},
// private
- afterReject: function(){
+ afterReject : function(){
if(this.store){
this.store.afterReject(this);
}
},
// private
- afterCommit: function(){
+ afterCommit : function(){
if(this.store){
this.store.afterCommit(this);
}
},
/**
- * Creates a copy of this Record.
- * @param {String} id (optional) A new Record id, defaults to {@link #Record.id autogenerating an id}.
- * Note: if an <code>id</code> is not specified the copy created will be a
- * <code>{@link #phantom}</code> Record.
+ * Creates a copy (clone) of this Record.
+ * @param {String} id (optional) A new Record id, defaults to the id
+ * of the record being copied. See <code>{@link #id}</code>.
+ * To generate a phantom record with a new id use:<pre><code>
+var rec = record.copy(); // clone the record
+Ext.data.Record.id(rec); // automatically generate a unique sequential id
+ * </code></pre>
* @return {Record}
*/
copy : function(newId) {
this.modified[f.name] = this.data[f.name];
},this);
}
-};/**
+};
+/**
* @class Ext.StoreMgr
* @extends Ext.util.MixedCollection
* The default global group of stores.
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})
* </code></pre>
+ * <p><u>Writing Data</u></p>
+ * <p>And <b>new in Ext version 3</b>, use the new {@link Ext.data.DataWriter DataWriter} to create an automated, <a href="http://extjs.com/deploy/dev/examples/writer/writer.html">Writable Store</a>
+ * along with <a href="http://extjs.com/deploy/dev/examples/restful/restful.html">RESTful features.</a>
* @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 <code>{@link #baseParams corresponding configuration option}</code>
- * for a description of this property.
- * To modify this property see <code>{@link #setBaseParam}</code>.
- * @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.
- * <p>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).</p>
- * <p>This property may be used to create new Records of the type held in this Store, for example:</p><pre><code>
-// 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);
- }
- }]
-});
- * </code></pre>
- * @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:
- * <pre><code>
- Ext.data.Record.EDIT
- Ext.data.Record.REJECT
- Ext.data.Record.COMMIT
- * </code></pre>
- */
- 'update',
- /**
- * @event clear
- * Fires when the data cache has been cleared.
- * @param {Store} this
- */
- 'clear',
- /**
- * @event exception
- * <p>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
- * <tt>false</tt> 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
- * <p>This event is <b>deprecated</b> in favor of the catch-all <b><code>{@link #exception}</code></b>
- * event instead.</p>
- * <p>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 <code>options.params</code> 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 <code>result['successProperty']</code> property.
- * The server-code might set the <code>successProperty</code> to <tt>false</tt> 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 <b>{@link Ext.StoreMgr StoreMgr}</b>.
* <p><b>Note</b>: if a (deprecated) <tt>{@link #id}</tt> is specified it will supersede the <tt>storeId</tt>
* internally be set to <tt>false</tt>.</p>
*/
restful: false,
-
+
/**
* @cfg {Object} paramNames
* <p>An object containing properties which specify the names of the paging and
* 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
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 <code>{@link #baseParams corresponding configuration option}</code>
+ * for a description of this property.
+ * To modify this property see <code>{@link #setBaseParam}</code>.
+ * @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.
+ * <p>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).</p>
+ * <p>This property may be used to create new Records of the type held in this Store, for example:</p><pre><code>
+ // 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);
+ }
+ }]
+ });
+ * </code></pre>
+ * @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:
+ * <pre><code>
+ Ext.data.Record.EDIT
+ Ext.data.Record.REJECT
+ Ext.data.Record.COMMIT
+ * </code></pre>
+ */
+ 'update',
+ /**
+ * @event clear
+ * Fires when the data cache has been cleared.
+ * @param {Store} this
+ * @param {Record[]} The records that were cleared.
+ */
+ 'clear',
+ /**
+ * @event exception
+ * <p>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
+ * <tt>false</tt> 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
+ * <p>This event is <b>deprecated</b> in favor of the catch-all <b><code>{@link #exception}</code></b>
+ * event instead.</p>
+ * <p>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 <code>options.params</code> 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 <code>result['successProperty']</code>property (<b>NOTE</b> 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();
},
/**
},
/**
- * 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);
}
},
/**
* Remove all Records from the Store and fires the {@link #clear} event.
+ * @param {Boolean} silent [false] Defaults to <tt>false</tt>. Set <tt>true</tt> 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);
},
/**
this.data.insert(index, records[i]);
records[i].join(this);
}
+ if(this.snapshot){
+ this.snapshot.addAll(records);
+ }
this.fireEvent('add', this, records, index);
},
* @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);
},
/**
this.lastOptions = o;
},
+ // private
+ clearData: function(){
+ this.data.each(function(rec) {
+ rec.join(null);
+ });
+ this.data.clear();
+ },
+
/**
* <p>Loads the Record cache from the configured <tt>{@link #proxy}</tt> using the configured <tt>{@link #reader}</tt>.</p>
* <br><p>Notes:</p><div class="mdetail-params"><ul>
* parameters to a remote data source. <b>Note</b>: <code>params</code> will override any
* <code>{@link #baseParams}</code> of the same name.</p>
* <p>Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.</p></div></li>
- * <li><b><tt>callback</tt></b> : Function<div class="sub-desc"><p>A function to be called after the Records
- * have been loaded. The <tt>callback</tt> is called after the load event and is passed the following arguments:<ul>
- * <li><tt>r</tt> : Ext.data.Record[]</li>
- * <li><tt>options</tt>: Options object from the load call</li>
- * <li><tt>success</tt>: Boolean success indicator</li></ul></p></div></li>
- * <li><b><tt>scope</tt></b> : Object<div class="sub-desc"><p>Scope with which to call the callback (defaults
+ * <li><b>callback</b> : Function<div class="sub-desc"><p>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:<ul>
+ * <li>r : Ext.data.Record[] An Array of Records loaded.</li>
+ * <li>options : Options object from the load call.</li>
+ * <li>success : Boolean success indicator.</li></ul></p></div></li>
+ * <li><b>scope</b> : Object<div class="sub-desc"><p>Scope with which to call the callback (defaults
* to the Store object)</p></div></li>
- * <li><b><tt>add</tt></b> : Boolean<div class="sub-desc"><p>Indicator to append loaded records rather than
+ * <li><b>add</b> : Boolean<div class="sub-desc"><p>Indicator to append loaded records rather than
* replace the current cache. <b>Note</b>: see note for <tt>{@link #loadData}</tt></p></div></li>
* </ul>
* @return {Boolean} If the <i>developer</i> provided <tt>{@link #beforeload}</tt> event handler returns
* @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();
}
},
* @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];
}
}
// 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;
},
* </pre>
* @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--) {
// @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;i<len;i++) {
this.removed.splice(this.removed.indexOf(rs[i]), 1);
}
},
/**
- * <p>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.</p>
+ * <p>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.</p>
* <p><b>Note</b>: see the Important note in {@link #load}.</p>
- * @param {Object} options (optional) An <tt>Object</tt> containing {@link #load loading options} which may
- * override the options used in the last {@link #load} operation. See {@link #load} for details (defaults to
- * <tt>null</tt>, in which case the {@link #lastOptions} are used).
+ * @param {Object} options <p>(optional) An <tt>Object</tt> 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 <tt>null</tt>, in which case the {@link #lastOptions} are
+ * used).</p>
+ * <br><p>To add new params to the existing params:</p><pre><code>
+lastOptions = myStore.lastOptions;
+Ext.apply(lastOptions.params, {
+ myNewParam: true
+});
+myStore.reload(lastOptions);
+ * </code></pre>
*/
reload : function(options){
this.load(Ext.applyIf(options||{}, this.lastOptions));
// 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);
this.data = this.snapshot;
delete this.snapshot;
}
- this.data.clear();
+ this.clearData();
this.data.addAll(r);
this.totalLength = t;
this.applySort();
* 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 <tt>false</tt> 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 (<code>this</code> 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);
* to test for filtering. Access field values using {@link Ext.data.Record#get}.</p></li>
* <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
* </ul>
- * @param {Object} scope (optional) The scope of the function (defaults to this)
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
*/
filterBy : function(fn, scope){
this.snapshot = this.snapshot || this.data;
* to test for filtering. Access field values using {@link Ext.data.Record#get}.</p></li>
* <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
* </ul>
- * @param {Object} scope (optional) The scope of the function (defaults to this)
+ * @param {Object} scope (optional) The scope (<code>this</code> 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){
},
/**
- * 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
},
/**
- * 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
*/
* to test for filtering. Access field values using {@link Ext.data.Record#get}.</p></li>
* <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
* </ul>
- * @param {Object} scope (optional) The scope of the function (defaults to this)
+ * @param {Object} scope (optional) The scope (<code>this</code> 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
*/
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);
},
'writer-undefined' : 'Attempted to execute a write-action without a DataWriter installed.'
}
});
-
/**
* @class Ext.data.Field
* <p>This class encapsulates the field definition information specified in the field definition objects
case "int":
cv = function(v){
return v !== undefined && v !== null && v !== '' ?
- parseInt(String(v).replace(stripRe, ""), 10) : '';
+ parseInt(String(v).replace(stripRe, ""), 10) : '';
};
break;
case "float":
cv = function(v){
return v !== undefined && v !== null && v !== '' ?
- parseFloat(String(v).replace(stripRe, ""), 10) : '';
+ parseFloat(String(v).replace(stripRe, ""), 10) : '';
};
break;
case "bool":
- case "boolean":
cv = function(v){ return v === true || v === "true" || v == 1; };
break;
case "date":
var parsed = Date.parse(v);
return parsed ? new Date(parsed) : null;
};
- break;
+ break;
+ default:
+ cv = function(v){ return v; };
+ break;
}
this.convert = cv;
reader: new Ext.data.JsonReader(
{
idProperty: 'key',
- root: 'daRoot',
+ root: 'daRoot',
totalProperty: 'total'
},
Dude // recordType
* <tt>"ASC"</tt>.
*/
sortDir : "ASC",
- /**
- * @cfg {Boolean} allowBlank
- * (Optional) Used for validating a {@link Ext.data.Record record}, defaults to <tt>true</tt>.
- * An empty value here will cause {@link Ext.data.Record}.{@link Ext.data.Record#isValid isValid}
- * to evaluate to <tt>false</tt>.
- */
- allowBlank : true
+ /**
+ * @cfg {Boolean} allowBlank
+ * (Optional) Used for validating a {@link Ext.data.Record record}, defaults to <tt>true</tt>.
+ * An empty value here will cause {@link Ext.data.Record}.{@link Ext.data.Record#isValid isValid}
+ * to evaluate to <tt>false</tt>.
+ */
+ allowBlank : true
};/**\r
* @class Ext.data.DataReader\r
* Abstract base class for reading structured data from a data source and converting\r
*/\r
this.recordType = Ext.isArray(recordType) ?\r
Ext.data.Record.create(recordType) : recordType;\r
+
+ // if recordType defined make sure extraction functions are defined\r
+ if (this.recordType){\r
+ this.buildExtractors();\r
+ }
};\r
\r
Ext.data.DataReader.prototype = {\r
-\r
/**\r
- * Abstract method, overridden in {@link Ext.data.JsonReader}\r
+ * @cfg {String} messageProperty [undefined] Optional name of a property within a server-response that represents a user-feedback message.\r
+ */\r
+ /**\r
+ * Abstract method created in extension's buildExtractors impl.\r
+ */\r
+ getTotal: Ext.emptyFn,\r
+ /**\r
+ * Abstract method created in extension's buildExtractors impl.\r
+ */\r
+ getRoot: Ext.emptyFn,\r
+ /**\r
+ * Abstract method created in extension's buildExtractors impl.\r
+ */\r
+ getMessage: Ext.emptyFn,\r
+ /**\r
+ * Abstract method created in extension's buildExtractors impl.\r
+ */\r
+ getSuccess: Ext.emptyFn,\r
+ /**\r
+ * Abstract method created in extension's buildExtractors impl.\r
+ */\r
+ getId: Ext.emptyFn,\r
+ /**\r
+ * Abstract method, overridden in DataReader extensions such as {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader}\r
*/\r
buildExtractors : Ext.emptyFn,\r
+ /**\r
+ * Abstract method overridden in DataReader extensions such as {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader}\r
+ */\r
+ extractData : Ext.emptyFn,\r
+ /**\r
+ * Abstract method overridden in DataReader extensions such as {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader}\r
+ */\r
+ extractValues : Ext.emptyFn,\r
\r
/**\r
* Used for un-phantoming a record after a successful database insert. Sets the records pk along with new data from server.\r
//rs.commit();\r
throw new Ext.data.DataReader.Error('realize', rs);\r
}\r
- this.buildExtractors();\r
- var values = this.extractValues(data, rs.fields.items, rs.fields.items.length);\r
rs.phantom = false; // <-- That's what it's all about\r
rs._phid = rs.id; // <-- copy phantom-id -> _phid, so we can remap in Store#onCreateRecords\r
- rs.id = data[this.meta.idProperty];\r
- rs.data = values;\r
+ rs.id = this.getId(data);\r
+\r
+ rs.fields.each(function(f) {\r
+ if (data[f.name] !== f.defaultValue) {\r
+ rs.data[f.name] = data[f.name];\r
+ }\r
+ });\r
rs.commit();\r
}\r
},\r
\r
/**\r
* Used for updating a non-phantom or "real" record's data with fresh data from server after remote-save.\r
- * You <b>must</b> return a complete new record from the server. If you don't, your local record's missing fields\r
- * will be populated with the default values specified in your Ext.data.Record.create specification. Without a defaultValue,\r
- * local fields will be populated with empty string "". So return your entire record's data after both remote create and update.\r
- * In addition, you <b>must</b> return record-data from the server in the same order received.\r
- * Will perform a commit as well, un-marking dirty-fields. Store's "update" event will be suppressed as the record receives\r
- * a fresh new data-hash.\r
+ * If returning data from multiple-records after a batch-update, you <b>must</b> return record-data from the server in\r
+ * the same order received. Will perform a commit as well, un-marking dirty-fields. Store's "update" event will be\r
+ * suppressed as the record receives fresh new data-hash\r
* @param {Record/Record[]} rs\r
* @param {Object/Object[]} data\r
*/\r
}\r
}\r
else {\r
- // If rs is NOT an array but data IS, see if data contains just 1 record. If so extract it and carry on.\r
+ // If rs is NOT an array but data IS, see if data contains just 1 record. If so extract it and carry on.\r
if (Ext.isArray(data) && data.length == 1) {\r
data = data.shift();\r
}\r
- if (!this.isData(data)) {\r
- // TODO: create custom Exception class to return record in thrown exception. Allow exception-handler the choice\r
- // to commit or not rather than blindly rs.commit() here.\r
- rs.commit();\r
- throw new Ext.data.DataReader.Error('update', rs);\r
+ if (this.isData(data)) {\r
+ rs.fields.each(function(f) {\r
+ if (data[f.name] !== f.defaultValue) {\r
+ rs.data[f.name] = data[f.name];\r
+ }\r
+ });\r
}\r
- this.buildExtractors();\r
- rs.data = this.extractValues(Ext.apply(rs.data, data), rs.fields.items, rs.fields.items.length);\r
rs.commit();\r
}\r
},\r
\r
+ /**\r
+ * returns extracted, type-cast rows of data. Iterates to call #extractValues for each row\r
+ * @param {Object[]/Object} data-root from server response\r
+ * @param {Boolean} returnRecords [false] Set true to return instances of Ext.data.Record\r
+ * @private\r
+ */\r
+ extractData : function(root, returnRecords) {\r
+ // A bit ugly this, too bad the Record's raw data couldn't be saved in a common property named "raw" or something.\r
+ var rawName = (this instanceof Ext.data.JsonReader) ? 'json' : 'node';\r
+\r
+ var rs = [];\r
+\r
+ // Had to add Check for XmlReader, #isData returns true if root is an Xml-object. Want to check in order to re-factor\r
+ // #extractData into DataReader base, since the implementations are almost identical for JsonReader, XmlReader\r
+ if (this.isData(root) && !(this instanceof Ext.data.XmlReader)) {\r
+ root = [root];\r
+ }\r
+ var f = this.recordType.prototype.fields,\r
+ fi = f.items,\r
+ fl = f.length,\r
+ rs = [];\r
+ if (returnRecords === true) {\r
+ var Record = this.recordType;\r
+ for (var i = 0; i < root.length; i++) {\r
+ var n = root[i];\r
+ var record = new Record(this.extractValues(n, fi, fl), this.getId(n));\r
+ record[rawName] = n; // <-- There's implementation of ugly bit, setting the raw record-data.\r
+ rs.push(record);\r
+ }\r
+ }\r
+ else {\r
+ for (var i = 0; i < root.length; i++) {\r
+ var data = this.extractValues(root[i], fi, fl);\r
+ data[this.meta.idProperty] = this.getId(root[i]);\r
+ rs.push(data);\r
+ }\r
+ }\r
+ return rs;\r
+ },\r
+\r
/**\r
* Returns true if the supplied data-hash <b>looks</b> and quacks like data. Checks to see if it has a key\r
* corresponding to idProperty defined in your DataReader config containing non-empty pk.\r
* @return {Boolean}\r
*/\r
isData : function(data) {\r
- return (data && Ext.isObject(data) && !Ext.isEmpty(data[this.meta.idProperty])) ? true : false;\r
+ return (data && Ext.isObject(data) && !Ext.isEmpty(this.getId(data))) ? true : false;\r
+ },\r
+\r
+ // private function a store will createSequence upon\r
+ onMetaChange : function(meta){\r
+ delete this.ef;\r
+ this.meta = meta;\r
+ this.recordType = Ext.data.Record.create(meta.fields);\r
+ this.buildExtractors();\r
}\r
};\r
\r
'invalid-response': "#readResponse received an invalid response from the server."\r
}\r
});\r
-\r
-\r
/**
* @class Ext.data.DataWriter
* <p>Ext.data.DataWriter facilitates create, update, and destroy actions between
* {@link Ext.data.JsonWriter}.</p>
* <p>Creating a writer is simple:</p>
* <pre><code>
-var writer = new Ext.data.JsonWriter();
+var writer = new Ext.data.JsonWriter({
+ encode: false // <--- false causes data to be printed to jsonData config-property of Ext.Ajax#reqeust
+});
+ * </code></pre>
+ * * <p>Same old JsonReader as Ext-2.x:</p>
+ * <pre><code>
+var reader = new Ext.data.JsonReader({idProperty: 'id'}, [{name: 'first'}, {name: 'last'}, {name: 'email'}]);
* </code></pre>
+ *
* <p>The proxy for a writer enabled store can be configured with a simple <code>url</code>:</p>
* <pre><code>
// Create a standard HttpProxy instance.
var proxy = new Ext.data.HttpProxy({
- url: 'app.php/users'
+ url: 'app.php/users' // <--- Supports "provides"-type urls, such as '/users.json', '/products.xml' (Hello Rails/Merb)
});
* </code></pre>
- * <p>For finer grained control, the proxy may also be configured with an <code>api</code>:</p>
+ * <p>For finer grained control, the proxy may also be configured with an <code>API</code>:</p>
* <pre><code>
-// Use the api specification
+// Maximum flexibility with the API-configuration
var proxy = new Ext.data.HttpProxy({
api: {
read : 'app.php/users/read',
create : 'app.php/users/create',
update : 'app.php/users/update',
- destroy : 'app.php/users/destroy'
+ destroy : { // <--- Supports object-syntax as well
+ url: 'app.php/users/destroy',
+ method: "DELETE"
+ }
}
});
* </code></pre>
- * <p>Creating a Writer enabled store:</p>
+ * <p>Pulling it all together into a Writer-enabled Store:</p>
* <pre><code>
var store = new Ext.data.Store({
proxy: proxy,
reader: reader,
- writer: writer
+ writer: writer,
+ autoLoad: true,
+ autoSave: true // -- Cell-level updates.
});
+ * </code></pre>
+ * <p>Initiating write-actions <b>automatically</b>, using the existing Ext2.0 Store/Record API:</p>
+ * <pre><code>
+var rec = store.getAt(0);
+rec.set('email', 'foo@bar.com'); // <--- Immediately initiates an UPDATE action through configured proxy.
+
+store.remove(rec); // <---- Immediately initiates a DESTROY action through configured proxy.
+ * </code></pre>
+ * <p>For <b>record/batch</b> updates, use the Store-configuration {@link Ext.data.Store#autoSave autoSave:false}</p>
+ * <pre><code>
+var store = new Ext.data.Store({
+ proxy: proxy,
+ reader: reader,
+ writer: writer,
+ autoLoad: true,
+ autoSave: false // -- disable cell-updates
+});
+
+var urec = store.getAt(0);
+urec.set('email', 'foo@bar.com');
+
+var drec = store.getAt(1);
+store.remove(drec);
+
+// Push the button!
+store.save();
* </code></pre>
* @constructor Create a new DataWriter
* @param {Object} meta Metadata configuration options (implementation-specific)
* using {@link Ext.data.Record#create}.
*/
Ext.data.DataWriter = function(config){
- /**
- * This DataWriter's configured metadata as passed to the constructor.
- * @type Mixed
- * @property meta
- */
Ext.apply(this, config);
};
-
Ext.data.DataWriter.prototype = {
/**
listful : false, // <-- listful is actually not used internally here in DataWriter. @see Ext.data.Store#execute.
/**
- * Writes data in preparation for server-write action. Simply proxies to DataWriter#update, DataWriter#create
- * DataWriter#destroy.
- * @param {String} action [CREATE|UPDATE|DESTROY]
- * @param {Object} params The params-hash to write-to
- * @param {Record/Record[]} rs The recordset write.
+ * Compiles a Store recordset into a data-format defined by an extension such as {@link Ext.data.JsonWriter} or {@link Ext.data.XmlWriter} in preparation for a {@link Ext.data.Api#actions server-write action}. The first two params are similar similar in nature to {@link Ext#apply},
+ * Where the first parameter is the <i>receiver</i> of paramaters and the second, baseParams, <i>the source</i>.
+ * @param {Object} params The request-params receiver.
+ * @param {Object} baseParams as defined by {@link Ext.data.Store#baseParams}. The baseParms must be encoded by the extending class, eg: {@link Ext.data.JsonWriter}, {@link Ext.data.XmlWriter}.
+ * @param {String} action [{@link Ext.data.Api#actions create|update|destroy}]
+ * @param {Record/Record[]} rs The recordset to write, the subject(s) of the write action.
*/
- write : function(action, params, rs) {
- this.render(action, rs, params, this[action](rs));
+ apply : function(params, baseParams, action, rs) {
+ var data = [],
+ renderer = action + 'Record';
+ // TODO implement @cfg listful here
+ if (Ext.isArray(rs)) {
+ Ext.each(rs, function(rec){
+ data.push(this[renderer](rec));
+ }, this);
+ }
+ else if (rs instanceof Ext.data.Record) {
+ data = this[renderer](rs);
+ }
+ this.render(params, baseParams, data);
},
/**
render : Ext.emptyFn,
/**
- * update
- * @param {Object} p Params-hash to apply result to.
- * @param {Record/Record[]} rs Record(s) to write
- * @private
- */
- update : function(rs) {
- var params = {};
- if (Ext.isArray(rs)) {
- var data = [],
- ids = [];
- Ext.each(rs, function(val){
- ids.push(val.id);
- data.push(this.updateRecord(val));
- }, this);
- params[this.meta.idProperty] = ids;
- params[this.meta.root] = data;
- }
- else if (rs instanceof Ext.data.Record) {
- params[this.meta.idProperty] = rs.id;
- params[this.meta.root] = this.updateRecord(rs);
- }
- return params;
- },
-
- /**
- * @cfg {Function} saveRecord Abstract method that should be implemented in all subclasses
- * (e.g.: {@link Ext.data.JsonWriter#saveRecord JsonWriter.saveRecord}
+ * @cfg {Function} updateRecord Abstract method that should be implemented in all subclasses
+ * (e.g.: {@link Ext.data.JsonWriter#updateRecord JsonWriter.updateRecord}
*/
updateRecord : Ext.emptyFn,
- /**
- * create
- * @param {Object} p Params-hash to apply result to.
- * @param {Record/Record[]} rs Record(s) to write
- * @private
- */
- create : function(rs) {
- var params = {};
- if (Ext.isArray(rs)) {
- var data = [];
- Ext.each(rs, function(val){
- data.push(this.createRecord(val));
- }, this);
- params[this.meta.root] = data;
- }
- else if (rs instanceof Ext.data.Record) {
- params[this.meta.root] = this.createRecord(rs);
- }
- return params;
- },
-
/**
* @cfg {Function} createRecord Abstract method that should be implemented in all subclasses
* (e.g.: {@link Ext.data.JsonWriter#createRecord JsonWriter.createRecord})
*/
createRecord : Ext.emptyFn,
- /**
- * destroy
- * @param {Object} p Params-hash to apply result to.
- * @param {Record/Record[]} rs Record(s) to write
- * @private
- */
- destroy : function(rs) {
- var params = {};
- if (Ext.isArray(rs)) {
- var data = [],
- ids = [];
- Ext.each(rs, function(val){
- data.push(this.destroyRecord(val));
- }, this);
- params[this.meta.root] = data;
- } else if (rs instanceof Ext.data.Record) {
- params[this.meta.root] = this.destroyRecord(rs);
- }
- return params;
- },
-
/**
* @cfg {Function} destroyRecord Abstract method that should be implemented in all subclasses
* (e.g.: {@link Ext.data.JsonWriter#destroyRecord JsonWriter.destroyRecord})
destroyRecord : Ext.emptyFn,
/**
- * Converts a Record to a hash
- * @param {Record}
- * @private
+ * Converts a Record to a hash, taking into account the state of the Ext.data.Record along with configuration properties
+ * related to its rendering, such as {@link #writeAllFields}, {@link Ext.data.Record#phantom phantom}, {@link Ext.data.Record#getChanges getChanges} and
+ * {@link Ext.data.DataReader#idProperty idProperty}
+ * @param {Ext.data.Record}
+ * @param {Object} config <b>NOT YET IMPLEMENTED</b>. Will implement an exlude/only configuration for fine-control over which fields do/don't get rendered.
+ * @return {Object}
+ * @protected
+ * TODO Implement excludes/only configuration with 2nd param?
*/
- toHash : function(rec) {
+ toHash : function(rec, config) {
var map = rec.fields.map,
data = {},
raw = (this.writeAllFields === false && rec.phantom === false) ? rec.getChanges() : rec.data,
data[m.mapping ? m.mapping : m.name] = value;
}
});
- data[this.meta.idProperty] = rec.id;
+ // we don't want to write Ext auto-generated id to hash. Careful not to remove it on Models not having auto-increment pk though.
+ // We can tell its not auto-increment if the user defined a DataReader field for it *and* that field's value is non-empty.
+ // we could also do a RegExp here for the Ext.data.Record AUTO_ID prefix.
+ if (rec.phantom) {
+ if (rec.fields.containsKey(this.meta.idProperty) && Ext.isEmpty(rec.data[this.meta.idProperty])) {
+ delete data[this.meta.idProperty];
+ }
+ } else {
+ data[this.meta.idProperty] = rec.id
+ }
return data;
+ },
+
+ /**
+ * Converts a {@link Ext.data.DataWriter#toHash Hashed} {@link Ext.data.Record} to fields-array array suitable
+ * for encoding to xml via XTemplate, eg:
+<code><pre><tpl for="."><{name}>{value}</{name}</tpl></pre></code>
+ * eg, <b>non-phantom</b>:
+<code><pre>{id: 1, first: 'foo', last: 'bar'} --> [{name: 'id', value: 1}, {name: 'first', value: 'foo'}, {name: 'last', value: 'bar'}]</pre></code>
+ * {@link Ext.data.Record#phantom Phantom} records will have had their idProperty omitted in {@link #toHash} if determined to be auto-generated.
+ * Non AUTOINCREMENT pks should have been protected.
+ * @param {Hash} data Hashed by Ext.data.DataWriter#toHash
+ * @return {[Object]} Array of attribute-objects.
+ * @protected
+ */
+ toArray : function(data) {
+ var fields = [];
+ Ext.iterate(data, function(k, v) {fields.push({name: k, value: v});},this);
+ return fields;
}
};/**\r
* @class Ext.data.DataProxy\r
}\r
}),\r
* </code></pre>\r
+ * <p>And <b>new in Ext version 3</b>, attach centralized event-listeners upon the DataProxy class itself! This is a great place\r
+ * to implement a <i>messaging system</i> to centralize your application's user-feedback and error-handling.</p>\r
+ * <pre><code>\r
+// Listen to all "beforewrite" event fired by all proxies.\r
+Ext.data.DataProxy.on('beforewrite', function(proxy, action) {\r
+ console.log('beforewrite: ', action);\r
+});\r
+\r
+// Listen to "write" event fired by all proxies\r
+Ext.data.DataProxy.on('write', function(proxy, action, data, res, rs) {\r
+ console.info('write: ', action);\r
+});\r
+\r
+// Listen to "exception" event fired by all proxies\r
+Ext.data.DataProxy.on('exception', function(proxy, type, action) {\r
+ console.error(type + action + ' exception);\r
+});\r
+ * </code></pre>\r
+ * <b>Note:</b> These three events are all fired with the signature of the corresponding <i>DataProxy instance</i> event {@link #beforewrite beforewrite}, {@link #write write} and {@link #exception exception}.\r
*/\r
Ext.data.DataProxy = function(conn){\r
// make sure we have a config object here to support ux proxies.\r
update : undefined,\r
destroy : undefined\r
}\r
-</code></pre>\r
+ * </code></pre>\r
+ * <p>The url is built based upon the action being executed <tt>[load|create|save|destroy]</tt>\r
+ * using the commensurate <tt>{@link #api}</tt> property, or if undefined default to the\r
+ * configured {@link Ext.data.Store}.{@link Ext.data.Store#url url}.</p><br>\r
+ * <p>For example:</p>\r
+ * <pre><code>\r
+api: {\r
+ load : '/controller/load',\r
+ create : '/controller/new', // Server MUST return idProperty of new record\r
+ save : '/controller/update',\r
+ destroy : '/controller/destroy_action'\r
+}\r
+\r
+// Alternatively, one can use the object-form to specify each API-action\r
+api: {\r
+ load: {url: 'read.php', method: 'GET'},\r
+ create: 'create.php',\r
+ destroy: 'destroy.php',\r
+ save: 'update.php'\r
+}\r
+ * </code></pre>\r
* <p>If the specific URL for a given CRUD action is undefined, the CRUD action request\r
* will be directed to the configured <tt>{@link Ext.data.Connection#url url}</tt>.</p>\r
* <br><p><b>Note</b>: To modify the URL for an action dynamically the appropriate API\r
// permanent, applying this URL for all subsequent requests.\r
store.proxy.setUrl('changed1.php', true);\r
\r
- // manually set the <b>private</b> connection URL.\r
- // <b>Warning:</b> Accessing the private URL property should be avoided.\r
- // Use the public method <tt>{@link Ext.data.HttpProxy#setUrl setUrl}</tt> instead, shown above.\r
- // It should be noted that changing the URL like this will affect\r
- // the URL for just this request. Subsequent requests will use the\r
- // API or URL defined in your initial proxy configuration.\r
- store.proxy.conn.url = 'changed1.php';\r
-\r
- // proxy URL will be superseded by API (only if proxy created to use ajax):\r
- // It should be noted that proxy API changes are permanent and will\r
- // be used for all subsequent requests.\r
- store.proxy.api.load = 'changed2.php';\r
-\r
- // However, altering the proxy API should be done using the public\r
- // method <tt>{@link Ext.data.DataProxy#setApi setApi}</tt> instead.\r
- store.proxy.setApi('load', 'changed2.php');\r
+ // Altering the proxy API should be done using the public\r
+ // method <tt>{@link Ext.data.DataProxy#setApi setApi}</tt>.\r
+ store.proxy.setApi('read', 'changed2.php');\r
\r
// Or set the entire API with a config-object.\r
// When using the config-object option, you must redefine the <b>entire</b>\r
* </code></pre>\r
* </p>\r
*/\r
- // Prepare the proxy api. Ensures all API-actions are defined with the Object-form.\r
- try {\r
- Ext.data.Api.prepare(this);\r
- } catch (e) {\r
- if (e instanceof Ext.data.Api.Error) {\r
- e.toConsole();\r
- }\r
- }\r
\r
this.addEvents(\r
/**\r
* @event exception\r
- * <p>Fires if an exception occurs in the Proxy during a remote request.\r
- * This event is relayed through a corresponding\r
- * {@link Ext.data.Store}.{@link Ext.data.Store#exception exception},\r
- * so any Store instance may observe this event.\r
- * This event can be fired for one of two reasons:</p>\r
+ * <p>Fires if an exception occurs in the Proxy during a remote request. This event is relayed\r
+ * through a corresponding {@link Ext.data.Store}.{@link Ext.data.Store#exception exception},\r
+ * so any Store instance may observe this event.</p>\r
+ * <p>In addition to being fired through the DataProxy instance that raised the event, this event is also fired\r
+ * through the Ext.data.DataProxy <i>class</i> to allow for centralized processing of exception events from <b>all</b>\r
+ * DataProxies by attaching a listener to the Ext.data.Proxy class itself.</p>\r
+ * <p>This event can be fired for one of two reasons:</p>\r
* <div class="mdetail-params"><ul>\r
* <li>remote-request <b>failed</b> : <div class="sub-desc">\r
* The server did not return status === 200.\r
'loadexception',\r
/**\r
* @event beforewrite\r
- * Fires before a request is generated for one of the actions Ext.data.Api.actions.create|update|destroy\r
+ * <p>Fires before a request is generated for one of the actions Ext.data.Api.actions.create|update|destroy</p>\r
+ * <p>In addition to being fired through the DataProxy instance that raised the event, this event is also fired\r
+ * through the Ext.data.DataProxy <i>class</i> to allow for centralized processing of beforewrite events from <b>all</b>\r
+ * DataProxies by attaching a listener to the Ext.data.Proxy class itself.</p>\r
* @param {DataProxy} this The proxy for the request\r
* @param {String} action [Ext.data.Api.actions.create|update|destroy]\r
* @param {Record/Array[Record]} rs The Record(s) to create|update|destroy.\r
'beforewrite',\r
/**\r
* @event write\r
- * Fires before the request-callback is called\r
+ * <p>Fires before the request-callback is called</p>\r
+ * <p>In addition to being fired through the DataProxy instance that raised the event, this event is also fired\r
+ * through the Ext.data.DataProxy <i>class</i> to allow for centralized processing of write events from <b>all</b>\r
+ * DataProxies by attaching a listener to the Ext.data.Proxy class itself.</p>\r
* @param {DataProxy} this The proxy that sent the request\r
* @param {String} action [Ext.data.Api.actions.create|upate|destroy]\r
* @param {Object} data The data object extracted from the server-response\r
'write'\r
);\r
Ext.data.DataProxy.superclass.constructor.call(this);\r
+\r
+ // Prepare the proxy api. Ensures all API-actions are defined with the Object-form.\r
+ try {\r
+ Ext.data.Api.prepare(this);\r
+ } catch (e) {\r
+ if (e instanceof Ext.data.Api.Error) {\r
+ e.toConsole();\r
+ }\r
+ }\r
+ // relay each proxy's events onto Ext.data.DataProxy class for centralized Proxy-listening\r
+ Ext.data.DataProxy.relayEvents(this, ['beforewrite', 'write', 'exception']);\r
};\r
\r
Ext.extend(Ext.data.DataProxy, Ext.util.Observable, {\r
...\r
)}\r
* </code></pre>\r
- * There is no <code>{@link #api}</code> specified in the configuration of the proxy,\r
+ * If there is no <code>{@link #api}</code> specified in the configuration of the proxy,\r
* all requests will be marshalled to a single RESTful url (/users) so the serverside\r
* framework can inspect the HTTP Method and act accordingly:\r
* <pre>\r
PUT /users/23 update\r
DESTROY /users/23 delete\r
* </pre></p>\r
+ * <p>If set to <tt>true</tt>, a {@link Ext.data.Record#phantom non-phantom} record's\r
+ * {@link Ext.data.Record#id id} will be appended to the url. Some MVC (e.g., Ruby on Rails,\r
+ * Merb and Django) support segment based urls where the segments in the URL follow the\r
+ * Model-View-Controller approach:<pre><code>\r
+ * someSite.com/controller/action/id\r
+ * </code></pre>\r
+ * Where the segments in the url are typically:<div class="mdetail-params"><ul>\r
+ * <li>The first segment : represents the controller class that should be invoked.</li>\r
+ * <li>The second segment : represents the class function, or method, that should be called.</li>\r
+ * <li>The third segment : represents the ID (a variable typically passed to the method).</li>\r
+ * </ul></div></p>\r
+ * <br><p>Refer to <code>{@link Ext.data.DataProxy#api}</code> for additional information.</p>\r
*/\r
restful: false,\r
\r
* @param {Object} params\r
* @param {Ext.data.DataReader} reader\r
* @param {Function} callback\r
- * @param {Object} scope Scope with which to call the callback (defaults to the Proxy object)\r
+ * @param {Object} scope The scope (<code>this</code> reference) in which the callback function is executed. Defaults to the Proxy object.\r
* @param {Object} options Any options specified for the action (e.g. see {@link Ext.data.Store#load}.\r
*/\r
request : function(action, rs, params, reader, callback, scope, options) {\r
load : null,\r
\r
/**\r
- * @cfg {Function} doRequest Abstract method that should be implemented in all subclasses\r
+ * @cfg {Function} doRequest Abstract method that should be implemented in all subclasses. <b>Note:</b> Should only be used by custom-proxy developers.\r
* (e.g.: {@link Ext.data.HttpProxy#doRequest HttpProxy.doRequest},\r
* {@link Ext.data.DirectProxy#doRequest DirectProxy.doRequest}).\r
*/\r
this.load(params, reader, callback, scope, options);\r
},\r
\r
+ /**\r
+ * @cfg {Function} onRead Abstract method that should be implemented in all subclasses. <b>Note:</b> Should only be used by custom-proxy developers. Callback for read {@link Ext.data.Api#actions action}.\r
+ * @param {String} action Action name as per {@link Ext.data.Api.actions#read}.\r
+ * @param {Object} o The request transaction object\r
+ * @param {Object} res The server response\r
+ * @fires loadexception (deprecated)\r
+ * @fires exception\r
+ * @fires load\r
+ * @protected\r
+ */\r
+ onRead : Ext.emptyFn,\r
+ /**\r
+ * @cfg {Function} onWrite Abstract method that should be implemented in all subclasses. <b>Note:</b> Should only be used by custom-proxy developers. Callback for <i>create, update and destroy</i> {@link Ext.data.Api#actions actions}.\r
+ * @param {String} action [Ext.data.Api.actions.create|read|update|destroy]\r
+ * @param {Object} trans The request transaction object\r
+ * @param {Object} res The server response\r
+ * @fires exception\r
+ * @fires write\r
+ * @protected\r
+ */\r
+ onWrite : Ext.emptyFn,\r
/**\r
* buildUrl\r
* Sets the appropriate url based upon the action being executed. If restful is true, and only a single record is being acted upon,\r
*/\r
buildUrl : function(action, record) {\r
record = record || null;\r
- var url = (this.api[action]) ? this.api[action].url : this.url;\r
+\r
+ // conn.url gets nullified after each request. If it's NOT null here, that means the user must have intervened with a call\r
+ // to DataProxy#setUrl or DataProxy#setApi and changed it before the request was executed. If that's the case, use conn.url,\r
+ // otherwise, build the url from the api or this.url.\r
+ var url = (this.conn && this.conn.url) ? this.conn.url : (this.api[action]) ? this.api[action].url : this.url;\r
if (!url) {\r
throw new Ext.data.Api.Error('invalid-url', action);\r
}\r
\r
- var format = null;\r
- var m = url.match(/(.*)(\.\w+)$/); // <-- look for urls with "provides" suffix, e.g.: /users.json, /users.xml, etc\r
+ // look for urls having "provides" suffix used in some MVC frameworks like Rails/Merb and others. The provides suffice informs\r
+ // the server what data-format the client is dealing with and returns data in the same format (eg: application/json, application/xml, etc)\r
+ // e.g.: /users.json, /users.xml, etc.\r
+ // with restful routes, we need urls like:\r
+ // PUT /users/1.json\r
+ // DELETE /users/1.json\r
+ var provides = null;\r
+ var m = url.match(/(.*)(\.json|\.xml|\.html)$/);\r
if (m) {\r
- format = m[2];\r
- url = m[1];\r
+ provides = m[2]; // eg ".json"\r
+ url = m[1]; // eg: "/users"\r
}\r
// prettyUrls is deprectated in favor of restful-config\r
- if ((this.prettyUrls === true || this.restful === true) && record instanceof Ext.data.Record && !record.phantom) {\r
+ if ((this.restful === true || this.prettyUrls === true) && record instanceof Ext.data.Record && !record.phantom) {\r
url += '/' + record.id;\r
}\r
- if (format) { // <-- append the request format if exists (ie: /users/update/69[.json])\r
- url += format;\r
- }\r
- return url;\r
+ return (provides === null) ? url : url + provides;\r
},\r
\r
/**\r
}\r
});\r
\r
+// Apply the Observable prototype to the DataProxy class so that proxy instances can relay their\r
+// events to the class. Allows for centralized listening of all proxy instances upon the DataProxy class.\r
+Ext.apply(Ext.data.DataProxy, Ext.util.Observable.prototype);\r
+Ext.util.Observable.call(Ext.data.DataProxy);\r
+\r
/**\r
* @class Ext.data.DataProxy.Error\r
* @extends Ext.Error\r
'api-invalid': 'Recieved an invalid API-configuration. Please ensure your proxy API-configuration contains only the actions from Ext.data.Api.actions.'\r
}\r
});\r
+\r
+\r
+/**
+ * @class Ext.data.Request
+ * A simple Request class used internally to the data package to provide more generalized remote-requests
+ * to a DataProxy.
+ * TODO Not yet implemented. Implement in Ext.data.Store#execute
+ */
+Ext.data.Request = function(params) {
+ Ext.apply(this, params);
+};
+Ext.data.Request.prototype = {
+ /**
+ * @cfg {String} action
+ */
+ action : undefined,
+ /**
+ * @cfg {Ext.data.Record[]/Ext.data.Record} rs The Store recordset associated with the request.
+ */
+ rs : undefined,
+ /**
+ * @cfg {Object} params HTTP request params
+ */
+ params: undefined,
+ /**
+ * @cfg {Function} callback The function to call when request is complete
+ */
+ callback : Ext.emptyFn,
+ /**
+ * @cfg {Object} scope The scope of the callback funtion
+ */
+ scope : undefined,
+ /**
+ * @cfg {Ext.data.DataReader} reader The DataReader instance which will parse the received response
+ */
+ reader : undefined
+};
+/**
+ * @class Ext.data.Response
+ * A generic response class to normalize response-handling internally to the framework.
+ */
+Ext.data.Response = function(params) {
+ Ext.apply(this, params);
+};
+Ext.data.Response.prototype = {
+ /**
+ * @cfg {String} action {@link Ext.data.Api#actions}
+ */
+ action: undefined,
+ /**
+ * @cfg {Boolean} success
+ */
+ success : undefined,
+ /**
+ * @cfg {String} message
+ */
+ message : undefined,
+ /**
+ * @cfg {Array/Object} data
+ */
+ data: undefined,
+ /**
+ * @cfg {Object} raw The raw response returned from server-code
+ */
+ raw: undefined,
+ /**
+ * @cfg {Ext.data.Record/Ext.data.Record[]} records related to the Request action
+ */
+ records: undefined
+};
/**\r
* @class Ext.data.ScriptTagProxy\r
* @extends Ext.data.DataProxy\r
if (scriptTag) {\r
out.write(");");\r
}\r
+</code></pre>\r
+ * <p>Below is a PHP example to do the same thing:</p><pre><code>\r
+$callback = $_REQUEST['callback'];\r
+\r
+// Create the output object.\r
+$output = array('a' => 'Apple', 'b' => 'Banana');\r
+\r
+//start output\r
+if ($callback) {\r
+ header('Content-Type: text/javascript');\r
+ echo $callback . '(' . json_encode($output) . ');';\r
+} else {\r
+ header('Content-Type: application/x-json');\r
+ echo json_encode($output);\r
+}\r
+</code></pre>\r
+ * <p>Below is the ASP.Net code to do the same thing:</p><pre><code>\r
+String jsonString = "{success: true}";\r
+String cb = Request.Params.Get("callback");\r
+String responseString = "";\r
+if (!String.IsNullOrEmpty(cb)) {\r
+ responseString = cb + "(" + jsonString + ")";\r
+} else {\r
+ responseString = jsonString;\r
+}\r
+Response.Write(responseString);\r
</code></pre>\r
*\r
* @constructor\r
* <li>The "arg" argument from the load function</li>\r
* <li>A boolean success indicator</li>\r
* </ul>\r
- * @param {Object} scope The scope in which to call the callback\r
+ * @param {Object} scope The scope (<code>this</code> reference) in which the callback function is executed. Defaults to the browser window.\r
* @param {Object} arg An optional argument which is passed to the callback as its second parameter.\r
*/\r
doRequest : function(action, rs, params, reader, callback, scope, arg) {\r
* @param {String} action [Ext.data.Api.actions.create|read|update|destroy]\r
* @param {Object} trans The request transaction object\r
* @param {Object} res The server response\r
- * @private\r
+ * @protected\r
*/\r
onRead : function(action, trans, res) {\r
var result;\r
* @param {String} action [Ext.data.Api.actions.create|read|update|destroy]\r
* @param {Object} trans The request transaction object\r
* @param {Object} res The server response\r
- * @private\r
+ * @protected\r
*/\r
- onWrite : function(action, trans, res, rs) {\r
+ onWrite : function(action, trans, response, rs) {\r
var reader = trans.reader;\r
try {\r
// though we already have a response object here in STP, run through readResponse to catch any meta-data exceptions.\r
- reader.readResponse(action, res);\r
+ var res = reader.readResponse(action, response);\r
} catch (e) {\r
this.fireEvent('exception', this, 'response', action, trans, res, e);\r
trans.callback.call(trans.scope||window, null, res, false);\r
return;\r
}\r
- if(!res[reader.meta.successProperty] === true){\r
+ if(!res.success === true){\r
this.fireEvent('exception', this, 'remote', action, trans, res, rs);\r
trans.callback.call(trans.scope||window, null, res, false);\r
return;\r
}\r
- this.fireEvent("write", this, action, res[reader.meta.root], res, rs, trans.arg );\r
- trans.callback.call(trans.scope||window, res[reader.meta.root], res, true);\r
+ this.fireEvent("write", this, action, res.data, res, rs, trans.arg );\r
+ trans.callback.call(trans.scope||window, res.data, res, true);\r
},\r
\r
// private\r
* @constructor\r
* @param {Object} conn\r
* An {@link Ext.data.Connection} object, or options parameter to {@link Ext.Ajax#request}.\r
- * <p>Note that if this HttpProxy is being used by a (@link Ext.data.Store Store}, then the\r
+ * <p>Note that if this HttpProxy is being used by a {@link Ext.data.Store Store}, then the\r
* Store's call to {@link #load} will override any specified <tt>callback</tt> and <tt>params</tt>\r
* options. In this case, use the Store's {@link Ext.data.Store#events events} to modify parameters,\r
* or react to loading events. The Store's {@link Ext.data.Store#baseParams baseParams} may also be\r
\r
// nullify the connection url. The url param has been copied to 'this' above. The connection\r
// url will be set during each execution of doRequest when buildUrl is called. This makes it easier for users to override the\r
- // connection url during beforeaction events (ie: beforeload, beforesave, etc). The connection url will be nullified\r
- // after each request as well. Url is always re-defined during doRequest.\r
+ // connection url during beforeaction events (ie: beforeload, beforewrite, etc).\r
+ // Url is always re-defined during doRequest.\r
this.conn.url = null;\r
\r
this.useAjax = !conn || !conn.events;\r
\r
- //private. A hash containing active requests, keyed on action [Ext.data.Api.actions.create|read|update|destroy]\r
+ // A hash containing active requests, keyed on action [Ext.data.Api.actions.create|read|update|destroy]\r
var actions = Ext.data.Api.actions;\r
this.activeRequest = {};\r
for (var verb in actions) {\r
};\r
\r
Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, {\r
- /**\r
- * @cfg {Boolean} restful\r
- * <p>If set to <tt>true</tt>, a {@link Ext.data.Record#phantom non-phantom} record's\r
- * {@link Ext.data.Record#id id} will be appended to the url (defaults to <tt>false</tt>).</p><br>\r
- * <p>The url is built based upon the action being executed <tt>[load|create|save|destroy]</tt>\r
- * using the commensurate <tt>{@link #api}</tt> property, or if undefined default to the\r
- * configured {@link Ext.data.Store}.{@link Ext.data.Store#url url}.</p><br>\r
- * <p>Some MVC (e.g., Ruby on Rails, Merb and Django) support this style of segment based urls\r
- * where the segments in the URL follow the Model-View-Controller approach.</p><pre><code>\r
- * someSite.com/controller/action/id\r
- * </code></pre>\r
- * Where the segments in the url are typically:<div class="mdetail-params"><ul>\r
- * <li>The first segment : represents the controller class that should be invoked.</li>\r
- * <li>The second segment : represents the class function, or method, that should be called.</li>\r
- * <li>The third segment : represents the ID (a variable typically passed to the method).</li>\r
- * </ul></div></p>\r
- * <p>For example:</p>\r
- * <pre><code>\r
-api: {\r
- load : '/controller/load',\r
- create : '/controller/new', // Server MUST return idProperty of new record\r
- save : '/controller/update',\r
- destroy : '/controller/destroy_action'\r
-}\r
-\r
-// Alternatively, one can use the object-form to specify each API-action\r
-api: {\r
- load: {url: 'read.php', method: 'GET'},\r
- create: 'create.php',\r
- destroy: 'destroy.php',\r
- save: 'update.php'\r
-}\r
- */\r
-\r
/**\r
* Return the {@link Ext.data.Connection} object being used by this Proxy.\r
* @return {Connection} The Connection object. This object may be used to subscribe to events on\r
this.conn.url = url;\r
if (makePermanent === true) {\r
this.url = url;\r
+ this.api = null;\r
Ext.data.Api.prepare(this);\r
}\r
},\r
* <li><tt>r</tt> : Ext.data.Record[] The block of Ext.data.Records.</li>\r
* <li><tt>options</tt>: Options object from the action request</li>\r
* <li><tt>success</tt>: Boolean success indicator</li></ul></p></div>\r
- * @param {Object} scope The scope in which to call the callback\r
+ * @param {Object} scope The scope (<code>this</code> reference) in which the callback function is executed. Defaults to the browser window.\r
* @param {Object} arg An optional argument which is passed to the callback as its second parameter.\r
+ * @protected\r
*/\r
doRequest : function(action, rs, params, reader, cb, scope, arg) {\r
var o = {\r
callback : this.createCallback(action, rs),\r
scope: this\r
};\r
- // Sample the request data: If it's an object, then it hasn't been json-encoded yet.\r
- // Transmit data using jsonData config of Ext.Ajax.request\r
- if (typeof(params[reader.meta.root]) === 'object') {\r
- o.jsonData = params;\r
+\r
+ // If possible, transmit data using jsonData || xmlData on Ext.Ajax.request (An installed DataWriter would have written it there.).\r
+ // Use std HTTP params otherwise.\r
+ if (params.jsonData) {\r
+ o.jsonData = params.jsonData;\r
+ } else if (params.xmlData) {\r
+ o.xmlData = params.xmlData;\r
} else {\r
o.params = params || {};\r
}\r
// Set the connection url. If this.conn.url is not null here,\r
- // the user may have overridden the url during a beforeaction event-handler.\r
+ // the user must have overridden the url during a beforewrite/beforeload event-handler.\r
// this.conn.url is nullified after each request.\r
- if (this.conn.url === null) {\r
- this.conn.url = this.buildUrl(action, rs);\r
- }\r
- else if (this.restful === true && rs instanceof Ext.data.Record && !rs.phantom) {\r
- this.conn.url += '/' + rs.id;\r
- }\r
+ this.conn.url = this.buildUrl(action, rs);\r
+\r
if(this.useAjax){\r
\r
Ext.applyIf(o, this.conn);\r
\r
// If a currently running request is found for this action, abort it.\r
if (this.activeRequest[action]) {\r
+ ////\r
// Disabled aborting activeRequest while implementing REST. activeRequest[action] will have to become an array\r
+ // TODO ideas anyone?\r
+ //\r
//Ext.Ajax.abort(this.activeRequest[action]);\r
}\r
this.activeRequest[action] = Ext.Ajax.request(o);\r
if (!success) {\r
if (action === Ext.data.Api.actions.read) {\r
// @deprecated: fire loadexception for backwards compat.\r
+ // TODO remove in 3.1\r
this.fireEvent('loadexception', this, o, response);\r
}\r
this.fireEvent('exception', this, 'response', action, o, response);\r
} else {\r
this.onWrite(action, o, response, rs);\r
}\r
- }\r
+ };\r
},\r
\r
/**\r
* @param {String} action Action name as per {@link Ext.data.Api.actions#read}.\r
* @param {Object} o The request transaction object\r
* @param {Object} res The server response\r
- * @private\r
+ * @fires loadexception (deprecated)\r
+ * @fires exception\r
+ * @fires load\r
+ * @protected\r
*/\r
onRead : function(action, o, response) {\r
var result;\r
result = o.reader.read(response);\r
}catch(e){\r
// @deprecated: fire old loadexception for backwards-compat.\r
+ // TODO remove in 3.1\r
this.fireEvent('loadexception', this, o, response, e);\r
+\r
this.fireEvent('exception', this, 'response', action, o, response, e);\r
o.request.callback.call(o.request.scope, null, o.request.arg, false);\r
return;\r
}\r
if (result.success === false) {\r
// @deprecated: fire old loadexception for backwards-compat.\r
+ // TODO remove in 3.1\r
this.fireEvent('loadexception', this, o, response);\r
\r
// Get DataReader read-back a response-object to pass along to exception event\r
else {\r
this.fireEvent('load', this, o, o.request.arg);\r
}\r
+ // TODO refactor onRead, onWrite to be more generalized now that we're dealing with Ext.data.Response instance\r
+ // the calls to request.callback(...) in each will have to be made identical.\r
+ // NOTE reader.readResponse does not currently return Ext.data.Response\r
o.request.callback.call(o.request.scope, result, o.request.arg, result.success);\r
},\r
/**\r
* @param {String} action [Ext.data.Api.actions.create|read|update|destroy]\r
* @param {Object} trans The request transaction object\r
* @param {Object} res The server response\r
- * @private\r
+ * @fires exception\r
+ * @fires write\r
+ * @protected\r
*/\r
onWrite : function(action, o, response, rs) {\r
var reader = o.reader;\r
o.request.callback.call(o.request.scope, null, o.request.arg, false);\r
return;\r
}\r
- if (res[reader.meta.successProperty] === false) {\r
- this.fireEvent('exception', this, 'remote', action, o, res, rs);\r
+ if (res.success === true) {\r
+ this.fireEvent('write', this, action, res.data, res, rs, o.request.arg);\r
} else {\r
- this.fireEvent('write', this, action, res[reader.meta.root], res, rs, o.request.arg);\r
+ this.fireEvent('exception', this, 'remote', action, o, res, rs);\r
}\r
- o.request.callback.call(o.request.scope, res[reader.meta.root], res, res[reader.meta.successProperty]);\r
+ // TODO refactor onRead, onWrite to be more generalized now that we're dealing with Ext.data.Response instance\r
+ // the calls to request.callback(...) in each will have to be made similar.\r
+ // NOTE reader.readResponse does not currently return Ext.data.Response\r
+ o.request.callback.call(o.request.scope, res.data, res, res.success);\r
},\r
\r
// inherit docs\r
* <li>The "arg" argument from the load function</li>\r
* <li>A boolean success indicator</li>\r
* </ul>\r
- * @param {Object} scope The scope in which to call the callback\r
+ * @param {Object} scope The scope (<code>this</code> reference) in which the callback function is executed. Defaults to the browser window.\r
* @param {Object} arg An optional argument which is passed to the callback as its second parameter.\r
*/\r
doRequest : function(action, rs, params, reader, callback, scope, arg) {\r