I hadn't looked at the Ext JS library in a while, but this weekend, I took a look at it with the idea of eventually building out a full CRUD grid using Ext JS and ColdFusion. It took some time, particularly trying to figure out the phantom records implementation, but its done now.

View the Demo

And as always, time to look at the code.

HTML, very simple. Just defining all our JS/CSS includes and our grid related HTML div

view plain print about
1<html>
2    
3    <head>
4        <title>Ext JS Grid with ColdFusion</title>
5        <link rel="stylesheet" type="text/css" href="resources/css/ext-all.css">
6        <script src="adapter/ext/ext-base.js"></script>
7        <InvalidTag src="js/ext-all.js"></script>        
8        
9        <InvalidTag src="editgrid.js"></script>
10        
11    </head>
12    <body>
13        
14        <div id="editgrid"></div>
15        
16    </body>
17</html>

Now the interesting stuff. The javascript.

view plain print about
1Ext.onReady(function(){
2    
3    //This is our JSON record set which defines what kind of data will be present in the JSON passed back from our component.
4    var users = Ext.data.Record.create([
5    {name:'ID',allowBlank:false},
6    {name:'FIRSTNAME',type:'string'},
7    {name:'LASTNAME',type:'string'},
8    {name:'DISPLAYNAME',type:'string'},
9    {name:'USERNAME',type:'string'},
10    {name:'USERACCOUNTINGCODE',type:'string'},
11    {name:'PHONE',type:'string'}
12    ])
13    
14    //The Proxy object will specify URLs to call for different CRUD operations
15    var proxy = new Ext.data.HttpProxy({
16        method:'POST',                
17        api:{
18            read: 'Users.cfc?method=getUsers',//Our URL for reading the grid data
19            create: 'Users.cfc?method=addUser',//For Adding a new User (future implementation)
20            update: 'Users.cfc?method=editUser'//When a User is updated
21        },
22        //the function writeSuccess will be called when the Store finishes a succesful write (add/edit) to the server
23        listeners: {
24            write: {
25                fn: writeSuccess
26            }
27        }
28    })
29    
30    //Our JSON Writer, configuration used to updating/adding of records
31    var writer = new Ext.data.JsonWriter({
32        //We will only send the changed fields
33        writeAllFields:false        
34    })
35    
36 // create the Data Store
37 var store = new Ext.data.JsonStore({
38     totalProperty:'DATASET',//This is how many total records are there in the set.
39     root:'ROWS',//The Root of the data.
40        proxy:proxy,
41        id:'ID',
42        successProperty:'SUCCESS',//What return string data will be set to true/false to let Ext know that server call was succesful or not
43        autoSave:false,//we will use event listeners to call save manually
44 remoteSort:true,//We will sort server side
45 //Base Params are parameters passed in during every call
46 baseParams:{     
47     returnFormat: 'JSON',
48     start: '0',
49     limit: '50'
50 },
51 //We define the JSON Reader for the data. We also need to set the totalProperty, root and idProperty for the dataset here.
52 reader: new Ext.data.JsonReader({
53         totalProperty:'DATASET',
54         root:'ROWS',
55         idProperty:'ID'                                                
56     },users
57 ),
58 //Fields read in
59 fields: [
60 'ID','FIRSTNAME','LASTNAME','DISPLAYNAME','USERNAME','USERACCOUNTINGCODE','PHONE'
61 ],
62        writer:writer,
63        //Any Store execeptions will be caught here
64        listeners: {            
65            exception : function(proxy,type,action,options,res,rs) {
66                alert(type);
67            }
68        }
69 });
70    
71    // Our Form Variable to be used for editor grid
72    var varForm = Ext.form;
73    
74     //We setup the Grid
75    var cm = new Ext.grid.ColumnModel({
76 columns:[
77         new Ext.grid.RowNumberer(),//This will do numbering on the grid for us
78 {
79            header: "ID",
80            dataIndex:'ID',
81            editable: false,
82            width:50
83        },
84        {            
85 header: "First Name",
86 dataIndex: 'FIRSTNAME',
87 width: 100,
88 hidden:false,
89 sortable: true,            
90 editor: new varForm.TextField({
91 allowBlank: false
92 })
93 },{
94 header: "Last Name",
95 dataIndex: 'LASTNAME',
96 width: 100,
97 hidden: false,
98 sortable: true,
99            editor: new varForm.TextField({
100 allowBlank: false
101 })
102 },{
103 header: "Display Name",
104 dataIndex: 'DISPLAYNAME',
105 width: 150,
106 hidden: false,
107 sortable: true,
108             editor: new varForm.TextField({
109 allowBlank: false
110 })
111 },{
112 header: "User Name",
113 dataIndex: 'USERNAME',
114 width: 100,
115 hidden: false,
116 sortable: true,
117            editor: new varForm.TextField({
118 allowBlank: false
119 })
120 },{
121 header: "Contact",
122 dataIndex: 'PHONE',
123 width: 100,
124 hidden: false,
125 sortable: true,
126            editor: new varForm.TextField({
127 allowBlank: false
128 })
129 }]
130 })
131    
132     // create the editor grid
133 var grid = new Ext.grid.EditorGridPanel({
134 width:750,
135 height:500,        
136 title:'Users',
137 store: store,
138 trackMouseOver:true,
139 disableSelection:false,
140 loadMask: true,
141        stripeRows: true,
142        collapsible: true,
143        cm:cm,//Our column model
144        frame:true,//Make it more nicer looking (?)
145 clicksToEdit: 1,//One click on row data will bring on edit box
146        renderTo:'editgrid',
147        // paging bar on the bottom
148 bbar: new Ext.PagingToolbar({
149 pageSize: 50,
150 store: store,
151 displayInfo: true,
152 displayMsg: 'Displaying Records {0} - {1} of {2}',
153 emptyMsg: "No Records to display"
154 }),
155        tbar: new Ext.Toolbar({
156            items : [
157                {
158                    xtype:'tbtext',
159                    text:'Welcome. Click Row Data to Edit.',
160                    itemId:'tbartext',
161                    id:'tbartext'
162                }
163            ]
164        }),
165         listeners: {
166             afteredit: { //This listener event function will be called when user finishes editing data box and tabs/enters/clicks out.
167                 fn: editStore
168             }
169        }
170 });
171    
172    function editStore(e)
173    {
174        //Call our store's save event that will fire things off'
175        this.store.save();        
176    }
177    
178    //Our server call was succesful, display user feedback here
179    function writeSuccess(store, action, result, res, rs)
180    {
181        //Get Toolbar Object
182        var tbar = grid.getTopToolbar();
183        //Remove current text
184        tbar.remove("tbartext");
185        //Add new text        
186        tbar.addText({
187            xtype:'tbtext',
188            text:'Success !! ' + res.MESSAGE,
189            itemId:'tbartextsuccess'
190            }
191        );
192        //Redo layout to render new toolbar text
193        tbar.doLayout();
194    }
195    
196    //Default Sort set for the grid load call
197    store.setDefaultSort('ID','ASC');
198
199 // trigger the data store load
200 store.load();    
201    
202});

Hopefully, the comments will help understand what is happening. But we will look at a few important aspects of the code. In the JSON Store definition, we have:

view plain print about
1successProperty:'SUCCESS', autoSave:false
By default, the successProperly is set to "success". This is important, because our ColdFusion returned JSON will convert the a returned "success" variable to upppercase. By setting the autosave to false, we will have to make our own store.save() call to trigger the update process. For purposes of this blog post, it makes no difference what this is set to. But by setting it to false, one can allow for batch edits very easily instead of editing on each change.
Also, in each column definition we have added its editor definition.
view plain print about
1editor: new varForm.TextField({ allowBlank: false })
This will let us edit the column using a textbox, and we will not allow blank values for the column.
This example, implements the in-built EditGridPanel. The definition here has a couple of important things. One is the Toolbar we have specified. We will use this to show some simple user feedback. Other is the listeners. We have added a listener for the afteredit event.
view plain print about
1listeners: {
2     afteredit: {
3         fn: editStore
4         }
5    }
This event is triggered when a user changes any data in an editable cell. We are telling Ext to call the editStore function when this happens. And our edit store function, is well very simple here. Just calls the store's save method, triggering the update process.

Now, if we go up a bit and look at our proxy definition.

view plain print about
1//The Proxy object will specify URLs to call for different CRUD operations
2    var proxy = new Ext.data.HttpProxy({
3        method:'POST',                
4        api:{
5            read: 'Users.cfc?method=getUsers',//Our URL for reading the grid data
6            create: 'Users.cfc?method=addUser',//For Adding a new User (future implementation)
7            update: 'Users.cfc?method=editUser'//When a User is updated
8        },
9        //the function writeSuccess will be called when the Store finishes a succesful write (add/edit) to the server
10        listeners: {
11            write: {
12                fn: writeSuccess
13            }
14        }
15    })
The proxy object can be defined to let Ext know which URLs to call for different CRUD functions. In addition, we are also specifying a listener for the write event. The write event is called upon a succesful write (add and edit) to the server. We will use this to show returned server feedback to the user.
view plain print about
1/Our server call was successful, display user feedback here
2    function writeSuccess(store, action, result, res, rs)
3    {
4        //Get Toolbar Object
5        var tbar = grid.getTopToolbar();
6        //Remove current text
7        tbar.remove("tbartext");
8        //Add new text        
9        tbar.addText({
10            xtype:'tbtext',
11            text:'Success !! ' + res.MESSAGE,
12            itemId:'tbartextsuccess'
13            }
14        );
15        //Redo layout to render new toolbar text
16        tbar.doLayout();
17    }
Our function here, manipulates the grid toolbar we have specified. First, we will remove our current toolbar text. We use the added text item's "itemId" that we specified for this. Then we add a new text item to the toolbar. After that, we have to call the toolbar's doLayout function, which forces Ext to recalculate the toolbar's render and hence it will show our new text.

Our CFC is good old ColdFusion code.

view plain print about
1<cfcomponent output="false">
2    
3    <cffunction name="getUsers" access="remote" returnFormat="json" output="false">
4        <cfargument name="limit" default="50">
5        <cfargument name="start" default="1">
6        <cfargument name="sort" default="FirstName">
7        <cfargument name="dir" default="ASC">
8        
9        <cfset var arrUsers = ArrayNew(1)>
10        <cfset var stcReturn = "">
11        
12        <!--- When going back to base state, ExtJS will make the function call with start set to 0. If this is the case
13        we set it to 1 --->

14        <cfset Arguments.start = Arguments.start + 1>        
15        
16        <cfquery name="selUsers" datasource="RIADemo">
17                SELECT
18                    ID, FirstName, LastName, DisplayName, UserName, UserAccountingCode, Phone
19                FROM
20                    Users
21                ORDER BY #Arguments.sort# #Arguments.dir#
22        </cfquery>
23        
24        <cfset arrUsers = convertQueryToExtJSGrid(selUsers,Arguments.start,Arguments.limit)>
25        
26        <cfset stcReturn = {rows=arrUsers,dataset=#selUsers.RecordCount#}>
27        
28        <cfreturn stcReturn>
29    
30    </cffunction>
31    
32    <cffunction name="convertQueryToExtJSGrid" access="public" hint="Convert Query to JSON usable by ExtJS Grid" returntype="array">
33        <cfargument name="qryData" type="query" required="true" hint="Query to convert">
34        <cfargument name="intStart" type="numeric" required="true" hint="Start of Result Set">
35        <cfargument name="intLimit" type="numeric" required="true" hint="How many records to return">
36        
37        <!--- For the Array --->    
38        <cfset var i = 1>        
39        <cfset var end = ((Arguments.intStart) + arguments.intLimit)-1>
40        <cfset var selUsers = "">
41        <cfset var stcReturn = "">
42        <cfset var arrUsers = ArrayNew(1)>
43        
44        <cfloop query="qryData" startrow="#Arguments.intStart#" endrow="#end#">        
45            <cfset stcUsers = StructNew()>
46            <cfset stcUsers.ID = #qryData.ID#>
47            <cfset stcUsers.FIRSTNAME = #qryData.FirstName#>
48            <cfset stcUsers.LASTNAME = #qryData.LastName#>
49            <cfset stcUsers.DISPLAYNAME = #qryData.DisplayName#>
50            <cfset stcUsers.USERNAME = #qryData.UserName#>
51            <cfset stcUsers.USERACCOUNTINGCODE = #qryData.UserAccountingCode#>
52            <cfset stcUsers.PHONE = #qryData.Phone#>
53            <cfset arrUsers[i] = stcUsers>
54            <cfset i = i + 1>            
55        </cfloop>
56        
57        
58        <cfreturn arrUsers>
59        
60    </cffunction>
61    
62    <cffunction name="editUser" access="remote" hint="Edit a User" returnformat="JSON">
63        <cfargument name="rows" type="any" required="true" default="0" hint="Row Data containing our edits">
64        <cfargument name="id" type="numeric" required="true" default="0" hint="our ID record, will tell us which record to update">
65        <!--- Deserialize the incoming JSON data to get our CF data --->
66        <cfset var stcData = deserializeJSON(Arguments.rows)>
67        <cfset var strUpdQuery = "">
68        <cfset var updUser = "">
69        <cfset var stcReturn = "">
70        <!--- Check which field we are editing, build Edit string based on that --->
71        <cfif StructKeyExists(stcData,"FIRSTNAME")>
72            <cfset strUpdQuery = strUpdQuery & "FirstName = '#stcData.FIRSTNAME#'">
73        <cfelseif StructKeyExists(stcData,"LASTNAME")>
74            <cfset strUpdQuery = strUpdQuery & "LastName = '#stcData.LASTNAME#'">
75        <cfelseif StructKeyExists(stcData,"DISPLAYNAME")>
76            <cfset strUpdQuery = strUpdQuery & "DisplayName = '#stcData.DISPLAYNAME#'">
77        <cfelseif StructKeyExists(stcData,"USERNAME")>
78            <cfset strUpdQuery = strUpdQuery & "UserName = '#stcData.USERNAME#'">
79        <cfelseif StructKeyExists(stcData,"PHONE")>
80            <cfset strUpdQuery = strUpdQuery & "Phone = '#stcData.PHONE#'">
81        </cfif>
82        
83        <cfquery name="updUser" datasource="RIADemo">
84            UPDATE USERS
85            SET #PreserveSingleQuotes(strUpdQuery)#
86            WHERE id = #Arguments.id#
87        </cfquery>
88        
89        <cfquery name="selUser" datasource="RIADemo">
90            SELECT
91                ID, FirstName, LastName, DisplayName, UserName, UserAccountingCode, Phone
92            FROM
93                Users
94            WHERE
95                ID = #Arguments.id#
96        </cfquery>
97        <!--- Our return row converted to Ext JS JSON format --->
98        <cfset arrUsers = convertQueryToExtJSGrid(selUser,1,1)>
99        <!--- Success!! and additional return data --->
100        <cfset stcReturn = {success=true,rows=arrUsers,message="Succesfully Edited User"}>
101        
102        <cfreturn stcReturn>
103        
104    </cffunction>
105    
106    <cffunction name="addUser" access="remote" hint="Edit a User" returnformat="JSON">
107        
108        
109        <cfset stcReturn = {success=true}>        
110        <cfreturn stcReturn>
111        
112    </cffunction>
113
114</cfcomponent>

The new function that I added was the editUser function. It simply, deserializes the incoming JSON, to get good old CF from it. Check which field is being edited. Run update query and then return success (because well, I assume its always going to be succesful).

Now, the challenge that I faced while I was writing this code up, was what Ext calls "phantom records". The easiest way to explain a phantom record is that it is basically a new record that has been added to the Ext Store that has not been committed to the server yet. But of course, I wasn't adding records, I was just updating someone's First Name, but Ext was still passing it off as a phantom record. What this does is that, Ext calls the create URL we have defined in our proxy instead of the update URL. For a while, I couldn't figure it out. I looked through the source, Ext used the specified idProperty for a recordset to determine whether it was a phantom or not. But I had specified my idProperty properly in the JSONReader. I searched the forums, others have seen the same issue, best suggestion given was read the source. Eventually, I realized, that you can specify an "id" for the JSONStore. And, so I added the simple definition:

view plain print about
1id:'ID',
to my JSONStore definition, and magic, it was working as intended. Great.

So, there we have, editor grid with ExtJS and ColdFusion.
Download Source (cfm/cfc/js files)

Questions? Comments? Leave a note here or contact me. Always glad to help.

Note: You might notice some javascript errors, especially if using Firebug. These are known bugs in the Ext JS library but don't seem to have any impact on the functionality.