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.
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
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 <script src="js/ext-all.js"></script>
8
9 <script src="editgrid.js"></script>
10
11 </head>
12 <body>
13
14 <div id="editgrid"></div>
15
16 </body>
17 </html>
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 });
Also, in each column definition we have added its editor definition.
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.
2 afteredit: {
3 fn: editStore
4 }
5 }
Now, if we go up a bit and look at our proxy definition.
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 })
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 CFC is good old ColdFusion code.
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>
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:
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.
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.
But why don“t you use the QueryConvertForGrid?
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
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>