Thursday, February 7, 2008

Javascript: Introduction to the Ext Grid Object

Grid controls get used in lots of user interfaces and thankfully, Ext makes it pretty easy to use a normally cumbersome component. It takes only a few relatively simple lines to create the control and most of them are configuration parameters that leave little room for bugs to appear later. To illustrate the point, let’s take a look at a small grid that has three populated data columns and one calculated column.

We have four columns: name, hours, rate and cost. The cost column is calculated by multiplying the rate and hours columns. This is pretty simple stuff. We’ll make it more interesting by letting you change the values of the rate and hour columns and automatically update the cost column.

Let’s first look at the grid and its children. The GridPanel class allows you to show data only. The EditorGridPanel allows you to configure a selectively editable grid control. What I mean is that you can configure a specific editor for each column in the grid. You can use any of the form package’s Field objects as an editor.The PropertyGrid is derived from the EditorGridPanel and let’s you manage a grid of key/value pairs. We will focus on the GridPanel and EditorGridPanel in this discussion.

We can see from the class diagram that a GridPanel has a ColumnModel field which is a collection of column configurations. The ColumnModel.renderer member lets you change the default rendering behavior of a column. The ColumnModel.editor member is an Ext.form.Field object, such as TextField or NumberField. GridPanel ignores the editor member, while EditorGridPanel uses the editor to allow the user to modify a cell’s contents.

So how does a user edit a cell? She clicks on it one or more times. You can set the number of clicks (single, double, etc.) during the configuration of the EditorGridPanel by setting the clicksToEdit field. The default number of clicks is two.

A GridPanel is backed by an Ext.data.Store object, which contains the data for the rows of the grid. There are several different kinds of stores for loading and managing various data sources. The relationship between a GridPanel and a Store is very tight: modify a value from the either the grid or the store and the other will immediately update.

This intimacy between the two objects is very helpful when managing calculated fields. Consider for a moment that the data is being loaded remotely, as it would be in a real application, and that the rate and hours columns are populated by the server and locally calculating the cost column when either the hours or rate columns change. There are several ways to accomplish this.

The first method is to register for the afteredit event, which fires after a cell is edited. Your event handler calculates the values of your calculated fields and saves them to the store. Remember, the grid will reflect the changes to the store so the grid will display the new cost value. A subtlety is that the store can load the cost column already populated by the server.

The second method is that you define a renderer for a calculated field and not bother to update the store. A row gets rendered whenever a cell is modified. I use this method in the example, however, I believe the first method is more useful.

Why write an example that uses an odd implementation? Both work well. The second implementation doesn’t require the calculated field to be populated by the server. I can simply calculate its value on the fly. It will perform more poorly if I have a lot of rows though. It also implies that the server doesn’t store the calculated field. The first method obviously implies that the server has a representation of the calculated field and that the grid should render more quickly because it has fewer calculations to perform.

The last thing I will mention about the Store class is that it has a kind of transaction management. You can make a series of changes and commit them by calling Store.commitChanges() or reject them by calling Store.rejectChanges(). The rejectChanges() method is handy for undoing mistakes.

Let’s look at the example application’s code now. The interesting sections have a red number in the comments and following explanation will refer to them.

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>Simple Ext EditorGrid Example</title>

<link rel="stylesheet" type="text/css" href="./ext-2.0.1/resources/css/ext-all.css"/>

<script type="text/javascript" src="./ext-2.0.1/adapter/ext/ext-base.js"></script>

<script type="text/javascript" src="./ext-2.0.1/ext-all-debug.js"></script>

<script>

function main() {

// 1. Create some dummy data data

var sampleData = [

[ 'Bob', 20, 13.45 ],

[ 'Bill', 40, 12.92],

[ 'Mike', 45, 23.02]

];


// 2. Create the Store

var store = new Ext.data.SimpleStore({

fields : [

{ name: 'name', type: 'string' },

{ name: 'hours', type: 'int' },

{ name: 'rate', type: 'float' },

{ name: 'cost', type: 'float' }

],

data: sampleData

});


// 3. Initialize the grid control

var simpleGrid = new Ext.grid.EditorGridPanel({

store: store,

columns: [{

header: 'Name', // 4. Field cannot be edited.

width: 160,

sortable: false,

dataIndex: 'name'

},

{

header: 'Hours', // 5. Field can be edited

width: 75,

sortable: false,

dataIndex: 'hours',

editor: new Ext.form.NumberField({

allowBlank: false,

allowDecimals: false

})

},

{

header: 'Rate', // 5. Field can be edited

width: 75,

sortable: false,

dataIndex: 'rate',

editor: new Ext.form.NumberField({

allowBlank: false,

allowDecimals: true

})

},

{

header: 'Cost', // 6. Calculated field

width: 75,

sortable: false,

renderer: formatCost,

dataIndex: 'cost'

}

],

clicksToEdit: 1,

stripeRows: true,

autoHeight:true,

width:500,

enableHdMenu: false, // 7. Disable the context menu

title:'Editor Grid',

renderTo: 'formDiv'

});

}


//8. Formatting function for the cost column

function formatCost(value, metadata, record, rowIndex, colIndex, store) {

var results = getCost(record);

return Ext.util.Format.usMoney(results);

}


//9. Calculates the cost

function getCost(record) {

var hours = record.get('hours');

var rate = record.get('rate');

var results = hours * rate;

return results;

}

</script>

</head>

<body>

<div style="margin: 10px 10px;">

<div id="formDiv"/>

</div>

<script>

Ext.onReady( main );

</script>

</body>

</html>

The code creates some sample array data(1) and a Store(2) to map the array into name-value pairs for each row. Then we create a new EditorGridPanel(3) with four columns: 1 non-editable(4), 2 editable(5) and 1 calculated(6). Note the dataIndex member in each column configuration that maps to a name in the Store’s configuration. This mapping allows the grid to lookup and update values in the store. We also set a formatter for the cost column. The formatCost(8)method does the calculation by calling getCost(9) and then changes its formatting using the Ext.utilFormat.usMoney() method. The formatter gets called whenever the row gets updated, including during its initial population.

It may seem like I glossed over a few important points, however, there isn’t very much to tell for this example. Most of the code is for the configuration of the EditorGridPanel and there are very few odd gotcha type things. Ok, there is one odd gotcha, but I haven’t tried it recently and don’t know if Ext 2.01 even suffers from it. The problem was that a grid would sometimes take up the entire width of the browser regardless of its container. Hardcoding the width gets around that problem.

That concludes our discussion of the Ext grid component for today. We’ll talk about it more though in the coming weeks with more interesting examples.

7 comments:

kangax said...

What happened to progressive enhancement? Is it possible to attach all this behavior on top of an existing markup (table with data)?

Andy Wilson said...

Sure, you can map a table to a store which is bound to a grid.

You could create a reader to read in the table, which sounds kind of painful. The other way you could do it would be to generate the record objects yourself, after reading in your table using whatever you like, and then add them to the store.

I'm not saying this is optimal.

Anonymous said...

Very interesting post.

You are very clean in the explanations... please go on with extjs!!! :-)

Thank's

Kenshin

Thomas said...

Can you add to your example; instead of the hard coded data, can we pull some data from a datasource?

Anonymous said...

I tried to pull the data from datasource and it works, but it give me one empty row between in another, and i have been trying to fix it. but i dont know how.

Anonymous said...

Can you put an example where you pull some data from ajax? or so? Thanks.

jeneli said...

any idea how many lines you can have in a grid. i have troube when you get to 160 records wont display the rows. any ideas? thanx

Post a Comment