My Examples of Using ExtJS 3.2 and ColdFusion. There do not seem to be many examples out there so I thought I would start contributing to help my fellow ColdFusion coders.
Suppose you have a listing of movies and you have a combo box where you can choose the genre to which that movie belongs. If you decide to add a new genre you do not want to have to log into the database and add a new genre record into your genre database. Why - because you are a developer and you are lazy right? And if someone else is doing the administration you do not want them to be in a position of needing to contact you to do this. Unless you can charge them for it. If that is the case, do not read this article.:)
Almost anytime you have a combo box you are pointing to a reference table where you may want to add entries. The only exception to this would be a states database because it is unlikely we will add any new states. But for combo boxes that list reference information that is subject to change, it would be great to add the "genre", "category" or whatever on the fly.
How do you do this?
Well first, add a row to your table through your database editor. Here just insert the genre and do not define the value of the id. If it works great. If not, it is because the id is not defined as autoincrement. I am assuming every table you create has a column called id or something similar which is defined as integer so you can reference this table by that identifier or id. As a side note, I recommend that every table also include a created_timestamp, created_userid, modified_timestamp and modified_userid. This is just my recommendation after 20 plus years of experience. I will discuss why in a later blog post.
The goal is to browse through the list of entries in a combo box, have the first entry defined as new genre, new category, or new whatever you are listing in your combo box. When you select this new item, your code will see it is the new entry (from the hardcoded id for the new item) provide a message prompt to enter a new genre, category, etc., capture that message text, and fire off an ajax request to insert that value into your reference table, then reload your store so when you click on the combo again your entry is there. Life is Wonderful.
What steps are involved?
1. Your application should be using a remote store rather than a simple store so you are querying your database.
2. Your application must be set up to provide the message prompt and react to that prompt.
3. You must define a cfc to insert a record into your database. But first, you must determine whether you set your table to auto increment or not. If it is not defined to auto increment then you must first query your database, select max(id) from your table, save the max id and increment it by one so you can then insert this new id into the table along with your value.
If you have auto increment defined, then you can insert the value right into the table without getting the maxid first. In a busy environment, you always want auto increment turned on. Otherwise, someone else may be inserting at the same time you are, they have already derived the same max id as you, when you go to insert your row you may get a failure as that key already exists because the other transaction inserted the record before you did. So, I recommend turning auto increment on.
Auto increment is a great term but in SqlServer it is actually called the Identity Specification. If you go into SqlServer, check your index, verify Identity Specification is set to YES, IS IDENTIFY = YES, Identity Increment is set to 1 or whatever step you prefer and Identity Seed (starting value) is set to your desired number or 1.
Here is some great information on auto incrementing indexes and it discusses how this is done with SqlServer, Mysql, Access, Oracle, etc. www.w3schools.com/sql/sql_autoincrement.asp.
Let's take a look at the combo box defintion for the form.
First we add a listener to the combo box. Then we check the index of the item selected is equal to the first item. Extjs starts at base 0 and the very first record we have stored in our database is defined as New Genre. If this is the item selected we provide a message prompt asking what new genre should be added.
If the ok button is selected, we fire off an Ext.Ajax.request to execute an cfc to insert the row. The new genre is entered as text in the message prompt, we set the data variable equal to text, and then pass the data variable as a param to the cfc. We then check the response and if we were successful we send a message that the insert was successful otherwise we send a message that the update failed.
Here is the addgenres function inside the cfc. Note that I did not need to select max id prior to the insert as the database has auto increment set to 1.
INSERT
INTO genres
(GENER_NAME)
Values
('#arguments.genre#')
This whole process was made easier by searching through the ExtJS examples, examples shown in Learning ExtJS 3.0 and 3.2 by Shea Frederick and examples in the ExtJS Cookbook by Jorge Ramon. If you do not own them you should go to PacktPub and buy them. They will provide many examples that show you how to do almost anything you want to do with ExtJS 3 series.
I started working with an ExtJS 4.0 Grid linked to a form example and miraculously got in working. I say miraculously because I believed with the changes to version 4 this would not be possible. But it was. The internal workings of ExtJS changed slightly but nonetheless I got it to work.
In my previous cell and row editor examples, after an insert, I check the response, get the insert_id or the id for the new row and insert it into the data before I load the grid. Here is the code:
In the CRUD example, you do not need to query the response to load the grid. ExtJS does this automatically by grabbing the row you pass back and inserting it into the grid and form. Pretty cool.
Well what did I change in the writer example to get it to work with ColdFusion?
First, I had to add an application.cfc. I kept debugging my code and seeing in my firebug html response that ColdFusion kept connecting to the ColdFusion administrator. Apparently every ColdFusion application requires the application cfc or it dies. I have seen this in numerous other jquery demos where I was using ColdFusion. So first, copy an application CFC, change what is necessary like the application name, and the "#request.dsn#" and paste it in the examples folder.
In this example, I am assuming you are modifying the code in the examples folder and working out of the examples folder. So go to the examples folder in ExtJS 3.2, look for the writer folder and click on that. We will be working with the files inside this folder. We will rename all of the files we need to touch from the file name to the filenamecfm so we can distinguish our changes.
There are four files we need to change in this example:
The form javascript file: UserForm.js which was renamed to UserFormcfm.js
The grid javascript file: UserGrid.js which was renamed to UserGridcfm.js
The writer javascript file: writer.js which was renamed to writercfm.js
The writer html file: writer.html which was renamed to writer.cfm
I also Added the UsersExt.cfc to execute the queries to read, edit, add and remove the data rows.
Let's take a look at each one and start with the UserForm.js file. Here the only thing I needed to change was the name of the data fields. Since ColdFusion, the way I coded it, returns uppercased names, I changed email to EMAIL, firstname to FIRSTNAME AND lastname to LASTNAME. Those are all the changes needed in this file.
/*!
* Ext JS Library 3.2.0
* Copyright(c) 2006-2010 Ext JS, Inc.
* licensing@extjs.com
* http://www.extjs.com/license
*/
Ext.ns('App', 'App.user');
/**
* @class App.user.FormPanel
* A typical FormPanel extension
*/
App.user.Form = Ext.extend(Ext.form.FormPanel, {
renderTo: 'user-form',
iconCls: 'silk-user',
frame: true,
labelAlign: 'right',
title: 'User -- All fields are required',
frame: true,
width: 500,
defaultType: 'textfield',
defaults: {
anchor: '100%'
},
// private A pointer to the currently loaded record
record : null,
/**
* initComponent
* @protected
*/
initComponent : function() {
// build the form-fields. Always a good idea to defer form-building to a method so that this class can
// be over-ridden to provide different form-fields
this.items = this.buildForm();
// build form-buttons
this.buttons = this.buildUI();
// add a create event for convenience in our application-code.
this.addEvents({
/**
* @event create
* Fires when user clicks [create] button
* @param {FormPanel} this
* @param {Object} values, the Form's values object
*/
create : true
});
// super
App.user.Form.superclass.initComponent.call(this);
},
/**
* buildform
* @private
*/
buildForm : function() {
return [
{fieldLabel: 'Email', name: 'EMAIL', allowBlank: false, vtype: 'email'},
{fieldLabel: 'First', name: 'FIRSTNAME', allowBlank: false},
{fieldLabel: 'Last', name: 'LASTNAME', allowBlank: false}
];
},
/**
* buildUI
* @private
*/
buildUI: function(){
return [{
text: 'Save',
iconCls: 'icon-save',
handler: this.onUpdate,
scope: this
}, {
text: 'Create',
iconCls: 'silk-user-add',
handler: this.onCreate,
scope: this
}, {
text: 'Reset',
handler: function(btn, ev){
this.getForm().reset();
},
scope: this
}];
},
/**
* loadRecord
* @param {Record} rec
*/
loadRecord : function(rec) {
this.record = rec;
this.getForm().loadRecord(rec);
},
/**
* onUpdate
*/
onUpdate : function(btn, ev) {
if (this.record == null) {
return;
}
if (!this.getForm().isValid()) {
App.setAlert(false, "Form is invalid.");
return false;
}
this.getForm().updateRecord(this.record);
},
/**
* onCreate
*/
onCreate : function(btn, ev) {
if (!this.getForm().isValid()) {
App.setAlert(false, "Form is invalid");
return false;
}
this.fireEvent('create', this, this.getForm().getValues());
this.getForm().reset();
},
/**
* onReset
*/
onReset : function(btn, ev) {
this.fireEvent('update', this, this.getForm().getValues());
this.getForm().reset();
}
});
The next file we will change is the UserGrid.js file. Here again I changed email, lastname and firstname to upper case and that is all that was necessary.
/*!
* Ext JS Library 3.2.0
* Copyright(c) 2006-2010 Ext JS, Inc.
* licensing@extjs.com
* http://www.extjs.com/license
*/
Ext.ns('App', 'App.user');
/**
* App.user.Grid
* A typical EditorGridPanel extension.
*/
App.user.Grid = Ext.extend(Ext.grid.EditorGridPanel, {
renderTo: 'user-grid',
iconCls: 'silk-grid',
frame: true,
title: 'Users',
height: 300,
width: 500,
style: 'margin-top: 10px',
initComponent : function() {
// typical viewConfig
this.viewConfig = {
forceFit: true
};
// relay the Store's CRUD events into this grid so these events can be conveniently listened-to in our application-code.
this.relayEvents(this.store, ['destroy', 'save', 'update']);
// build toolbars and buttons.
this.tbar = this.buildTopToolbar();
this.bbar = this.buildBottomToolbar();
this.buttons = this.buildUI();
// super
App.user.Grid.superclass.initComponent.call(this);
},
/**
* buildTopToolbar
*/
buildTopToolbar : function() {
return [{
text: 'Add',
iconCls: 'silk-add',
handler: this.onAdd,
scope: this
}, '-', {
text: 'Delete',
iconCls: 'silk-delete',
handler: this.onDelete,
scope: this
}, '-'];
},
/**
* buildBottomToolbar
*/
buildBottomToolbar : function() {
return ['@cfg:', '-', {
text: 'autoSave',
enableToggle: true,
pressed: true,
tooltip: 'When enabled, Store will execute Ajax requests as soon as a Record becomes dirty.',
toggleHandler: function(btn, pressed) {
this.store.autoSave = pressed;
},
scope: this
}, '-', {
text: 'batch',
enableToggle: true,
pressed: true,
tooltip: 'When enabled, Store will batch all records for each type of CRUD verb into a single Ajax request.',
toggleHandler: function(btn, pressed) {
this.store.batch = pressed;
},
scope: this
}, '-', {
text: 'writeAllFields',
enableToggle: true,
tooltip: 'When enabled, Writer will write *all* fields to the server -- not just those that changed.',
toggleHandler: function(btn, pressed) {
store.writer.writeAllFields = pressed;
},
scope: this
}, '-'];
},
/**
* buildUI
*/
buildUI : function() {
return [{
text: 'Save',
iconCls: 'icon-save',
handler: this.onSave,
scope: this
}];
},
/**
* onSave
*/
onSave : function(btn, ev) {
this.store.save();
},
/**
* onAdd
*/
onAdd : function(btn, ev) {
var u = new this.store.recordType({
FIRSTNAME : '',
LASTNAME: '',
EMAIL : ''
});
this.stopEditing();
this.store.insert(0, u);
this.startEditing(0, 1);
},
/**
* onDelete
*/
onDelete : function(btn, ev) {
var index = this.getSelectionModel().getSelectedCell();
if (!index) {
return false;
}
var rec = this.store.getAt(index[0]);
this.store.remove(rec);
}
});
The next file I changed was the writer.js file. Here I needed to change the calls to the php urls to my urls and add a method=getUsers or method=addUser or method=editUser or method=removeUser depending on the action the action that is requested.Here are the changes I just described:
read: 'UsersExt.cfc?method=getUsers',
//Add a row
create: 'UsersExt.cfc?method=addUser',
//Update a row
update: 'UsersExt.cfc?method=editUser',
//Delete a row
destroy: 'UsersExt.cfc?method=removeUser'
I then needed to change the json reader and change the totalProperty to DATASET, root to ROWS, id to ID, messageProperty to MESSAGE and successProperty to SUCCESS as this is what my CFC returns.
var reader = new Ext.data.JsonReader({
totalProperty:'DATASET',//This is how many total records are there in the set.
root:'ROWS',//The Root of the data.
proxy:proxy,
id:'ID',
messageProperty: 'MESSAGE',
successProperty: 'SUCCESS'
//totalProperty: 'total',
//successProperty: 'success',
// idProperty: 'id',
//root: 'data',
//messageProperty: 'message' // <-- New "messageProperty" meta-data
}, [
{name: 'ID'},
{name: 'EMAIL', allowBlank: false},
{name: 'FIRSTNAME', allowBlank: false},
{name: 'LASTNAME', allowBlank: false}
]);
// The new DataWriter component.
var writer = new Ext.data.JsonWriter({
encode: true,
writeAllFields: false
});
Please pay attention to the uppercasing of these fields. I also changed the email, firstname and lastname fields to uppercase where I define the fields for the reader as well further down in the code where I am defining my userColumns. Notice that i changed the dataIndex names to uppercase as well.
/*!
* Ext JS Library 3.2.0
* Copyright(c) 2006-2010 Ext JS, Inc.
* licensing@extjs.com
* http://www.extjs.com/license
*/
// Application instance for showing user-feedback messages.
var App = new Ext.App({});
// Create HttpProxy instance. Notice new configuration parameter "api" here instead of load. However, you can still use
// the "url" paramater -- All CRUD requests will be directed to your single url instead.
var proxy = new Ext.data.HttpProxy({
api: {
//Read the table and pass back an array of data
read: 'UsersExt.cfc?method=getUsers',
//Add a row
create: 'UsersExt.cfc?method=addUser',
//Update a row
update: 'UsersExt.cfc?method=editUser',
//Delete a row
destroy: 'UsersExt.cfc?method=removeUser'
}
});
// Typical JsonReader. Notice additional meta-data params for defining the core attributes of your json-response
var reader = new Ext.data.JsonReader({
totalProperty:'DATASET',//This is how many total records are there in the set.
root:'ROWS',//The Root of the data.
proxy:proxy,
id:'ID',
messageProperty: 'MESSAGE',
successProperty: 'SUCCESS'
//totalProperty: 'total',
//successProperty: 'success',
// idProperty: 'id',
//root: 'data',
//messageProperty: 'message' // <-- New "messageProperty" meta-data
}, [
{name: 'ID'},
{name: 'EMAIL', allowBlank: false},
{name: 'FIRSTNAME', allowBlank: false},
{name: 'LASTNAME', allowBlank: false}
]);
// The new DataWriter component.
var writer = new Ext.data.JsonWriter({
encode: true,
writeAllFields: false
});
// Typical Store collecting the Proxy, Reader and Writer together.
var store = new Ext.data.Store({
id: 'user',
proxy: proxy,
reader: reader,
writer: writer, // <-- plug a DataWriter into the store just as you would a Reader
autoSave: true // <-- false would delay executing create, update, destroy requests until specifically told to do so with some [save] buton.
});
// load the store immeditately
store.load();
////
// ***New*** centralized listening of DataProxy events "beforewrite", "write" and "writeexception"
// upon Ext.data.DataProxy class. This is handy for centralizing user-feedback messaging into one place rather than
// attaching listenrs to EACH Store.
//
// Listen to all DataProxy beforewrite events
//
Ext.data.DataProxy.addListener('beforewrite', function(proxy, action) {
App.setAlert(App.STATUS_NOTICE, "Before " + action);
});
////
// all write events
//
Ext.data.DataProxy.addListener('write', function(proxy, action, result, res, rs) {
App.setAlert(true, action + ':' + res.message);
});
////
// all exception events
//
Ext.data.DataProxy.addListener('exception', function(proxy, type, action, options, res) {
if (type === 'remote') {
Ext.Msg.show({
title: 'REMOTE EXCEPTION',
msg: res.message,
icon: Ext.MessageBox.ERROR,
buttons: Ext.Msg.OK
});
}
});
// A new generic text field
var textField = new Ext.form.TextField();
// Let's pretend we rendered our grid-columns with meta-data from our ORM framework.
var userColumns = [
{header: "ID", width: 40, sortable: true, dataIndex: 'ID'},
{header: "Email", width: 100, sortable: true, dataIndex: 'EMAIL', editor: textField},
{header: "First", width: 50, sortable: true, dataIndex: 'FIRSTNAME', editor: textField},
{header: "Last", width: 50, sortable: true, dataIndex: 'LASTNAME', editor: textField}
];
Ext.onReady(function() {
Ext.QuickTips.init();
// create user.Form instance (@see UserForm.js)
var userForm = new App.user.Form({
renderTo: 'user-form',
listeners: {
create : function(fpanel, data) { // <-- custom "create" event defined in App.user.Form class
var rec = new userGrid.store.recordType(data);
userGrid.store.insert(0, rec);
}
}
});
// create user.Grid instance (@see UserGrid.js)
var userGrid = new App.user.Grid({
renderTo: 'user-grid',
store: store,
columns : userColumns,
listeners: {
rowclick: function(g, index, ev) {
var rec = g.store.getAt(index);
userForm.loadRecord(rec);
},
destroy : function() {
userForm.getForm().reset();
}
}
});
});
Our next step is to modify the writer.html file and save it as writer.cfm. Here we modify the javascript files to reference the files we have just changed:
UserFormcfm.js
UserGridcfm.js
writercfm.js
Here is the code:
Grid with DataWriter Example
Ext.data.DataWriter Example
This example shows how to implement a Writer for your Store. A Writer-enabled Store frees you from having to manually compose Ajax requests
to perform CRUD actions on a Store.
The HttpProxy plugged into the store in this example uses the new api configuration instead of an url.
A simple MVC-like php backend has been created for this example which simulates a database by storing records in $_SESSION. See the file /remote/app/controllers/users.php. You may have to configure
your web-server to allow scripts to be executed in the /examples directory.
var proxy = new Ext.data.HttpProxy({
api: {
read: 'UsersExt.cfc?method=getUsers',//Our URL for reading the grid data
create: 'UsersExt.cfc?method=addUser',//For Adding a new User (future implementation)
update: 'UsersExt.cfc?method=editUser',//When a User is updated
destroy: 'UsersExt.cfc?method=removeUser'//When a User is updated
}
});
Take note of the requests being generated in Firebug as you interact with the Grid and Form.
An Error has been simulated on the server-side: Attempting to update a record having ODD-numbered id will generate this errror.
Responses from the update action will have successProperty === false along with a message. This error can be handled by
listening to the "exception" event upon your Store.
Note: This new "exception" event supercedes the old loadexception event which is now deprecated.
And finally we create the CFC. Here is the code for the ExtUsers CFC:
SELECT
UserID as ID, Email, FirstName, LastName
FROM
Users
ORDER BY UserID Asc
UPDATE Users
Set Email = '#stcData.EMAIL#',
LastName = '#stcData.LASTNAME#',
FirstName = '#stcData.FIRSTNAME#'
WHERE UserID = #Arguments.id#
SELECT
UserID as ID, Email, FirstName, LastName
FROM
Users
WHERE
UserID = #Arguments.id#
Insert into Users
( Email, FirstName, LastName
)
Values
( '#stcData.EMAIL#', '#stcData.FIRSTNAME#', '#stcData.LASTNAME#'
)
SELECT UserID AS ID, Email, FirstName, LastName
FROM Users
WHERE (Email = '#stcData.EMAIL#') AND (LastName = '#stcData.LASTNAME#') AND (FirstName = '#stcData.FIRSTNAME#')
Delete from Users
WHERE UserID = #Arguments.id#
Now an important part of this application is your database. I am assuming, that you have a SqlServer, Derby, Access or MySql database with a table named Users with the following fields defined:
UserID defined as Integer or int
FirstName defined as Charachter 50 or nvarchar(50)
LastName defined as Charachter 50 or nvarchar(50)
Email defined as Character 50 or nvarchar(50)
Now all you need to do is go into firefox, type in http://127.0.0.1:8500/ext-3.2.0/examples/writer/writer.cfm (be sure to change ext-3.2.0 to whatever you have this file named in your root directory) and it should work. If not, check firebug. Happy coding!