8 min read

When writing Chapter 12, “It’s All about the Data,” of Learning Ext JS, I switched things up a bit and switched the server-side processes to utilizing Adobe’s ColdFusion application server, instead of the PHP we had been using in the rest of the book. There were a few reasons we decided to do this.

  1. To show that Ext JS can work with any server-side technology.
  2. ColdFusion 8 includes Ext JS 1.1 for it’s new Ajax form components.
  3. Adobe uses a custom format for the serialized JSON return of query data, making it perfect for our example needs.
  4. I’m a ColdFusion programmer.

Some time ago, before writing Chapter 12, I had begun to use a Custom Data Reader that I had found on the Ext JS forums. Another Ext user and ColdFusion programmer, John Wilson, had written the custom reader to consume Adobe’s custom JSON return for queries. First, let me show you why Adobe’s format differs from the generally expected serialized JSON return of a query.

Here’s an example of a typical query response.

    {
'results': 2,
'rows': [
{ 'id': 1, 'firstname': 'Bill', occupation: 'Gardener' }, // a row object
{ 'id': 2, 'firstname': 'Ben' , occupation: 'Horticulturalist' } // another row object
]
}

And here’s an example of how ColdFusion returns a query response.

    {
        "COLUMNS":["INTPROPERTIESID","STRDEVELOPMENT","INTADDRESSID",
"STRSTREET","STRSTREET2", "STRCITY","CHSTATEID","INTZIP"],
        "DATA":[
            [2,"Abbey Road",6,"456 Abbey Road","Villa 5","New York","NY",12345],
            [6,"Splash",39,"566 aroundthe bend dr",null,"Nashville","TN",37221]
        ]
    }

You can see, when examining the two formats that they are very divergent. The typical format returns an array of row objects of the query’s results, whereas ColdFusion’s format is an array (DATA) of arrays (each row of the query result), with each row array only containing the data. The ColdFusion format has extracted the column names into it’s own array (COLUMNS), as opposed to the name/value pairing found in the object notation of the typical return. It’s actually very smart, on Adobe’s part, to return the data in this fashion, as it would ultimately mean smaller data sets returned from a remote call, especially with large recordsets.

John’s CFJsonReader, a custom data reader and an extended component of Ext’s base DataReader, was able to translate ColdFusion’s data returns by properly parsing the JSON return into Records of an Ext Store. It worked fairly well, with a few minor exceptions.

  • it didn’t handle the column aliasing you could do with any other Ext JS data reader (name:’development’,mapping:’STRDEVELOPMENT’)
  • it didn’t allow data type association with a value, as other Ext JS data readers (INTZIP is of type ‘int’, STRDEVELOPMENT is of type ‘string’, etc)

So, it worked, but ultimately was limited. When I was writing Chapter 13, “Code for Reuse: Extending Ext JS“, I really dove into extending existing Ext JS components. This helped me gain a better understanding of what John had done, when writing CFJsonReader. But, after really reviewing the code, I saw there was a better way of handling ColdFusion’s JSON return. What it basically came down to was that John was extending Ext’s base DataReader object, and then hand parsing almost the entire return. Looking at the above examples, you’ll notice that Adobe’s implementation is an array of arrays, rather than an array of objects. Ext JS already comes with an ArrayReader object, so I knew that by writing a custom data reader that extended it I would be able to get the desired results. Half an hour later, I had “built a better mousetrap” and we now have a Custom Data Reader for properly parsing ColdFusion’s JSON return, without the previous limitations.

   /*
* Ext JS Library 2.0
* Copyright(c) 2006-2007, Ext JS, LLC.
* [email protected]
*
* http://extjs.com/license
*
*******************************************
* Steve 'Cutter' Blades (CutterBl) no.junkATcutterscrossingDOTcom
* http://blog.cutterscrossing.com
*
* Inspired by the CFJsonReader, originally writtin by John Wilson (Daemach)
* http://extjs.com/forum/showthread.php?t=21408&highlight=cfjsonreader
*
* This Custom Data Reader will take the JSON return of a ColdFusion
* Query object, rather returned straight up, or via the ColdFusion
* QueryForGrid() method.
*
* The CFQueryReader constructor takes two arguments
* @meta : object containing single key/value pair for the 'id' of each record
* @recordType : field mapping object
*
* The recordType object allows you to alias the returned ColdFusion column
* name (which is always passed in upper case) to any 'name' you wish, as
* well as assign a data type, which your ExtJS app will attempt to cast
* whenever the value is referenced.
*
* ColdFusion's JSON return, for a ColdFusion Query object, will appear in the
* following format:
*
* {"COLUMNS":["INTVENDORTYPEID","STRVENDORTYPE","INTEXPENSECATEGORIESID",
* "STREXPENSECATEGORIES"],"DATA" :[[2,"Carpet Cleaning",1,"Cleaining"],
* [1,"Cleaning Service",1,"Cleaining"]]}
*
* The ColdFusion JSON return on any query that is first passed through
* ColdFusion's QueryForGrid() method will return the object in the
* following format:
*
* {"TOTALROWCOUNT":3, "QUERY":{"COLUMNS":["MYIDFIELD","DATA1","DATA2"],
* "DATA":[[1,"Bob","Smith"],[6,"Jim","Brown"]]}}
*
* The Ext.data.CFQueryReader is designed to accomodate either format
* automatically. You would create your reader instance in much the same
* way as the CFJsonReader was created:
*
* var myDataModel = [
* {name: 'myIdField', mapping: 'MYIDFIELD'},
* {name: 'data1', mapping: 'DATA1'},
* {name: 'data2', mapping: 'DATA2'}
* ];
*
* var myCFReader = new Ext.data.CFJsonReader({id:'myIdField'},myDataModel);
*
* Notice that the 'id' value mirrors the alias 'name' of the record's field.
*/
Ext.data.CFQueryReader = function(meta, recordType){
this.meta = meta || {};
Ext.data.CFQueryReader.superclass.constructor.call(this, meta, recordType || meta.fields);
};

Ext.extend(Ext.data.CFQueryReader, Ext.data.ArrayReader, {
read : function(response){
var json = response.responseText;
var o = eval("("+json+")");
if(!o) {
throw {message: "JsonReader.read: Json object not found"};
}
if(o.TOTALROWCOUNT){
this.totalRowCount = o.TOTALROWCOUNT;
}
return this.readRecords(((o.QUERY)? o.QUERY : o));
},
readRecords : function(o){
var sid = this.meta ? this.meta.id : null;
var recordType = this.recordType, fields = recordType.prototype.fields;
var records = [];
var root = o.DATA;
// give sid an integer value that equates to it's mapping
sid = fields.indexOfKey(sid);
// re-assign the mappings to line up with the column position
// in the returned json response
for(var a = 0; a < o.COLUMNS.length; a++){
for(var b = 0; b < fields.length; b++){
if(fields.items[b].mapping == o.COLUMNS[a]){
fields.items[b].mapping = a;
}
}
}
for(var i = 0; i < root.length; i++){
var n = root[i];
var values = {};
var id = ((sid || sid === 0) && n[sid] !== undefined && n[sid] !== "" ? n[sid] : null);
for(var j = 0, jlen = fields.length; j < jlen; j++){
var f = fields.items[j];
var k = f.mapping !== undefined && f.mapping !== null ? f.mapping : j;
var v = n[k] !== undefined ? n[k] : f.defaultValue;
v = f.convert(v, n);
values[f.name] = v;
}
var record = new recordType(values, id);
record.json = n;
records[records.length] = record;
}
if(!this.totalRowCount){
this.totalRowCount = records.length;
}
return {
records : records,
totalRecords : this.totalRowCount
};
}
});

So, this changes our examples for Chapter 12 just a little bit. First of all, we’ll need to have the CFQueryReader included, in place of the CFJsonReader. You can change the script tags in the samples for Examples 3 and 4.

	...
<script language="javascript" type="text/javascript" src="/scripts/custom-ext/CFQueryReader.js"></script>
...

Next, we’ll change the scripts for these two examples. We’ll remove our configuration references for CFJsonReader, and replace them with the updated configuration for the CFQueryReader.

    /*
* Chapter 12 Example 3
* Data Store from custom reader
*
* Revised: SGB (Cutter): 12.17.08
* Replaced CFJsonReader with CFQueryReader
*/

// Save all processing until the
// DOM is completely loaded
Ext.onReady(function(){
var ourStore = new Ext.data.Store({
url:'Chapter12Example.cfc',
baseParams:{
method: 'getFileInfoByPath',
returnFormat: 'JSON',
queryFormat: 'column',
startPath: '/images/'
},
reader: new Ext.data.CFQueryReader({
id: 'NAME', // This is supposed to match the 'mapping'
fields:[
{name:'file_name',mapping:'NAME'},
{name:'file_size',mapping:'SIZE'},
{name:'type',mapping:'TYPE'},
{name:'lastmod',mapping:'DATELASTMODIFIED'},
{name:'file_attributes',mapping:'ATTRIBUTES'},
{name:'mode',mapping:'MODE'},
{name:'directory',mapping:'DIRECTORY'}

]
}),
fields: recordModel,
listeners:{
beforeload:{
fn: function(store, options){
if (options.startPath && (options.startPath.length > 0)){
store.baseParams.startPath = options.startPath;
}
},
scope:this
},
load: {
fn: function(store,records,options){
console.log(records);
}
},
scope:this
}
});

ourStore.load();
});


/*
* Chapter 12 Example 4
* Data Store from custom reader - Filtering
*
* Revised: SGB (Cutter): 12.17.08
* Replaced CFJsonReader with CFQueryReader
*/

// Simple function/object to 'clone' objects
cloneConfig = function (config) {
for (i in config) {
if (typeof config[i] == 'object') {
this[i] = new cloneConfig(config[i]);
}
else
this[i] = config[i];
}
}

// Save all processing until the
// DOM is completely loaded
Ext.onReady(function(){
var initialBaseParams = {
method: 'getDirectoryContents',
returnFormat: 'JSON',
queryFormat: 'column',
startPath: '/testdocs/'
};

var ourStore = new Ext.data.Store({
url:'Chapter12Example.cfc',
baseParams: new cloneConfig(initialBaseParams),
reader: new Ext.data.CFQueryReader({
id: 'NAME', // This is supposed to match the 'mapping'
fields:[
{name:'file_name',mapping:'NAME'},
{name:'file_size',mapping:'SIZE'},
{name:'type',mapping:'TYPE'},
{name:'lastmod',mapping:'DATELASTMODIFIED'},
{name:'file_attributes',mapping:'ATTRIBUTES'},
{name:'mode',mapping:'MODE'},
{name:'directory',mapping:'DIRECTORY'}
]
}),
listeners:{
beforeload:{
fn: function(store, options){
for(var i in options){
if(options[i].length > 0){
store.baseParams[i] = options[i];
}
}
},
scope:this
},
load: {
fn: function(store, records, options){
console.log(records);
},
scope: this
},
update: {
fn: function(store, record, operation){
switch (operation){
case Ext.record.EDIT:
// Do something with the edited record
break;
case Ext.record.REJECT:
// Do something with the rejected record
break;
case Ext.record.COMMIT:
// Do something with the committed record
break;
}
},
scope:this
}
}
});

ourStore.load({recurse:true});

filterStoreByType = function (type){
ourStore.load({dirFilter:type});
}

filterStoreByFileType = function (fileType){
ourStore.load({fileFilter:fileType});
}

clearFilters = function (){
ourStore.baseParams = new cloneConfig(initialBaseParams);
ourStore.load();
}
});

Summary

These very basic changes have no overall effect on our examples. They function exactly as they did before. The new Custom Data Reader loads the data, returned from ColdFusion, exactly as it should. Now, we can also work with these data stores in the same manor as we would with any other data store set up through Ext JS, having the ability to alias columns, define field data types, and more.

LEAVE A REPLY

Please enter your comment!
Please enter your name here