Last updated by 7 months ago

Page: AJAX-Driven SELECTs in GSP, Version:15

AJAX-Driven SELECTs in GSP

A common pattern that pops up in Web development is the need to have one SELECT box change the contents of another SELECT box on the same page without doing a refresh.

Take the following domains as an example:

class Country {
    String name
    String abbr
    String language

static hasMany = [cities:City] }

class City { String name String timezone

static belongsTo = [country:Country] }

On our web page, we want to constrain the user to entering only Countries and Cities that our database knows about. This is a perfect use for SELECTs on our webpage. However, because City belongs to Country, we will need to change the City SELECT when the user change's the Country SELECT. Once-upon-a-time we'd either just reload the entire page in response to ONCHANGE events in the Country SELECT or we'd preload all the countries and cities into the page in Javascript.

However, a modern web page can leverage AJAX and JSON to quickly and efficiently update the City select.

First, in the CountryController, add the following handler:

import grails.converters.*

class CountryController {

def ajaxGetCities = { def country = Country.get(params.id) render country?.cities as JSON } }

Remember to import the converters!

You can test your AJAX call using curl from the command line:

$ curl "http://localhost:8080/widgetco/country/ajaxGetCities/1

"[ {"id":4,"class":"City","country":1,"name":"Dallas","timezone":"CST"}, {"id":1,"class":"City","country":1,"name":"New York","timezone":"EST"} ]"

That was the easy part.

Next, you need to wire up your SELECTs in a GSP page. Things get a little more complicated. You'll be using a AJAX library in your GSP page, so first you must include that in your page's HEAD element, eg:

<g:javascript library="prototype" />

Next, you'll need a form with the SELECTs:

<form>
    <g:select
        optionKey="id" optionValue="name" name="country.name" id="country.name" from="${Country.list()}"
        onchange="${remoteFunction(
            controller:'country', 
            action:'ajaxGetCities', 
            params:'\\'id=\\' + escape(this.value)', 
            onComplete:'updateCity(e)')}"
    ></g:select>
    <g:select name="city" id="city"></g:select>
</form>
Where the 2 \ should be replaced by 1 \ (due to wiki syntax)
If you need to pass more than 1 parameter, use the character & :

params:'\\'id=\\' + escape(this.value)+\\'&id2=\\' + escape(this.value)'

In Grails 2 where JQuery is used by default, the return value is named "data" and you need to use "onSuccess" instead of "onComplete" method, such as bellow:

onSuccess:'updateCity(data)'
The above GSP creates a simple form with two SELECTs and preloads the country.name SELECT from the database.

To complete the wiring, you need to write some Javascript:

<g:javascript>

function updateCity(e) { // The response comes back as a bunch-o-JSON var cities = eval("(" + e.responseText + ")") // evaluate JSON

if (cities) { var rselect = document.getElementById('city')

// Clear all previous options var l = rselect.length

while (l > 0) { l-- rselect.remove(l) }

// Rebuild the select for (var i=0; i < cities.length; i++) { var city = cities[i] var opt = document.createElement('option'); opt.text = city.name opt.value = city.id try { rselect.add(opt, null) // standards compliant; doesn't work in IE } catch(ex) { rselect.add(opt) // IE only } } } }

// This is called when the page loads to initialize city var zselect = document.getElementById('country.name') var zopt = zselect.options[zselect.selectedIndex] ${remoteFunction(controller:"country", action:"ajaxGetCities", params:"'id=' + zopt.value", onComplete:"updateCity(e)")}

</g:javascript>

The above Code is for Grails 1.x where prototype is used. In Grails 2 with JQuery this has to be a bit different, especially the eval(...) is not necessary. "data" is a valid javascript object.

Everything is wired up now. When you change the country.name SELECT, it will make an AJAX call to your Country controller. When the call completes, updateCity() will execute on your page, it being passed the block of JSON returned from your controller. The function evaluates the JSON and rebuilds the city SELECT.

One final bit of code at the bottom of the page initializes the city SELECT to match the country.name SELECT when the page is loaded.

You can download the working demo from Dropbox.

There is a small typo in this example code:

<title>Demo of Selects</time>
should be
<title>Demo of Selects</title>
Also, adding
"/" (controller: 'country', action: 'selectdemo')
to the UrlMappings.groovy file allowed the index page to load properly.