View Full Version : [OPEN-58] ArrayReader should be refactored to almost nothing.
Animal
26 May 2009, 12:38 AM
The only logical difference between ArrayReader and JsonReader is that for an ArrayReader, the default mapping for a Field is the Field's index, and for a JsonReader, the default mapping is the Field's name. (and the idProperty/idIndex configs)
ArrayReader.js now only contains a readRecords method which is almost identical to that in JsonReader except that it does not use extraction functions
Really the two classes should be merged to save code.
so
Ext.data.ArrayReader = Ext.extend(Ext.data.JsonReader, {
useArray: true
});
and the buildExtractors function just tests the useArray flag to see whether it should use numeric subscripts or string subscripts to access the data, or use the idIndex/idPoperty config.
Animal
26 May 2009, 1:56 AM
I now have JsonReader as
/**
* @class Ext.data.JsonReader
* @extends Ext.data.DataReader
* Data reader class to create an Array of {@link Ext.data.Record} objects from a JSON response
* based on mappings in a provided {@link Ext.data.Record} constructor.<br>
* <p>
* Example code:
* <pre><code>
var Employee = Ext.data.Record.create([
{name: 'firstname'}, // map the Record's "firstname" field to the row object's key of the same name
{name: 'job', mapping: 'occupation'} // map the Record's "job" field to the row object's "occupation" key
]);
var myReader = new Ext.data.JsonReader(
{ // The metadata property, with configuration options:
totalProperty: "results", // the property which contains the total dataset size (optional)
root: "rows", // the property which contains an Array of row objects
idProperty: "id" // the property within each row object that provides an ID for the record (optional)
},
Employee // {@link Ext.data.Record} constructor that provides mapping for JSON object
);
</code></pre>
* <p>This would consume a JSON data object of the form:</p><pre><code>
{
results: 2, // Reader's configured totalProperty
rows: [ // Reader's configured root
{ id: 1, firstname: 'Bill', occupation: 'Gardener' }, // a row object
{ id: 2, firstname: 'Ben' , occupation: 'Horticulturalist' } // another row object
]
}
</code></pre>
* <p><b><u>Automatic configuration using metaData</u></b>
* <p>It is possible to change a JsonReader's metadata at any time by including a <b><tt>metaData</tt></b>
* property in the JSON data object. If the JSON data object has a <b><tt>metaData</tt></b> property, a
* {@link Ext.data.Store Store} object using this Reader will reconfigure itself to use the newly provided
* field definition and fire its {@link Ext.data.Store#metachange metachange} event. The metachange event
* handler may interrogate the <b><tt>metaData</tt></b> property to perform any configuration required.
* Note that reconfiguring a Store potentially invalidates objects which may refer to Fields or Records
* which no longer exist.</p>
* <p>The <b><tt>metaData</tt></b> property in the JSON data object may contain:</p>
* <div class="mdetail-params"><ul>
* <li>any of the configuration options for this class</li>
* <li>a <b><tt>{@link Ext.data.Record#fields fields}</tt></b> property which the JsonReader will
* use as an argument to the {@link Ext.data.Record#create data Record create method} in order to
* configure the layout of the Records it will produce.</li>
* <li>a <b><tt>{@link Ext.data.Store#sortInfo sortInfo}</tt></b> property which the JsonReader will
* use to set the {@link Ext.data.Store}'s {@link Ext.data.Store#sortInfo sortInfo} property</li>
* <li>any user-defined properties needed</li>
* </ul></div>
* <p>To use this facility to send the same data as the example above (without having to code the creation
* of the Record constructor), you would create the JsonReader like this:</p><pre><code>
var myReader = new Ext.data.JsonReader();
</code></pre>
* <p>The first data packet from the server would configure the reader by containing a
* <b><tt>metaData</tt></b> property <b>and</b> the data. For example, the JSON data object might take
* the form:</p>
<pre><code>
{
metaData: {
idProperty: 'id',
root: 'rows',
totalProperty: 'results',
fields: [
{name: 'name'},
{name: 'job', mapping: 'occupation'}
],
sortInfo: {field: 'name', direction:'ASC'}, // used by store to set its sortInfo
foo: 'bar' // custom property
},
results: 2,
rows: [
{ 'id': 1, 'name': 'Bill', occupation: 'Gardener' },
{ 'id': 2, 'name': 'Ben', occupation: 'Horticulturalist' }
]
}
</code></pre>
* @cfg {String} totalProperty Name of the property from which to retrieve the total number of records
* in the dataset. This is only needed if the whole dataset is not passed in one go, but is being
* paged from the remote server.
* @cfg {String} successProperty Name of the property from which to retrieve the success attribute used by forms.
* @cfg {String} root name of the property which contains the Array of row objects.
* @cfg {String} idProperty Name of the property within a row object that contains a record identifier value.
* @constructor
* Create a new JsonReader
* @param {Object} meta Metadata configuration options.
* @param {Object} recordType Either an Array of field definition objects as passed to
* {@link Ext.data.Record#create}, or a {@link Ext.data.Record Record} constructor created using {@link Ext.data.Record#create}.
*/
Ext.data.JsonReader = function(meta, recordType){
meta = meta || {};
Ext.data.JsonReader.superclass.constructor.call(this, meta, recordType || meta.fields);
};
Ext.extend(Ext.data.JsonReader, Ext.data.DataReader, {
dataPropertyName: 'jsonData',
idPropertyName: 'idProperty',
/**
* This JsonReader's metadata as passed to the constructor, or as passed in
* the last data packet's <b><tt>metaData</tt></b> property.
* @type Mixed
* @property meta
*/
/**
* After any data loads, the raw JSON data is available for further custom processing. If no data is
* loaded or there is a load exception this property will be undefined.
* @type Object
* @property jsonData
*/
/**
* This method is only used by a DataProxy which has retrieved data from a remote server.
* @param {Object} response The XHR object which contains the JSON data in its responseText.
* @return {Object} data A data block which is used by an Ext.data.Store object as
* a cache of Ext.data.Records.
*/
read : function(response){
var json = response.responseText;
var o = Ext.decode(json);
if(!o) {
throw {message: "JsonReader.read: Json object not found"};
}
return this.readRecords(o);
},
// private function a store will implement
onMetaChange : function(meta, recordType, o){
},
/**
* @ignore
*/
simpleAccess: function(obj, subsc) {
return obj[subsc];
},
/**
* @ignore
*/
getJsonAccessor: function(){
var re = /[\[\.]/;
return function(expr) {
try {
return(re.test(expr))
? new Function("obj", "return obj." + expr)
: function(obj){
return obj[expr];
};
} catch(e){}
return Ext.emptyFn;
};
}(),
/**
* Create a data block containing Ext.data.Records from a JSON object.
* @param {Object} o An object which contains an Array of row objects in the property specified
* in the config as 'root, and optionally a property, specified in the config as 'totalProperty'
* which contains the total size of the dataset.
* @return {Object} data A data block which is used by an Ext.data.Store object as
* a cache of Ext.data.Records.
*/
readRecords : function(o){
this[this.dataPropertyName] = o;
if(o.metaData){
delete this.ef;
this.meta = o.metaData;
this.recordType = Ext.data.Record.create(o.metaData.fields);
this.onMetaChange(this.meta, this.recordType, o);
}
var s = this.meta, Record = this.recordType,
f = Record.prototype.fields, fi = f.items, fl = f.length;
// Generate extraction functions for the total property, the success property, the root, the id, and for each field
if (!this.ef) {
this.ef = this.buildExtractors();
}
var root = this.getRoot(o), c = root.length, totalRecords = c, success = true;
if(this.getTotal){
var v = parseInt(this.getTotal(o), 10);
if(!isNaN(v)){
totalRecords = v;
}
}
if(this.getSuccess){
var v = this.getSuccess(o);
if(v === false || v === 'false'){
success = false;
}
}
var records = [];
for(var i = 0; i < c; i++){
var n = root[i];
var record = new Record(this.extractValues(n, fi, fl), this.getId(n));
record.json = n;
records[i] = record;
}
return {
success : success,
records : records,
totalRecords : totalRecords
};
},
// private
buildExtractors : function() {
var p, s = this.meta, Record = this.recordType,
f = Record.prototype.fields, fi = f.items, fl = f.length;
if(p = s.totalProperty) {
this.getTotal = this.getJsonAccessor(p);
}
if(p = s.successProperty) {
this.getSuccess = this.getJsonAccessor(p);
}
this.getRoot = s.root ? this.getJsonAccessor(s.root) : function(p){return p;};
if (p = (s.id || s[this.idPropertyName])) {
var g = this.getJsonAccessor(p);
this.getId = function(rec) {
var r = g(rec);
return (r === undefined || r === "") ? null : r;
};
} else {
this.getId = function(){return null;};
}
var ef = [];
for(var i = 0; i < fl; i++){
f = fi[i];
var map = (f.mapping !== undefined && f.mapping !== null) ? f.mapping : (this.useArray ? i : f.name);
ef.push(this.getJsonAccessor(map));
}
return ef;
},
// private extractValues
extractValues: function(data, items, len) {
var f, values = {};
for(var j = 0; j < len; j++){
f = items[j];
var v = this.ef[j](data);
values[f.name] = f.convert((v !== undefined) ? v : f.defaultValue, data);
}
return values;
},
/**
* readResponse
* decodes a json response from server
* @param {String} action [Ext.data.Api.actions.create|read|update|destroy]
* @param {Object} response
*/
readResponse : function(action, response) {
var json = response.responseText;
var o = Ext.decode(json);
if(!o) {
throw new Ext.data.JsonReader.Error('response', 'JsonReader.js');
}
if (Ext.isEmpty(o[this.meta.successProperty])) {
throw new Ext.data.JsonReader.Error('success', 'JsonReader.js', this.meta.successProperty);
}
// TODO, separate empty and undefined exceptions.
else if ((action == Ext.data.Api.actions.create || action == Ext.data.Api.actions.update) && Ext.isEmpty(o[this.meta.root])) {
throw new Ext.data.JsonReader.Error('root', 'JsonReader.js', this.meta.root);
}
// makde sure extaction functions are defined.
if (!this.ef) {
this.ef = this.buildExtractors();
}
return o;
}
});
/**
* Error class for JsonReader
*/
Ext.data.JsonReader.Error = Ext.extend(Ext.Error, {
cls : 'Ext.data.JsonReader',
render : function(id, file, data) {
switch (id) {
case 'response':
return "An error occurred while json-decoding your server response";
break;
case 'success':
return 'Could not locate your "successProperty" (' + data + ') in your server response. Please review your JsonReader config to ensure the config-property "successProperty" matches the property in your server-response. See the JsonReader docs.';
break;
case 'root':
return 'Could not locate your "root" property (' + data + ') in your server response. Please review your JsonReader config to ensure the config-property "root" matches the property your server-response. See the JsonReader docs.';
break;
}
}
});
So ArrayReader is just
Ext.data.ArrayReader = Ext.extend(Ext.data.JsonReader, {
/**
* @cfg {String} idProperty
* @hide
*/
/**
* @cfg {Number} id (optional) The subscript within row Array that provides an ID for the Record.
* Deprecated. Use {@link #idIndex} instead.
*/
/**
* @cfg {Number} idIndex (optional) The subscript within row Array that provides an ID for the Record.
*/
dataPropertyName: 'arrayData',
idPropertyName: 'idIndex',
useArray: true
});
mystix
26 May 2009, 5:01 AM
+1
i like. :)
mjlecomte
26 May 2009, 7:59 AM
I had been working on something also. Here's my hack merged with Nige's.
Propose:
deprecating jsonData, xmlData, arrayData, in favor of readData for all.
extracting extraction functions to properties which are more easily overridden
Do a diff to see all the changes.
/**
* @class Ext.data.JsonReader
* @extends Ext.data.DataReader
* Data reader class to create an Array of {@link Ext.data.Record} objects from a JSON response
* based on mappings in a provided {@link Ext.data.Record} constructor.<br>
* <p>
* Example code:
* <pre><code>
var Employee = Ext.data.Record.create([
{name: 'firstname'}, // map the Record's "firstname" field to the row object's key of the same name
{name: 'job', mapping: 'occupation'} // map the Record's "job" field to the row object's "occupation" key
]);
var myReader = new Ext.data.JsonReader(
{ // The metadata property, with configuration options:
totalProperty: "results", // the property which contains the total dataset size (optional)
root: "rows", // the property which contains an Array of row objects
idProperty: "id" // the property within each row object that provides an ID for the record (optional)
},
Employee // {@link Ext.data.Record} constructor that provides mapping for JSON object
);
</code></pre>
* <p>This would consume a JSON data object of the form:</p><pre><code>
{
results: 2, // Reader's configured totalProperty
rows: [ // Reader's configured root
{ id: 1, firstname: 'Bill', occupation: 'Gardener' }, // a row object
{ id: 2, firstname: 'Ben' , occupation: 'Horticulturalist' } // another row object
]
}
</code></pre>
* <p><b><u>Automatic configuration using metaData</u></b>
* <p>It is possible to change a JsonReader's metadata at any time by including a <b><tt>metaData</tt></b>
* property in the JSON data object. If the JSON data object has a <b><tt>metaData</tt></b> property, a
* {@link Ext.data.Store Store} object using this Reader will reconfigure itself to use the newly provided
* field definition and fire its {@link Ext.data.Store#metachange metachange} event. The metachange event
* handler may interrogate the <b><tt>metaData</tt></b> property to perform any configuration required.
* Note that reconfiguring a Store potentially invalidates objects which may refer to Fields or Records
* which no longer exist.</p>
* <p>The <b><tt>metaData</tt></b> property in the JSON data object may contain:</p>
* <div class="mdetail-params"><ul>
* <li>any of the configuration options for this class</li>
* <li>a <b><tt>{@link Ext.data.Record#fields fields}</tt></b> property which the JsonReader will
* use as an argument to the {@link Ext.data.Record#create data Record create method} in order to
* configure the layout of the Records it will produce.</li>
* <li>a <b><tt>{@link Ext.data.Store#sortInfo sortInfo}</tt></b> property which the JsonReader will
* use to set the {@link Ext.data.Store}'s {@link Ext.data.Store#sortInfo sortInfo} property</li>
* <li>any user-defined properties needed</li>
* </ul></div>
* <p>To use this facility to send the same data as the example above (without having to code the creation
* of the Record constructor), you would create the JsonReader like this:</p><pre><code>
var myReader = new Ext.data.JsonReader();
</code></pre>
* <p>The first data packet from the server would configure the reader by containing a
* <b><tt>metaData</tt></b> property <b>and</b> the data. For example, the JSON data object might take
* the form:</p>
<pre><code>
{
metaData: {
idProperty: 'id',
root: 'rows',
totalProperty: 'results',
fields: [
{name: 'name'},
{name: 'job', mapping: 'occupation'}
],
sortInfo: {field: 'name', direction:'ASC'}, // used by store to set its sortInfo
foo: 'bar' // custom property
},
results: 2,
rows: [
{ 'id': 1, 'name': 'Bill', occupation: 'Gardener' },
{ 'id': 2, 'name': 'Ben', occupation: 'Horticulturalist' }
]
}
</code></pre>
* @cfg {String} totalProperty Name of the property from which to retrieve the total number of records
* in the dataset. This is only needed if the whole dataset is not passed in one go, but is being
* paged from the remote server.
* @cfg {String} successProperty Name of the property from which to retrieve the success attribute used by forms.
* @cfg {String} root name of the property which contains the Array of row objects.
* @cfg {String} idProperty Name of the property within a row object that contains a record identifier value.
* @cfg {Array/Object} fields Either an Array of field definition objects as passed to
* {@link Ext.data.Record#create}, or a {@link Ext.data.Record Record} constructor created
* using {@link Ext.data.Record#create}. Note that a <code>recordType</code> passed to the
* constructor supersedes the <code>fields</code> configuration.
* @constructor
* Create a new JsonReader
* @param {Object} meta Metadata configuration options.
* @param {Array/Object} recordType See <code>{@link #fields}</code>
*/
Ext.data.JsonReader = function(meta, recordType){
meta = meta || {};
Ext.data.JsonReader.superclass.constructor.call(this, meta, recordType || meta.fields);
};
Ext.extend(Ext.data.JsonReader, Ext.data.DataReader, {
/**
* This JsonReader's metadata as passed to the constructor, or as passed in
* the last data packet's <b><tt>metaData</tt></b> property.
* @type Mixed
* @property meta
*/
/**
* This method is only used by a DataProxy which has retrieved data from a remote server.
* @param {Object} response The XHR object which contains the JSON data in its responseText.
* @return {Object} data A data block which is used by an Ext.data.Store object as
* a cache of Ext.data.Records.
*/
read : function(response){
var json = response.responseText;
var o = Ext.decode(json);
if(!o) {
throw {message: "JsonReader.read: Json object not found"};
}
return this.readRecords(o);
},
dataPropertyName: 'jsonData',
idPropertyName: 'idProperty',
// private function a store will implement
onMetaChange : function(meta, recordType, o){
},
/**
* @ignore
*/
simpleAccess: function(obj, subsc) {
return obj[subsc];
},
/**
* @ignore
*/
getJsonAccessor: function(){
var re = /[\[\.]/;
return function(expr) {
try {
return(re.test(expr))
? new Function("obj", "return obj." + expr)
: function(obj){
return obj[expr];
};
} catch(e){}
return Ext.emptyFn;
};
}(),
/**
* After any data loads, the raw data packet is available for further custom processing.
* If no data is loaded or there is a load exception this property will be undefined.
* @type Object
* @property readData
*/
/**
* Create a data block containing Ext.data.Records from a JSON object.
* @param {Object} o An object which contains an Array of row objects in the property specified
* in the config as 'root, and optionally a property, specified in the config as 'totalProperty'
* which contains the total size of the dataset.
* @return {Object} data A data block which is used by an Ext.data.Store object as
* a cache of Ext.data.Records.
*/
readRecords : function(o){
this[this.dataPropertyName] = o; // deprecate this!
this.readData = o; // use readData for consistency among readers
if(o.metaData){
delete this.ef;
this.meta = o.metaData;
this.recordType = Ext.data.Record.create(o.metaData.fields);
this.onMetaChange(this.meta, this.recordType, o);
}
var Record = this.recordType,
f = Record.prototype.fields, fi = f.items, fl = f.length;
// Generate extraction functions for the totalProperty, the root, the id, and for each field
if (!this.ef) {
this.ef = this.buildExtractors();
}
var root = this.getRoot(o), c = root.length, totalRecords = c, success = true;
var t = parseInt(this.getTotal(o), 10);
if(!isNaN(t)){
totalRecords = t;
}
var s = this.getSuccess(o);
if(s === false || s === 'false'){
success = false;
}
var records = [];
for(var i = 0; i < c; i++){
var n = root[i];
var record = new Record(this.extractValues(n, fi, fl), this.getId(n));
record.json = n;
records[i] = record;
}
return {
success : success,
records : records,
totalRecords : totalRecords
};
},
getTotal : function(o) {
return this.meta.totalProperty ? this.getJsonAccessor(this.meta.totalProperty)(o) : false;
},
getSuccess : function(o) {
return this.meta.successProperty ? this.getJsonAccessor(this.meta.successProperty)(o) : true;
},
getRoot : function(o) {
return this.meta.root ? this.getJsonAccessor(this.meta.root)(o) : o;
},
// private
buildExtractors : function() {
var s = this.meta, Record = this.recordType,
f = Record.prototype.fields, fi = f.items, fl = f.length;
var p = (s.id || s[this.idPropertyName]);
if (p) {
var g = this.getJsonAccessor(p);
this.getId = function(rec) {
var r = g(rec);
return (r === undefined || r === "") ? null : r;
};
} else {
this.getId = function(){return null;};
}
var ef = [];
for(var i = 0; i < fl; i++){
f = fi[i];
var map = (f.mapping !== undefined && f.mapping !== null) ? f.mapping : (this.useArray ? i : f.name);
ef.push(this.getJsonAccessor(map));
}
return ef;
},
// private extractValues
extractValues: function(data, items, len) {
var f, values = {};
for(var j = 0; j < len; j++){
f = items[j];
var v = this.ef[j](data);
values[f.name] = f.convert((v !== undefined) ? v : f.defaultValue, data);
}
return values;
},
/**
* readResponse
* decodes a json response from server
* @param {String} action [Ext.data.Api.actions.create|read|update|destroy]
* @param {Object} response
*/
readResponse : function(action, response) {
var json = response.responseText;
var o = Ext.decode(json);
if(!o) {
throw new Ext.data.JsonReader.Error('response', 'JsonReader.js');
}
if (Ext.isEmpty(o[this.meta.successProperty])) {
throw new Ext.data.JsonReader.Error('success', 'JsonReader.js', this.meta.successProperty);
}
// TODO, separate empty and undefined exceptions.
else if ((action == Ext.data.Api.actions.create || action == Ext.data.Api.actions.update) && Ext.isEmpty(o[this.meta.root])) {
throw new Ext.data.JsonReader.Error('root', 'JsonReader.js', this.meta.root);
}
// makde sure extaction functions are defined.
if (!this.ef) {
this.ef = this.buildExtractors();
}
return o;
}
});
/**
* Error class for JsonReader
*/
Ext.data.JsonReader.Error = Ext.extend(Ext.Error, {
cls : 'Ext.data.JsonReader',
render : function(id, file, data) {
switch (id) {
case 'response':
return "An error occurred while json-decoding your server response";
case 'success':
return 'Could not locate your "successProperty" (' + data + ') in your server response. Please review your JsonReader config to ensure the config-property "successProperty" matches the property in your server-response. See the JsonReader docs.';
case 'root':
return 'Could not locate your "root" property (' + data + ') in your server response. Please review your JsonReader config to ensure the config-property "root" matches the property your server-response. See the JsonReader docs.';
}
}
});
Animal
26 May 2009, 9:24 AM
getTotal could not call this.getJsonAccessor() to create the accessor function on the fly. The reason that this is done is to only create those Json Accessor functions on metachange
so getTotal, getSuccess and getRoot need to be generated in buildExtractors
mjlecomte
26 May 2009, 9:58 AM
Did you test it? I tested it against my MetaGrid and it worked ok. Those get() functions get and then evaluate the extractor.
mjlecomte
26 May 2009, 10:01 AM
PS, part of the motivation behind pulling out those get() functions was so they could be more easily overridden. Perhaps someone will want getSuccess to get some variable defined elsewhere, for instance like an auth check or something, something not actually sent with the immediate data packet (perhaps).
Animal
26 May 2009, 11:18 AM
Yes, you code will have worked. But it generates new accessor functions for every packet read, not just on mete change.
Animal
3 Aug 2009, 8:08 AM
Hey dev team? How about this for a code saving?
JsonReader and ArrayReader are one and the same.
Only in the absence of a field's mapping, JsonReader uses the field's name as a subscript, and ArrayReader uses the field's zero-based position.
Apart from that, there's just the idProperty/idIndex and jsonData/arrayData properties
mjlecomte
3 Aug 2009, 8:39 AM
jsonData/arrayData properties
and I think those should be deprecated for all readers into an "readData" property to be used for all readers.
Condor
3 Aug 2009, 9:08 AM
I had some similar requests:
Code reduction in DataProxies (http://www.extjs.com/forum/showthread.php?t=60233)
Code reduction in DataReaders (http://www.extjs.com/forum/showthread.php?t=62120)
(unfortunately the code from those 2 posts predates the Ext direct additions)
watrboy00
26 Aug 2009, 10:58 AM
and I think those should be deprecated for all readers into an "readData" property to be used for all readers.
I think this would be nice for consistency across usage. No matter what I am using for my implementation if I can rely on a single property then refactoring when needed reduces the amount of code I have to refactor.
Powered by vBulletin® Version 4.1.5 Copyright © 2012 vBulletin Solutions, Inc. All rights reserved.