Sunday, July 27, 2008

Filtering a List

While the list screen is useful with sorting and pagination, it would be nice to filter the number of items on a list. This tutorial provides a way of implementing this.

For this tutorial, I'll show an employee filter. To make things a bit more useful, I'll also allow the user to filter by Department. Shown below is the datamodel.



My domain classes are as follows.

class Employee {
String firstName

String lastName

Department department

static mapping = {
version false }
}


class Department {
String name
String toString() {"${this.name}"}

static mapping = {
version false }
}


Lets have a look at the list without filters. Notice that there are around 100 employees from different departments.

















I want to be able to filter by Department, First Name, and Last Name. I will add the code for the filters in views/employee/list.gsp. I created this file by running
grails generate-all Employee

The following snippits of code is from my edited version of list.gsp. Notice, I have added 3 new inputs fields. I have placed them above the List table. Notice I have used flash to default values. More on this later. You can see the full source here

<g:select name='department'
noSelection="['':'']"
optionKey="id"
value="${flash.department}"
from='${Department.list()}'>
</g:select>

<input type="text" id="firstName" name="firstName" value="${flash.firstName}"/>

<input type="text" id="lastName" name="lastName" value="${flash.lastName}"/>



The list screen now looks like this.
















Of course, the filtering won't actually work unless we modify the controller to change the database query. He is the list method of the EmployeeController.groovy. The key thing to note is that I'm putting the request parameter back into flash so I can have the value defaulted when the list.gsp displays.

def list = {
//keep these values so we can rerender on the filters
flash.firstName = params.firstName
flash.lastName = params.lastName
flash.department = params.department

if(!params.max) {
params.max = 10
}
def query
def criteria = Employee.createCriteria()
def results

query = {
and {
like("firstName", params.firstName + '%')
like("lastName", params.lastName + '%')

if(params.department){
def selectedDepartment = Department.get(Integer.parseInt(params.department))
eq('department', selectedDepartment )
}
}
}

results = criteria.list(params, query)

render(view:'list', model:[ employeeList: results ])

}


I also need to change my list.gsp slightly. Note, I've changed the pagination tag and the each tag. We need to pass the flash params in the paginate tag so the controller will still know what the filter criteria was when the user selects next/prev. Full source code here.

<g:each in="${employeeList}" status="i" var="employee">


<g:paginate total="${employeeList.getTotalCount()}" params="${flash}"/>


And thats it, you should now be able to filter on Department, First Name, and Last Name. I suppose this could be scaffolded. It might get a bit more complex when dealing with dates, but it is not impossible.



12 comments:

Unknown said...

Simple and useful! Thanks!

Anonymous said...

I like this idea to show a way to filter records.I am not sure when I created this that I had The Employee filter for department. How do you get Employees added to department?. I created the screen to add departments.
What am I missing?
Thanks
Jim

Chris said...

Jim,

In my empoyee/create.gsp, I have the following code

<tr class="prop">

<td valign="top" class="name">

<label for="department">Department:</label>

</td>

<td valign="top" class="value ${hasErrors(bean:employee,field:'department','errors')}">

<g:select optionKey="id" from="${Department.list()}" name="department.id" value="${employee?.department?.id}" ></g:select>

</td>

</tr>

Anonymous said...

I commented as anonymous previously.
I restudied your example. I saw where I could add employees.
It's a great article for me.!!!!!!!
Thanks
Jim

Anonymous said...

Great example, it helped me a lot.
You might want to add params="${flash}" to the sortable columns so you will order the filtered result set.

Michael Blake said...

Thanks very much Chris

Would it be possible for you to post the EmployeeController.groovy file as well?

Chris said...

class EmployeeController {

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

// the delete, save and update actions only accept POST requests
def allowedMethods = [delete:'POST', save:'POST', update:'POST']

def list = {
//keep these values so we can rerender on the filters
flash.firstName = params.firstName
flash.lastName = params.lastName
flash.department = params.department

if(!params.max) {
params.max = 10
}
def query
def criteria = Employee.createCriteria()
def results

query = {
and {
like("firstName", params.firstName + '%')
like("lastName", params.lastName + '%')

if(params.department){
def selectedDepartment = Department.get(Integer.parseInt(params.department))
eq('department', selectedDepartment )
}
}
}

results = criteria.list(params, query)

render(view:'list', model:[ employeeList: results ])

}

def show = {
def employee = Employee.get( params.id )

if(!employee) {
flash.message = "Employee not found with id ${params.id}"
redirect(action:list)
}
else { return [ employee : employee ] }
}

def delete = {
def employee = Employee.get( params.id )
if(employee) {
employee.delete()
flash.message = "Employee ${params.id} deleted"
redirect(action:list)
}
else {
flash.message = "Employee not found with id ${params.id}"
redirect(action:list)
}
}

def edit = {
def employee = Employee.get( params.id )

if(!employee) {
flash.message = "Employee not found with id ${params.id}"
redirect(action:list)
}
else {
return [ employee : employee ]
}
}

def update = {
def employee = Employee.get( params.id )
if(employee) {
employee.properties = params
if(!employee.hasErrors() && employee.save()) {
flash.message = "Employee ${params.id} updated"
redirect(action:show,id:employee.id)
}
else {
render(view:'edit',model:[employee:employee])
}
}
else {
flash.message = "Employee not found with id ${params.id}"
redirect(action:edit,id:params.id)
}
}

def create = {
def employee = new Employee()
employee.properties = params

return ['employee':employee]
}

def save = {
def employee = new Employee(params)
if(!employee.hasErrors() && employee.save()) {
flash.message = "Employee ${employee.id} created"
redirect(action:show,id:employee.id)
}
else {
render(view:'create',model:[employee:employee])
}
}
def newEmployeeWizardFlow = {

}
}

Anonymous said...

This is like something I'm trying to do at the moment, so it's very helpful. How does the filter get executed, though? When the user refreshes the list having entered values into the filter fields? Would it work if you had a search button there? I imagine you'd have to pass it the parameters it needed.

Chris said...

There is a search button.

Anonymous said...

Thank you for posting this

Fabbo said...

I tried implementing this with Grails 1.3.6 and had a few issues.

* Instead of a ParseInt to match the department, I had to use a ParseLong.
* Instead of matching to department, I had to match to department.id

So, it looked like this:

eq('department.id', Long.parseLong(params.department))

Also, this code doesn't work with the standard table-sorting provided by g:sortableColumn. I ended up storing the selections in session rather than flash so that it would persist through sorting changes, together with a bit of logic surrounding this change.

Thanks for the original code, though!

Anonymous said...

Thanks, this was extremely helpful.