I was fiddling with YUI 2 and Grails and needed a rich table in list screen with search functionality. YUI datatable can talk json and Grails can reply in it, so with some changes to the list screen and controller template this feature could be easily implemented. This blog documents some of these code snippets.

So the tasks ahead were

  1. Modify the controller template to send json
  2. Add YUI datatable column and response schema definition from the grails domain class
  3. Add a search section for the domain natural keys

Send JSON

Well this task like most of the other tasks in grails is straightforward. I added a new method listJson to Controller.groovy which renders a JSON response.

Column and Datasource Definition

The columns and the response schema can be used from the grails domain class, looking it up is as easy as calling domainClass.properties, though very intuitive, implicit variables in the template are not the most well documented feature, but things are improving greatly. These properties are then used to define the columns and the response schema for the YUI datasoure. Once the data table is defined, we can listen to specific events via its subscription model(code snippet at bottom of the page).

Integrating search

In list.gsp I iterated through the properties to find the natural keys, converted that to displayable names and listed it at the top of the screen. At the back end these params were then converted to an hql and executed via findAll which creates a hql query from the request parameters and executes it via findAll.

The only caveat was since there were associations in the object model, they needed to be translated while firing the query, for example Student may have an association with Department, so it would be shown as “Department Name”, but at time of creating the hql it should be referred to as department.name, a simple string replace and this too is taken care of (code snippet at bottom of the page).

Pitfalls

The only pitfalls are in customization, for example if we wanted to send the natural key of the referenced object in the json response, we would need to plugin our own converter, which is a bit more involved, but that’s a story for another day.

Controller Code Snippet

def listJson = {
params.max = Math.min( params.max ? params.max.toInteger() : 10, 100)
log.debug "parameters received at controller \${params}"
render getValues(params) as JSON
}

private YUIResultSet getValues(Map params) {
def ${propertyName}List = null
def definedActions = ["max","action","controller"]
def queryString = "from ${className} as a";
def countQuery = "select count(*) ${queryString}"

def predicate = "";
def totalRecords = 0

if (params.keySet().size() > 3) {
def fieldFilters = new LinkedHashMap();
params.each{ key, value ->
if (!definedActions.contains(key)) {
if (value != null && value.length() > 0) {
if (!fieldFilters.isEmpty()) {
predicate +=" and "
} else {
predicate +=" where "
}

//since we also need to handle nested properties (like department.name), we need this value replacement
def fieldName = "\${key}".replace( '-','.')
def fieldKey = "\${key}".replace( '-','')
if (value.indexOf(",")!= -1) {
String[] values = value.split(",")
predicate +=" a.\${fieldName} in (:\${fieldKey})"
fieldFilters.put(fieldKey, values);
} else {
predicate +=" a.\${fieldName} = :\${fieldKey}"
fieldFilters.put(fieldKey, value);
}
}
}
}

def offsetAndMaxFilters = new HashMap(fieldFilters).plus([max:10, offset:0]);
${propertyName}List = ${className}.findAll(queryString + predicate, offsetAndMaxFilters);
totalRecords = ${className}.executeQuery(countQuery + predicate, fieldFilters).get(0);
} else {
${propertyName}List = ${className}.list( params );
totalRecords = ${className}.count()
}

return new YUIResultSet(totalResultsAvailable: totalRecords, results:${propertyName}List, totalResultsReturned: Math.min(${propertyName}List.size(), 10))
}
}

list.gsp sinppet

var myData
var myColumnDefs = [
<% excludedProps = ['version', Events.ONLOAD_EVENT, Events.BEFORE_DELETE_EVENT, Events.BEFORE_INSERT_EVENT, Events.BEFORE_UPDATE_EVENT] props = domainClass.properties.findAll { !excludedProps.contains(it.name) && it.type != Set.class } Collections.sort(props, comparator.constructors[0].newInstance([domainClass] as Object[])) props.eachWithIndex { p,i ->
print "{key : '${p.name}', label:'${p.naturalName}', sortable:true, formatter:'myCustom'}"
if (i < (props.size() -1 )) println "," } println "]" println "var dataSourceFields = [" props.eachWithIndex { p,i ->
println "'${p.name}'"
if (i < (props.size() -1 )) println "," } println "]" %>

var myDataSource, myDataTable;

YAHOO.util.Event.addListener(window, "load", function() {

YAHOO.example.XHR_JSON = function() {
this.jsonAwareCustomFormatter = function(elLiner, oRecord, oColumn, oData) {

var fieldValue = oData;
if (oData!=null && oData.name) {
fieldValue = oData.name;
}
elLiner.innerHTML = fieldValue;
};

YAHOO.widget.DataTable.Formatter.myCustom = this.jsonAwareCustomFormatter;

myDataSource = new YAHOO.util.DataSource("${camelCaseUrl(className, 'listJson')}");
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
myDataSource.connXhrMode = "queueRequests";
myDataSource.responseSchema = {
resultsList: "results",
fields: dataSourceFields
};

myDataTable = new YAHOO.widget.DataTable("json", myColumnDefs, myDataSource, {initialRequest:""});
myDataTable.subscribe("rowMouseoverEvent", myDataTable.onEventHighlightRow);
myDataTable.subscribe("rowMouseoutEvent", myDataTable.onEventUnhighlightRow);
myDataTable.subscribe("rowClickEvent", myDataTable.onEventSelectRow);

// Programmatically select the first row
myDataTable.selectRow(myDataTable.getTrEl(0));

// Programmatically bring focus to the first text box used for search
jQuery(":text ")[1].focus();

return {
oDS: myDataSource,
oDT: myDataTable
};
}();
});