Ext Application Framework
From BriansWiki
I have been working with Ext.js for about a year now and I think I finally have a pretty good model established for full javascript UI application. The idea had its genesis in an Ext training I attended when the last exercise was to construct an sample Ext application. This struck a chord with me and I have been working on it for a while. The goal was to come up with a good design and then "Templatize" it so we can churn out Javascript apps using the Code Generation techniques I learned 3 years ago at CFUnited. Here are the main pieces of the app:
[edit] App Admin Setup
[edit] Define your application
Every application has to have an App ID; go to the regional application admin app located here:
http://devregionalintranet.cny.twcable.com/programs/admin/applications.cfm and select the add button on the detail div. You will need to create a unique TLA (Three Letter Acronym - its a good idea to search the table before you decide on one). Enter a homepage (usually "programs/<yourAppName>"). This is used by the Applications dropdown to launch your app so make sure you have an index page. After you press OK, remember the index for the new record since you will need to add it to the Project Info page below.
[edit] Add the index page to pages table
While you are in the admin, select the 'pages' button from the toolbar and add an entry for your index page. If you are creating the main application it should be called "index.cfm" otherwise it will be the <tableName>.cfm. Be sure and check the 'isWrapper' check box so it gets wrapper-ed appropriately. One of the nice things about an ext driven UI is that you really only have one form so you don't need to enter any of the other pages.
[edit] Create Folders
You only need a root project folder and a "generated" folder but you might as well create all the directories while you are at it:
- Root: This should contain the Application.cfc, index and action pages for your main app and any maintenance pages. Your application folder needs to have the following sub folders:
- components - all the .cfcs (except the Application.cfc).
- generated - the generated code (not in production).
- includes - the TLA_menu.cfm file.
- reports - the List reports and any custom reports for your app.
- scripts - all the .js forms, data stores and app files.
[edit] Project Info
[edit] Define the Project Metadata
Go to the main extTemplate page located here: http://devregionalintranet.cny.twcable.com/extTemplate and enter the following:
- Project Folder: This is usually the short application name but it should also be the same as your apps folder beneath Q:\Inetpub\regionalIntranet\programs.
- Project NameSpace: This is a javascript variable prefix for all your app's objects - it should be unique across the site. I have been prefixing the app name with "N2" to designate that it is regional but this is only needed if there is already a Division version of the app.
- Project TLA: Your project's unique Three Letter Acronym - all upper case by convention.
- DSN Name: This is the Cold Fusion Data source the template generator uses to get your table's metadata and it is also set in the cfc's queries.
- Table Name: Your table that is the base for the detail, list and search forms. By convention it should start with "TLA_".
- Index Name: The table's primary key, by convention the singular version of the table name minus the TLA and with an "ID" suffix e.g. if your table is called "TLA_myRecords" then the index should be "myRecordID".
- Link / Sort By Name: This the default sort field for your table (its not used as a link anymore).
- App ID: The ID your app was assigned in the Regional Applications admin (the first step above).
- Output Path: The physical path on the web server - by convention we have been using "generated" off the root of your application folder but it could be an EXISTING folder anywhere; if it doesn't exist the generator will throw an error.
[edit] Run the Generator
If you are creating the main app, select the "Create All" link on the bottom of the Project info page. Running all the templates takes a few seconds but eventually you should see a new page with the DAO's code on it (the first template). The next step is to copy the generated files from the server to your local perforce folder so we can keep them under source control.
This is an explanation of all the pages that extTemplate creates:
[edit] CFC DAO
This is the same as the extTemplate. It is the "CRUD" for your table and is returned as an object by the gateway's "get" method. You need one of these for every table in your app.
[edit] CFC Gateway
This is the same as the extTemplate but it has a new function "getAllForGrid" that returns data in JSON format. The Gateway is usually created in the Application.cfc and is the primary interface for your table's data. You need one of these for every table in your app.
[edit] Ext dataStore
This is a javascript class that maps the Coldfusion style JSON data into an object that Ext can read.
[edit] Detail Form
This will create a JavaScript detail form for the table that you referenced in the code generator 'Project Info' form.
[edit] Action Page
This is the same as the ctTemplate action page, its purpose is to take the results of the form's submit method and set the values in the DAO object (the DAO knows if it should update or create) and then save the record.
[edit] Search Form
This will create a JavaScript search form for the table that you referenced in the code generator 'Project Info' form.
[edit] List Form (Sortable Table)
This will create a JavaScript grid form for the table that you referenced in the code generator 'Project Info' form.
[edit] List Report
This is the same export report as in the cfTemplate; a csv version of the search results.
[edit] Index Page
For Ext, the index simply creates the application object and renders it to the Regional Intranet's main div. It also includes all the JavaScript needed for the app except for the common scripts that are included in the Application.cfc. If you have a secure app, you can use the App Alert Manager to display a warning message.
[edit] Request Handler Script
You will need one of these per project, all of the code is generic except for the post handler at the top of the file. I usually run this and copy the switch statements for the table you are generating into the common request handler for the project.
[edit] Application cfc
This handles the security for your application and includes the main app's javascript. It should not include the maintenance forms since this would just slow things down and it introduces naming conflicts. I usually run this for every maintenance form and copy the create object references in the application start and request start functions so I get the syntax correct.
[edit] Application js
This is the main class for the JavaScript application, it defines the search, list and detail forms and has the event handlers for most operations.
[edit] Menu
The menu is a cfm file that creates a JavaScript menu dynamically so we can turn menu items on and off based on the user's permissions. There should be only one per application and it should be referenced by the main and maintenance index pages.
[edit] Support Files
The application.cfc references the following common files:
- /scripts/CFQueryReader.js - this converts Adobe JSON to Ext format.
- regionalIntranet.programs.admin.components.pagesGateway - this is used by the Application cfc to determine if a page is wrappered or not (the "lNoWrapper" list replacement).
- regionalIntranet.programs.components.userSecurity - this is used to get the user roles and
permissions.
[edit] Reports
Here is an example tree-based report selector that loads a reports in a new tab.
[edit] Testing
Once all the files have been moved to the appropriate folders in your local Perforce folders, push them to the dev server and navigate to the index.cfm. If everything is perfect you should see your app but if not here's the common problems.
[edit] Trouble Shooting
- Nothing happens at all and you get an "Ext is not defined" error.
- You didn't add the index to the admin "pages" table or you did add it and forgot to check the "isWrapper" checkbox.
- You throw a bunch of errors and you see two application menus.
- There is a JavaScript error somewhere; check firebug to see where the error occurred or start IE and enable debugging to bring you to the source of the error, if it is in your main js application file you will have to go over it line by line looking for errors.
- There are no javaScript errors but you still see 2 application menus and nothing else.
- Sometimes an error on a form is not obvious; turn on "break on all errors" to confirm that there is a problem then remove all the forms one by one till your app loads to determine which form has an error and then remove components from that form till you find the error.
- Your data store doesn't throw an error but you don't see any data.
- Check firebug to confirm that the component is returning data; if you see header and footer code too, the problem is most likely that the component you are calling is not within the root of your application. The fix is you will need to add it to the N2Intranet root Application.cfc's lNoWrapper list. This happens when you are using shared components like "Users" or "Groups" located in "Programs\Components"
- Everything works in Firefox but not in IE
- You left a comma at the end of a list of properties like this:
items:[{
title: 'Search Results',
id:'listPortlet',
layout:'fit',
items: this.listGrid,
autoHeight: true,
collapsible: true
},{
title: 'Details',
id:'detailsPortlet',
layout:'fit',
items: this.detailForm,
height: 300,
collapsible: true,
}]
Do you see it? the problem is the last "collapsible: true,".
- List grid works on Dev but is empty on Live.
- Null values break the list grid and nulls are handled differently on Dev than on live. Check in the JSON and look for Nulls and make sure your app handles them appropriately either by adding a default value in the init function of the DAO or using a transform in the data store to convert to an empty string.
[edit] Simple Store
Here is an example of a simple store for testing:
store: new Ext.data.SimpleStore({
fields: ['abbr', 'state'],
data : Ext.exampledata.states // from states.js
}),
The referenced 'states.js' data file looks like this:
Ext.namespace('Ext.exampledata');
Ext.exampledata.states = [
['NH', 'New Hampshire', 'Granite State'],
['NJ', 'New Jersey', 'Garden State'],
['NM', 'New Mexico', 'Land of Enchantment'],
['NY', 'New York', 'Empire State']
];
Or even simpler:
store: new Ext.data.SimpleStore({
fields: ['abbr', 'state'],
data : [
['NH', 'New Hampshire', 'Granite State'],
['NJ', 'New Jersey', 'Garden State'],
['NM', 'New Mexico', 'Land of Enchantment'],
['NY', 'New York', 'Empire State']
]
}),
NOTE: to set the default value of a ComboBox, set the 'value' property to the index of your default item.
[edit] Encrypted Fields
Here is a page that describes show how to use encrypted fields in the Ext Template.
[edit] File Upload
There is a detailed description of how to setup a file upload form located here.
[edit] Grids
[edit] Logging (History)
- This page shows how to add history to an Ext template App.
[edit] Radio Groups
Here's how to listen to a selection of a radio group (in case you want to validate or save it in a hidden field):
items: {
xtype: 'radio',
name: 'isActive',
id: 'isActive_yes',
inputValue: true,
fieldLabel: 'Yes',
listeners: {'check': this.setIsActive}
}
There are 2 things going on here, I added an ID to the radio button and then a listener for the 'check' event. Here is the referenced function:
// set hidden field values for isActive group
setIsActive: function(isActive_group) {
var cmpActive = Ext.getCmp('edit_isActive');
if (isActive_group.name == 'isActive_no') {
cmpActive.setValue(0);
} else {
cmpActive.setValue(1);
}
},
The function gets handed the radio button and I check the name to see if it is yes or no, the interesting part is I set the id; the name has to be the same on all the radio buttons for it to work as a group. It seems to work however so what the hey!
[edit] Linked Combo Boxes
[edit] Remote ComboBox Stores
If you have a comboBox that has a list of more than a 100 items, you will probably want to use the comboBox in "remote" mode. This means that the data will be loaded on demand which speeds up loading the form tremendously. Here's what a typical remote comboBox definition looks like:
NOTE: this approach assumes that the display field value for your combo box is in the record that gets passed to it on row select. If this is not the case, or if using hidden fields to pass data makes you feel dirty, you can the remote combobox redux approach.
new Ext.form.ComboBox({
id: 'assignedID_data',
fieldLabel: 'Assigned',
hiddenName:'assignedName',
hiddenId:'edit_assignedName',
name:'edit_assignedName',
store: new N2I.Programs.DataStore({
url: '/programs/components/usersGateway.cfc',
baseParams: {
method: 'getAllForGrid',
fieldlist: "userID as assignedID,lastname + ', ' + firstname as assignedName",
returnformat:'json',
queryformat:'column',
queryField:'lastname',
sort:'lastname',
limit:50
},
fields: [{name: 'assignedID', type: 'int'},'assignedName'],
reader: new Ext.data.CFQueryReader({id:'USERID'},[
// set up the fields mapping into the json
{name: 'assignedID', mapping: 'ASSIGNEDID', type:'string'},
{name: 'assignedName', mapping: 'ASSIGNEDNAME', type:'string'}
])
}),
valueField:'assignedID',
displayField:'assignedName',
typeAhead: true,
mode: 'remote',
triggerAction: 'all',
emptyText:'Enter name to search...',
selectOnFocus:true,
allowBlank:false,
width:125,
minChars:1
}),{
xtype:'hidden',
hideLabel: true,
hidden: true,
name: 'assignedID',
id: 'edit_assignedID'
}
NOTES:
- Add hidden Value field
I may just not understand this properly but I couldn't get a combobox in remote mode to load the displayField contents from the underlying form's data store. To work around this problem, I create a hidden field that accepts the persisted comboBox's value and then create an event to update this hidden field on change. Sample events to load the remote comboBox exist in the template page as comments, so you just need to un-comment them and make sure they refer to the correct fields. Here's a sample:
// in the events section of the detail form add this:
Ext.getCmp('assignedID_data').on('select', this.setAssignedID, this);
// In the Custom Methods section add this:
// set hidden field values for remote mode combo boxes
setAssignedID: function(assignedID_data) {
var oAssigned = Ext.getCmp('edit_assignedID');
oAssigned.setValue(assignedID_data.value);
},
- Remove Store Load
Unlike the local mode comboBox, you don't need to load the store's data on form load, so if you generated your form with extTemplate you should remove the "store.load" section for this control in the detail form.
[edit] Lookup Behavior
- List limit
The lookup control will show the first "n" number of records where n is the "limit" property defined in the baseParams section of the data store; you may want to change this to fit your app.
- Last Search
The control will remember the last search and display that when you select the dropdown button. This is stored as a session variable so it will persist as long as the session (10 hours by default). If you want to change this behavior you will need to modify the "GetAllForGrid function that your control is calling.
- Minimum characters
There is a "minChars" property that controls how many characters you need to type before the control does a lookup. This is set to 1 in the example, you may need to increase this if a lookup takes too long.
- Query Delay
The "queryDelay" parameter is the length of time in milliseconds to delay between the start of typing and sending the query to filter the dropdown . The defaults is 500 if mode = 'remote' or 10 if mode = 'local'.
[edit] Using Joins in a Data Store
Most of the time you need to return results from multiple tables in a single query; here's how to make it work:
- Add joins to getAllForGrid
Add any lookup tables to the query like this:
<cfquery name="qList" datasource="TWCRegional_Intranet" > select #preserveSingleQuotes(arguments.fieldList)# from userGroups left outer join users on users.userID = userGroups.userID left outer join groups on groups.groupID = userGroups.groupID left outer join appGroups on appGroups.groupID = groups.groupID <cfif arguments.query neq ""> where (#arguments.queryField# like <cfqueryparam value="#arguments.query#%" cfsqltype="CF_SQL_VARCHAR">) <cfelse> where #preserveSingleQuotes(decodedWhere)# </cfif> <cfif arguments.sort neq ""> order by #arguments.sort# #arguments.dir# </cfif> </cfquery>
- Prefix field names with table name
Since getAllForGrid is an abstract function, you can't be sure of the "where" clause that may be used, so to be safe, we add the table prefix to the field name in all our where clauses and field lists.
fieldlist: "userGroups.userGroupID," + "userGroups.userID," + "users.lastname + ', ' + users.firstname as userName," + "userGroups.groupID," + "groups.groupName," + "userGroups.subGroupID", whereClause: '(appGroups.appID=112)',
- Prefix search form names
Since the search function is abstract as well, you also need prefix any fields that may be ambiguous too; with comboBoxes, the search function gets the field name from the "hiddenName" property:
new Ext.form.ComboBox({
id: 'srch_groupID_data',
fieldLabel: 'groupID',
hiddenName:'userGroups.groupID',
hiddenId:'srch_groupID',
name: 'srch_groupID',
store: new N2I.Programs.DataStore({
url: '/programs/components/groupsGateway.cfc',
reader: new Ext.data.CFQueryReader({id:'groupID'},[
// set up the fields mapping into the json
{name: 'groupID', mapping: 'GROUPID', type:'string'},
{name: 'groupName', mapping: 'GROUPNAME', type:'string'}
])
}),
valueField:'groupID',
displayField:'groupName',
typeAhead: true,
mode: 'local',
triggerAction: 'all',
emptyText:'Select a group...',
selectOnFocus:true,
allowBlank:true,
width:125
})
[edit] Tabs
Most of our detail forms these days have a tabbed interface; here's how I set them up in the new Research Tracker App. (For an example of how to setup dynamic tabs that each hold a list grid check out this page). This example loads the form on the search result grid's row click event; it waits to load the tab's data until the tab click event. This example also assumes that each tab has it's own datastore but it shouldn't really matter; just change the logic that loads the data to whatever you need. The important thing is to get an Ext record object to send to the form (that's the "r" variable that is handed to the form).
NOTE: If you are hiding a grid on startup in a collapsed panel and the grid is all scrunched up when the panel is expanded you may have "over-nested-panel-syndrome". There is no cure that I know of outside of a redesign, but there is a work around detailed here.
- Add a tab object to the center region in the main application.js
region: 'center',
layout: 'fit',
items:[{
title: 'Search Results',
id:'listPortlet',
layout:'fit',
items: this.listGrid,
autoHeight: true,
collapsible: true
},
new Ext.TabPanel({
id: 'NRT_detailTabPanel',
activeTab: 0,
autoScroll:true,
height: 295,
items:[{
title: 'Details',
id:'detailsPortlet',
items: this.detailForm
}]
})
]
- In the onRowSelect event add the tab handling logic:
onRowSelect: function(sm, rowIdx, r) {
this.detailForm.getForm().loadRecord(r);
// remove any existing tabs except the detail
var oTabPanel = Ext.getCmp('NRT_detailTabPanel');
var tab;
var tabCount = oTabPanel.items.length;
for (i=1;i<=tabCount;i++) {
tab = oTabPanel.getItem(oTabPanel.items.length - 1);
if (tab.id != 'NRT_detailsTab') {
oTabPanel.remove(tab, true);
}
}
// check the researchType and load the appropriate tab
var researchType_Data = Ext.getCmp('researchTypeID_data');
if (researchType_Data.value == 1) {
// create an equipment form
oForm = new N2ResearchTracker.EquipmentForm({requestData: r});
} else {
// create an payments form
oForm = new N2ResearchTracker.PaymentForm({requestData: r});
}
oTabPanel.add(oForm);
},
- Add an event for the tab click - this is added after the initComponent call on the detail form:
Ext.getCmp('researchTypeID_data').on('select', this.createResearchTab, this);
- Add a Tab click handler function - this assume that you have dynamic tabs that are determined by an option field on the main tab's form
createResearchTab: function(researchType_Data) {
var oTabPanel = Ext.getCmp('NRT_detailTabPanel');
// remove any existing tabs except the detail
var tab;
var tabCount = oTabPanel.items.length;
for (i=1;i<=tabCount;i++) {
tab = oTabPanel.getItem(oTabPanel.items.length - 1);
if (tab.id != 'NRT_detailsTab') {
oTabPanel.remove(tab, true);
}
}
// create a dummy record so the form load doesn't complain
// (you only need to add the fields that are in the "loadStore" where clause)
var newRecord = Ext.data.Record.create([
{name:'researchRequestID', type:'int'},
{name:'isActive', type:'boolean'}
]);
var newRecord = new newRecord({});
if (researchType_Data.value == 1) {
// create an equipment form
oForm = new N2ResearchTracker.EquipmentForm({requestData:newRecord});
} else {
// create an payments form
oForm = new N2ResearchTracker.PaymentForm({requestData:newRecord});
}
oTabPanel.add(oForm);
},
- Add "onShow" and "onLoad" events to setup data store in each tab detail form:
// wait till the tab is shown before loading the data
this.on('show', this.loadStore, this);
// wait till the store is loaded till loading the form
this.store.on('load', this.loadForm, this);
- Add custom "loadStore" and "loadForm" methods:
// Custom Methods
loadStore: function(equipForm) {
// load this form from the local store
equipForm.store.load({params:
{whereClause:'NRT_equipmentDisputes.researchRequestID=' + this.requestData.get('researchRequestID') + ' and NRT_equipmentDisputes.isActive=1'}
});
},
loadForm: function(equipStore){
//load the top record (there should only be one)
var curRecord = equipStore.getAt(0);
if (typeof curRecord != 'undefined') {
this.getForm().loadRecord(curRecord);
}
},
[edit] Form Validation
In Ext, there are explicit parameters that handle "allowBlank", "minLength" and "maxLength" or you can use a custom validator function but the easiest way to validate common fields is to use a vType, some basic ones are included by default:
- alpha - only allow letters
- alphanum - allow only letters or numbers
- email - allow a valid email address
- url - allow a valid url
To use one of these, simply include a vtype property in your field definition:
vtype: 'alpha';
We have created a number of additional vtypes that are part of the devregionalIntranet/scripts/formVal.js file:
vtype:'time'; timeText: 'Time must be in the format "12:34 PM".', vtype:'money'; moneyText: 'Number must be in the format "10.00" or "-10.00.', vtype:'acctno' acctnoText: 'Account number must be in the format "9999999-99" or "999999999".', vtype:'phone' phoneText: 'Phone number must be in the format "(123) 456-7890" or "123-456-7890".', vtype:'int'; intText: 'Number must be in the format "123" or "-123".', vtype:'posint'; posintText: 'Number must be in the format "123".',
To use one of these, simply refer to one of these custom vtypes like this:
{
fieldLabel: 'myMoneyField',
name: 'myMoneyField',
id: 'edit_myMoneyField',
allowBlank:false,
vtype: 'money'
}
If you need to create another vtype you need to create (or Google) a regex that tests the condition, say that a account number is valid and then create another regex that is the "mask" which restricts what characters the user can enter - this can make your validation a lot easier.
[edit] Radio Buttons
There doesn't appear to be an easy way to require an input for a radio group; here is a heavy handed approach that you can add to the "save" function:
if (detailForm.isValid()) {
// check for a value in the isActive radio group
var IDvalue = null;
var IDobj = document.getElementsByName('isActive');
for ( j = 0; j < IDobj.length; j++ ) {
if (IDobj[j].checked == true) {
IDvalue = IDobj[j].value;
}
}
if (IDvalue == null) {
Ext.MessageBox.alert('Warning', 'You must select a value for the "isActive" field');
} else {
processForm({
<blah blah blah>
[edit] Handling Resizing
When you add tabs to a form, the resizing of the child panels may not work automatically. This was noted in the "over-nested-panel-syndrome" fix but that still doesn't address the problems with detail forms where we want to dynamically resize fields when the search form is collapsed or expanded. If you have an app that was generated before 9/18/09, you will need to make a couple of changes to support this feature:
[edit] anchor your columns
I know this doesn't sound right, but in order to allow the size of the fields in your columns to automatically resize, you need to "anchor" them. Luckily this is easy to do by setting the column defaults:
this.items = [{
layout: 'column',
border:false,
layoutConfig: {
forceFit:true
},
defaults:{
columnWidth: .33,
layout:'form',
width: 180,
defaultType: 'textfield',
border: false
},
items: [{
// column 1
defaults:{anchor:'100%'},
items: [{
fieldLabel: 'userID',
name: 'userID',
id: 'edit_userID',
allowBlank:false,
disabled:true
},
The critical piece here is add the "defaults:{anchor:'100%'}" property to all the columns but we have also moved all the column defaults to the parent item to make configuration (like changing the width and padding) easier. (I have added this to the template but you may want to make this change to already generated code).
[edit] add resize listeners to the app
// setup listeners for resizing
this.on('loaded',this.handleResize());
Ext.getCmp('searchPanel').on('collapse', this.handleResize, this);
Ext.getCmp('searchPanel').on('bodyresize', this.handleResize, this);
Ext.getCmp('searchPanel').on('expand', this.handleResize, this, {buffer: 500});
// if you have multiple grids within tabs you need to handle their resizing too
Ext.getCmp('listPortlet').on('collapse', this.handleGridResize, this);
Ext.getCmp('listPortlet').on('bodyresize', this.handleGridResize, this);
Ext.getCmp('listPortlet').on('expand', this.handleGridResize, this);
NOTE: We need the "buffer" on the expand event to allow the search form to render before we have resize the detail form.
This also assumes that you set an id for your search panel called "searchPanel"
[edit] add resize handler function
handleResize: function(form) {
// get a cross platform browser window height
this.height = Ext.ux.getWindowHeight();
// do the layout on the detail form
var detailForm = Ext.getCmp('detailsPortlet');
detailForm.doLayout();
// You only need to do the grid reload if it is within a tab,
// otherwise it should resize correctly by itself.
var gridStore = Ext.getCmp('users_listGrid').store;
if (gridStore.data.length) {
gridStore.reload();
}
},
// if you have multiple grids within tabs you need to handler their resizing too
handleGridResize: function(northPortlet) {
// get the active tab
var listTab = Ext.getCmp('NSA_listTabsPanel');
var tab = listTab.getActiveTab();
// set the grid height to the portet height
if (tab != null && northPortlet!= null) {
var curHeight = northPortlet.getInnerHeight() - 30;
tab.view.grid.setHeight(curHeight);
}
},
Finally, if you have multiple grids within tabs you will need to add a call to handleGridResize at the end of your 'loadListTab' function too:
// reload and resize the grid
gridStore.reload();
var portlet = Ext.getCmp('listPortlet');
this.handleGridResize(portlet);
[edit] Bugs
[edit] empty rows in grid
To fix the bug where the last page of a grid displays empty rows, add this code before the QueryConvertForgrid function call:
<!--- check for a remainder which means we don't have a full page ---> <cfset remainder = (arguments.start + arguments.limit) - qList.recordCount> <cfif remainder gt 0 and remainder lt arguments.limit> <cfset maxRows = arguments.limit - remainder> <cfset page = (arguments.start + maxRows) / maxRows> </cfif>
[edit] lame new button
The "New" button on the detail form simply reloads the page. Here's how to reset the current page and reload the new record on save.
[edit] lame reset on search
The "reset" button on the search form simple reloads the entire app - who writes this stuff!. To do a real reset replace the "history.go(0);" with this:
this.getForm().reset();
This is fixed in the template as of 10/08/09.
[edit] broken delete
If in IE7 you get a 'Error: Expected identifier, string or number' on delete, you have experienced yet another reason why I hate IE. It turns out that when you run
eval("params." + fieldList[i] + " = IDvalue"); and your fieldList[i] = "delete", IE thinks you are calling the function "params.delete()" and you are missing a parameter. To work around this annoyance, rename the hidden delete field to "deleteFlag" or something and then change any references to 'delete' in the app and action pages. This is fixed in the template as of 10/12/09.
[edit] slow paging
If you have a really big table, your loading and paging time may be really slow, this is due to the way getAllForGrid has to return all the records and then create a page that is a subset of these records with the "QueryConvertForGrid" ColdFusion function. The workaround uses the latest version (1.2) of the ext user extension CFQueryReader.js. The main advantage is the ability to return a struct that can include additional information - most importantly the overall recordCount. Here is the procedure to modify a standard extTemplate app to use this new approach.
