Custom Data Readers in Ext JS

Exclusive offer: get 50% off this eBook here
Learning Ext JS

Learning Ext JS — Save 50%

Build dynamic, desktop-style user interfaces for your data-driven web applications

$23.99    $12.00
by Steve 'Cutter' Blades | January 2009 | AJAX Open Source

Ext JS is an extremely powerful, cross-browser library, providing any developer with a beautiful, consistent set of tools for laying out browser-based applications. But there's a lot more here than just pretty boxes and grids. An application without data is really nothing more than an interactive static page, and our users are going to want to manipulate real information.

Steve Blades, one of the authors of Learning Ext JS follows up with this article on custom data readers.

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.
* licensing@extjs.com
*
* 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='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/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.

Learning Ext JS Build dynamic, desktop-style user interfaces for your data-driven web applications
Published: November 2008
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

About the Author :


Steve 'Cutter' Blades

Cutter is the Senior Web Developer for Dealerskins, a Nashville, Tennessee based hosting provider that develops websites for the Automobile Dealership market. Cutter began his web career when he began learning HTML 1 while in the US Army and stationed with the National Security Agency. Cutter got into application development as a Corporate Support Specialist for a regional ISP, just prior to becoming the IT Director of Seacrets, a large resort destination on the Eastern Shore of Maryland. Cutter has extensive experience as a server- and client-side developer, with a popular blog dedicated to ColdFusion, Ext JS, and other web development technologies.

Books From Packt

Microsoft AJAX Library Essentials: Client-side ASP.NET AJAX 1.0 Explained
Microsoft AJAX Library Essentials: Client-side ASP.NET AJAX 1.0 Explained

ZK Developer’s Guide
ZK Developer’s Guide

EJB 3 Developer Guide
EJB 3 Developer Guide

DWR Java AJAX Applications
DWR Java AJAX Applications

Java EE 5 Development with NetBeans 6
Java EE 5 Development with NetBeans 6

Apache OFBiz Development: The Beginner's Tutorial
Apache OFBiz Development: The Beginner's Tutorial

Object-Oriented JavaScript
Object-Oriented JavaScript

Drupal 6 JavaScript and jQuery: RAW
Drupal 6 JavaScript and jQuery: RAW

 

 

Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software