Grails and ExtJS Grid - A case study

On a quest to build the kick-ass grid for my Grails application, I discovered the ExtJS javascript library. Although maybe a little intimidating due to the many hooks and configuration options in the API, I was extremely impressed with the consistency and cleanliness of its architecture. So, we dive right in...

What do we need?

Create the folder under PROJECT_HOME/web-app/js/ext, and drop in ext-all.js, ext-base.js, and ext-core.js. Files are quite large, I plan to gzip and optimize the size of these at some point.
Next, create your own JS file in PROJECT_HOME/web-app/js/grid.js

The controller...

We have a model with an Account object, the Account object has a set of Transaction objects. We use the convenient GORM dynamic finder and call Transaction.findByAccount(account) to load our transactions.
Our controller uses the grails.converters utility to convert our object to JSON to be returned to our script. Notice how the JSON object is a hash which contains a "rows" key to our list of results...using this hash we can add some other data such as the account name and opening balance quite easily.

import groovy.xml.*
import grails.converters.*
import org.jsecurity.SecurityUtils


class AjaxController {
        /** get the transactions for an account, return JSON object **/
	def transactions = {
		def user = User.findByUsername(SecurityUtils.subject.principal)
		def account = Account.findById(params.account_id)
		def txs = [rows:[], title: account.description, openingBalance: account.openingBalance]

		Transaction.findAllByAccount(account, [sort:"txDate", order: "asc"]).each {
			def amount = it.amount
			if(it.debitAccount?.id?.equals(account.id)) amount = 0 - amount
			txs["rows"] << [id:it.id, date: it.txDate, amount: amount, description: it.description, 
                                          balance: account.balanceAt(it.txDate), cleared: it.cleared]
		}

		render txs as JSON
	}
	
        /** Update the transaction **/
	def updateTransaction = {
		def tx = Transaction.findById(params.id)
		tx.description = params.description
		tx.cleared = Boolean.parseBoolean(params.cleared)
		tx.save()
		render ""
	}
}

The script...

This app is for cashflow management - our grid will be a table of transactions (deposit/withdrawal), which can be edited in-place, sorted, and paged - all the things you'd expect a cool grid to do. We create a wrapper function around our Ajax call - the ajax request will build the grid on successful response.

function loadTransactions(account_id){

	Ext.Ajax.request({
		url: "/cashflow/ajax/transactions?account_id="+account_id,
		success: function (response){
               //..... we will build the grid here
               }
	   }

}

Within the Ajax response handler function, we start to build the grid. Using the ExtJS Record construct, we create a client-side representation of the Transaction:

		var Transaction = new Ext.data.Record.create([
			{name: 'id'},
			{name: 'date', type: 'date', dateFormat: 'm/d/Y'},
			{name: 'amount', type: 'float'},
			{name: 'description', type: 'string'},
			{name: 'cleared', type: 'boolean'},
			{name: 'balance', type: 'float'}
		]);

Next, we create a reader that can interpret the JSON response and map it to our object.

		var dataReader = new Ext.data.JsonReader({
			root:'rows'
		}, Transaction);

Next, we creata a data store object to wrap our data set:

		var store = new Ext.data.Store({
			reader: dataReader
		});

Next, we load the JSON data into the store

		var jsonData = Ext.util.JSON.decode(response.responseText);
		store.loadData(jsonData);

Next, we attach a listener to the store, so that when it is update it we can update our back-end:

		store.addListener("update", function(s,record,operation){
			Ext.Ajax.request({
				url: "/cashflow/ajax/updateTransaction?id="+record.get('id')+"&description="+Ext.util.Format.htmlEncode(record.get('description')) + "&amount=" + record.get('amount')
					+ "&cleared="+record.get('cleared') + "&txDate=" + record.get('date'),
				success: function(){
                                   //reader's exercise - do something cool here.
				}
			});
		});

Next, we flush out the grid's HTML content with this simple call:

Ext.get('grid-example').update("");

Next, we create the grid. Notice the rich set of options ExtJS provides for each column, and also that we use other JSON data to set the title of the grid.

		var grid = new Ext.grid.EditorGridPanel({
			store: store,
			columns: [
				{header: "Date", width: 85, sortable: true, renderer: Ext.util.Format.dateRenderer('m/d/Y'), dataIndex: 'date'},
				{id:'description', header: "Description", width: 160, sortable: true, dataIndex: 'description', editor: new Ext.form.TextField()},
				{id:'amount', header: "Amount", width: 85, sortable: true, dataIndex: 'amount', editor: new Ext.form.TextField()},
				{id:'cleared', header: "Cleared?", width: 60, sortable: true, dataIndex: 'cleared', renderer: clearedRenderer, editor: new Ext.form.Checkbox({})},
				{id:'balance', header: "Balance", width: 75, sortable: true, dataIndex: 'balance'}
	       ],
				stripeRows: true,
				autoExpandColumn: 'balance',
				height:400,
				width:800,
				title:jsonData.title + " " + jsonData.openingBalance,
				clicksToEdit:1
		});

Finally, we render the grid:

grid.render('grid-example');

Awesome!