ExtJS 3.0 Editor Grid with ColdFusion + Ext Toolbar Play

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 plainprintabout
 <html>
     
     <head>
         <title>Ext JS Grid with ColdFusion</title>
         <link rel="stylesheet" type="text/css" href="resources/css/ext-all.css">
         <script src="adapter/ext/ext-base.js"></script>
         <script src="js/ext-all.js"></script>        
         
         <script 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 plainprintabout
 Ext.onReady(function(){
     
     //This is our JSON record set which defines what kind of data will be present in the JSON passed back from our component.
     var users = Ext.data.Record.create([
     {name:'ID',allowBlank:false},
     {name:'FIRSTNAME',type:'string'},
     {name:'LASTNAME',type:'string'},
     {name:'DISPLAYNAME',type:'string'},
     {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 plainprintabout
 successProperty:'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 plainprintabout
 editor: 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 plainprintabout
 listeners: {
      afteredit: {
          fn: editStore
          }
     }
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 plainprintabout
 //The Proxy object will specify URLs to call for different CRUD operations
     var proxy = new Ext.data.HttpProxy({
         method:'POST',                
         api:{
             read: 'Users.cfc?method=getUsers',//Our URL for reading the grid data
             create: 'Users.cfc?method=addUser',//For Adding a new User (future implementation)
             update: 'Users.cfc?method=editUser'//When a User is updated
         },
         //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 plainprintabout
 /Our server call was successful, display user feedback here
     function writeSuccess(store, action, result, res, rs)
     {
         //Get Toolbar Object
         var tbar = grid.getTopToolbar();
         //Remove current text
         tbar.remove("tbartext");
         //Add new text        
         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 plainprintabout
 <cfcomponent output="false">
     
     <cffunction name="getUsers" access="remote" returnFormat="json" output="false">
         <cfargument name="limit" default="50">
         <cfargument name="start" default="1">
         <cfargument name="sort" default="FirstName">
         <cfargument name="dir" default="ASC">
         
         <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 plainprintabout
 id:'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.

Related Blog Entries

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Henry Ho's Gravatar Hmm... Isn't this what CFGRID is for?
# Posted By Henry Ho | 12/15/09 5:12 PM
Kumar's Gravatar cfgrid is great for out of the box implementations. Using the Ext JS libraries you can achieve a lot of great functionality, not present in cfgrid out of the box.
# Posted By Kumar | 12/15/09 10:56 PM
Murray's Gravatar Thanks for that. It saved me a lot of time trying to figure it all out!
# Posted By Murray | 12/28/09 8:55 PM
Murray's Gravatar @Henry Ho

Another reason that this post is very relevant to me is that the apps I build are totally ExtJS on the client side. I am only using CF for the server side.
# Posted By Murray | 12/28/09 8:58 PM
Jochen's Gravatar Great example of ExtJS-Grid and Coldfusion.

But why don“t you use the QueryConvertForGrid?
# Posted By Jochen | 4/15/10 5:40 AM
Alan's Gravatar Almost working ... but when I update the toolbox message is Success !!! undefined. (and not updating). But if i make cfargument name="id" default as an ID of 1 or 2 etc then it updates the relavent record for that ID. Not sure why the ID is not getting passed. Help much appreciated.
# Posted By Alan | 7/8/10 3:07 AM
Taguchi's Gravatar Alan,

I've been struggling for 5 hours on why it wasn't updating properly. Read your comment and it worked like a charm. I have no idea why have a default value of 0 doesn't work.

Unlike java, coldfusion does not use the 0-index for arrays, I'm not sure if that would be causing the issue.

Unfortunately there is very little information regarding ExtJS and coldfusion online. There were a couple of hooks and frameworks created in 2009 by fellow CF'ers (e.g, EXTDirectCFC and ColdEXT) but there developers are no longer maintaining them. Kumar's blogs are the only examples with real DB queries.

Thanks Kumar,

Taguchi
# Posted By Taguchi | 7/19/10 8:55 AM
Taguchi's Gravatar Hi Alan,

Disregard my previous post. Changing the default value of ID only updates that ID value.

If you look at firebug, coldfusion is receiving a structure where the ID (the row being updated is being passed along with the other fields. You can extract it's value via
<cfset fred = #stcData.id#>

I then reference "fred" when in my query. for example,

      <cfquery name="selUser" datasource="#THIS.DSN#">
         SELECT
            userID, FirstName, LastName, phone, city
         FROM
            USERS
         WHERE
            ID = #fred#
      </cfquery>
# Posted By Taguchi | 7/19/10 9:30 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.5.003.  Design based on ARCLITE by: digitalnature