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.