Monday, April 27, 2009

How to change result of GrailsUI DataTable in an ajax way

DataTable is a good way to show data, but if you can give a way to your application users to change its result at rumtime they will get a better experience. GrailsUI DataTable taglib has an attribute named "params", which value is a query string and will be sent to your data reader since the first time DataTable sends request. So the thing becomes simple, if you can:

  1. change this attribute value at runtime

  2. change your data reader to check this query string, filter data by it and return result as JSON that DataTable needs.


Then you can change the content of DataTable as your users' wish.
There are two implement styles - one is a server-side way; the other is an ajax way, which will be this blog's topic. Since the server-side way is not complicated, I think you can do it yourself. ;)
The Domain class I will use in this example is following, and the fields used for filter are title and closed.
class Question {
String title
String details
Boolean closed
Date dateCreated
Date lastUpdated
}

First, let's see the simple part of the question: How to filter data and return the result. It's really simple, and any grails developer will finish it in no time:
def load = {
params.max = Math.min( params.max ? params.max.toInteger() : 10, 100)
def fields=[]
def fieldsCount= 0

def questionList=[]
if(!params.title && !params.closed){
fieldsCount= Question.count()
questionList= Question.list(params)
}else{
def c= Question.createCriteria()
fieldsCount= c.get{
projections{
count('id')
}
if(!params.closed params.closed=='all'){
like("title", "%${params.title?:''}%")
}else{
def closed= params.closed=='y'? true:false
and{
like("title", "%${params.title?:''}%")
eq("closed", closed)
}
}
}
questionList= Question.withCriteria{
if(!params.closed params.closed=='all'){
like("title", "%${params.title?:''}%")
}else{
def closed= params.closed=='y'? true:false
and{
like("title", "%${params.title?:''}%")
eq("closed", closed)
}
}
maxResults(params.max)
firstResult(params.offset? params.offset.toInteger():0)
}
}

questionList.each{ question ->
fields << [
id: question.id
,title: question.title
,dateCreated: g.formatDate(date:question.dateCreated, format:'yyyy-MM-hh HH:mm')
,lastUpdated: g.formatDate(date:question.lastUpdated, format:'yyyy-MM-hh HH:mm')
,closed: question.closed
,action: """<A href='/codeline/question/edit/${question.id}'>update</A>
${ if(!question.closed){
"<br/><A href='/codeline/question/delete/${question.id}'>delete</A>"
}else{
""
}
}
"""
]
}

response.setHeader("Cache-Control", "no-store")
def result= [
totalRecords: fieldsCount
, results: fields]
render result as JSON
}

Then I will show you the markup of page: a query zone and a datatable:
<div class="roundbg roundspace">
<div class="roundtl"><div class="roundbr"><div class="roundtrc"><div class="roundblc roundpad">
<h5 class="title_ico">Question List</h5>
<div class="list_toolbar"><form>
<label for="title">Title:</label>
<input type="text" maxlength="30" id="title" name="title"/>
<label for="closed">Closed?:</label>
<input type="radio" id="closed" name="closed" value="all" class="cssform_radiobutton" checked/>All
<input type="radio" id="closed" name="closed" value="y" class="cssform_radiobutton"/>Y
<input type="radio" id="closed" name="closed" value="n" class="cssform_radiobutton"/>N
<input type="button" value="submit" class="button" onclick="query">
</form></div>

<gui:dataTable id="questions" draggableColumns="true"
columnDefs="[
[key:'id', sortable:true, resizeable: true, label:'ID']
,[key:'title', sortable:true, resizeable: true, label:'Title']
,[key:'dateCreated', sortable:true, resizeable:true, label:'Created Time']
,[key:'lastUpdated', sortable:true, resizeable:true, label:'Updated Time']
,[key:'closed', label:'Is closed?']
,[key:'action', label:'Action']
]"
controller="question" action="load" rowsPerPage="1"/>
</div></div></div></div>

You may notice the submit button has a handler named "query", which will build a query string and send it to the action given above. Its code is here:
<script type="text/javascript">
var query = function(){
var str= '';
if(YAHOO.util.Dom.get('title').value.length>0){
str+= "title=" + YAHOO.util.Dom.get('title').value;
}

if(str.length>0){
str+= '&'
}
str+= "closed=" + YAHOO.util.Selector.query('#closed:checked')[0].value;
GRAILSUI.questions.customQueryString = str;

GRAILSUI.questions.loadingDialog.show();
GRAILSUI.questions.cleanup();
var sortedBy = GRAILSUI.questions.get('sortedBy');
var newState = {
startIndex: 0,
sorting: {
key: sortedBy.key,
dir: ((sortedBy.dir === YAHOO.widget.DataTable.CLASS_DESC) ? YAHOO.widget.DataTable.CLASS_DESC : YAHOO.widget.DataTable.CLASS_ASC)
},
pagination : {
recordOffset: 0,
rowsPerPage: GRAILSUI.questions.get("paginator").getRowsPerPage()
}
};
var oCallback = {
success: GRAILSUI.questions.onDataReturnInitializeTable,
failure: GRAILSUI.questions.onDataReturnInitializeTable,
scope: GRAILSUI.questions,
argument: newState
};
GRAILSUI.questions.getDataSource().sendRequest(GRAILSUI.questions.buildQueryString(newState), oCallback);
}
</script>

This code needs to be explained carefully:

  • In the example I did not use GrailsUI DataTable taglib's attribute "params", but used "customQueryString" instead. The reason is that the value of "params" will be passed to this attribute. Its purpose is to give you a way to define your own query string, its value will be a part of url that the DataSource owned by DataTable request. And this url will be built by buildQueryString()

  • The remains of query fucntion was mainly copied from GRAILSUI.DataTable.onPaginatorChangeRequest, which is in "%grailsui-home%/web-app/js/grailsui/DataTable.js". And there are two differences:


    • startIndex and recordOffser is zero

    • success handler and failure handler is not onDataReturnSetRows but onDataReturnInitializeTable

Hope you enjoy it.

3 comments:

celeraman+ said...

Thanks for sharing!
Your two posts help me so much.
Grails+GrailsUI was my choice...

Unknown said...

This is great, however I cannot get it to work. I am using a select to add the criteria to the customQueryString but it does not append the values when is posts. I am checking the params on the call of the load method and nothing. Any help would be great.

Thanks
Casey

vind said...

This is what i was searching for.. But when i tried this, i am not getting any error, but the rows are not getting filtered. any help would be appreciated.