Monday, June 16, 2008

Grails YUI DataTable example



After I quit looking into the Ext JS library because of the license problems, I wanted to use another AJAX framework which comes bundled with nice widgets. Before I started with Ext I had already looked into The Yahoo! User Interface Library (YUI) but preferred Ext that time. So switching back to YUI makes sense... The only reason I was hesitating was because of Yahoo's possible takeover by Microsoft as it would be uncertain what would happen to YUI on the short and long term. The news that the deal between Microsoft and Yahoo seems to be off the table definitely, made my choise to use YUI easy.

Today I created an example on how to use YUI's DataTable within a Grails application.



The example is based on this simple Country domain class:



class Country {

String country

}





The Grails CountryController looks like:



class CountryController {

def index = { redirect(action: list, params: params) }

def list = {
}

def listData = {
def countryList = Country.list(params)
response.setHeader("Cache-Control", "no-store")
render(contentType: "text/json") {
totalRecords(Country.count())
records {
for (r in countryList) {
country(id: r.id, country: r.country)
}
}
}
}

}



As you can see the list action is doing nothing except rendering the list.gsp. The listData action will be called by the YUI DataTable and returns the records in JSON format together with the total count of records (totalRecords). So at last - and a big chunk of JavaScript code - we need the list.gsp to create and display the YUI DataTable:



<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main" />
<title>Country List</title>
</head>
<body>

<div id="demo">
<div id="paging"></div>
<div id="dt"></div>
</div>

<script type="text/javascript">
// Set up DataSource
var myDataSource = new YAHOO.util.DataSource('${createLink(action: 'listData')}?');
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
myDataSource.responseSchema = {
resultsList : 'records',
fields : ['id', 'country'],
metaFields : {
totalRecords: 'totalRecords'
}
};

// A custom function to translates sorting and pagination values
// into a query string the server will accept
var buildQueryString = function (state,dt) {
return "offset=" + state.pagination.recordOffset +
"&max=" + state.pagination.rowsPerPage +
"&sort=" + state.sorting.key +
"&order=" + ((state.sorting.dir === YAHOO.widget.DataTable.CLASS_ASC) ? "asc" : "desc");
};

// Custom function to handle pagination requests
var handlePagination = function (state,dt) {
var sortedBy = dt.get('sortedBy');

// Define the new state
var newState = {
startIndex: state.recordOffset,
sorting: {
key: sortedBy.key,
dir: ((sortedBy.dir === YAHOO.widget.DataTable.CLASS_DESC) ? YAHOO.widget.DataTable.CLASS_DESC : YAHOO.widget.DataTable.CLASS_ASC)
},
pagination : { // Pagination values
recordOffset: state.recordOffset, // Default to first page when sorting
rowsPerPage: dt.get("paginator").getRowsPerPage()
}
};

// Create callback object for the request
var oCallback = {
success: dt.onDataReturnSetRows,
failure: dt.onDataReturnSetRows,
scope: dt,
argument: newState // Pass in new state as data payload for callback function to use
};

// Send the request
dt.getDataSource().sendRequest(buildQueryString(newState), oCallback);
};

var myColumnDefs = [
{key: "id", label: "Id", sortable: true},
{key: "country", label: "Country", sortable: true}
];

var myTableConfig = {
initialRequest : 'max=10&offset=0&sort=id&order=asc',
generateRequest : buildQueryString,
paginator : new YAHOO.widget.Paginator({containers: "paging", rowsPerPage: 10}),
paginationEventHandler : handlePagination,
sortedBy : {key: "id", dir: YAHOO.widget.DataTable.CLASS_ASC}
};

var myDataTable = new YAHOO.widget.DataTable('dt', myColumnDefs, myDataSource, myTableConfig);

// Override function for custom server-side sorting
myDataTable.sortColumn = function(oColumn) {
// Default ascending
var sDir = "asc";

// If already sorted, sort in opposite direction
if(oColumn.key === this.get("sortedBy").key) {
sDir = (this.get("sortedBy").dir === YAHOO.widget.DataTable.CLASS_ASC) ? "desc" : "asc";
}

// Define the new state
var newState = {
startIndex: 0,
sorting: { // Sort values
key: oColumn.key,
dir: (sDir === "desc") ? YAHOO.widget.DataTable.CLASS_DESC : YAHOO.widget.DataTable.CLASS_ASC
},
pagination : { // Pagination values
recordOffset: 0, // Default to first page when sorting
rowsPerPage: this.get("paginator").getRowsPerPage()
}
};

// Create callback object for the request
var oCallback = {
success: this.onDataReturnSetRows,
failure: this.onDataReturnSetRows,
scope: this,
argument: newState // Pass in new state as data payload for callback function to use
};

// Send the request
this.getDataSource().sendRequest(buildQueryString(newState), oCallback);
};
</script>
</body>
</html>



The Grails' createLink taglib is used to define the url to retrieve the JSON data from. For the rest, the YUI DataTable configuration and paginator take care of all server side paging and sorting.

Note that I included the following YUI javascript and css files in my main.gsp:



<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.2/build/reset-fonts-grids/reset-fonts-grids.css">
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.2/build/assets/skins/sam/skin.css">
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/utilities/utilities.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/calendar/calendar-min.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/datasource/datasource-beta-min.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/datatable/datatable-beta-min.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/container/container_core-min.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.2/build/menu/menu-min.js"></script>



Off course it's also possible to install the Grails YUI plugin if you don't want to use the hosted files.

PS: a little bit of refactoring to make it more re-usable, and it would not be difficult to use in the scaffolding generation.

8 comments:

Andres Almiray said...

I suppose that something similar (datasource that is) can be done with Enums now ;-)

Anonymous said...

What kinds of problems do you have with the Ext license?

Anonymous said...

Isn't that a lot of javascript for generating a table?

Marcel Overdijk said...

@Tim

This example is a lot of JavaScript yes. You can refactor it easily in a couple of re-usable JavaScript classes/functions, so you have less JavaScript code.

You could off course also create a Grails taglib which does the work.

Unknown said...

Marcel,

timely posts! Just what I was looking for!

One thing I spotted somewhere else for returning the list of Country as a JSON structure:

def countryList = Country.list(params)
render([totalRecords:countryList.size(),rows:countryList] as JSON)

Jeff said...

I just copied this and can't get it to work. My list page doesn't display anything. I've added 33 countries to my database. Do you have a copy of this example that I can download?

Unknown said...

This is a great example, but doesn't seem to work with the latest datatable available in yui 2.6.0. Any suggestions?

Mainster said...

Where in your code are your modified scaffolding templates located?

Can you handle multiple similar YUI controls on the same page using scaffolding?

A simple use case: two YUI sliders or calendar controls on the same page...

I am also looking into Grails + YUI integrations and I have run into problems for these kinds of use cases. Perhaps YUI 3.x will be better suited for this?