Creating dependent selects in Grails

Grails
In my previous post I explained how to use Ajaxify Grails Webflows, and this time I will address how to add two select boxes where the latter is depending on the input of the first. Let’s say we would like to add a select containing species, and a select containing genotypes of a particular species. As the latter is depending on the first, we would like the latter to automatically update whenever another species is selected.

First, we will create the two select elements in the view. The genotype select can be empty as it will be dynamically filled by the species select. The species select, however, is a little bit more complicated as it uses a list (speciesList) as input and contains an onChange action:

...
    <tr>
      <td class="title">Species</td>
      <td>
          <g:select
              name="species_id"
              value="${species?.id}"
              optionKey="id"
              optionValue="name"
              from="${speciesList}"
              noSelection="['':'-Choose species-']"
              onChange="${remoteFunction( action:'ajaxGetGenotypeBySpecies',
                                          params: '\'id=\'+escape(this.value)',
                                          onSuccess:'updateSelect(\'genotype_id\',data,false,false,\'default\')')}" />
      </td>
    </tr>
    <tr>
      <td class="title">Genotype</td>
      <td><g:select name="genotype_id" optionKey="id" optionValue="name" from="[]" /></td>
    </tr>
    ...

Looking at the two selects, you will see the species select will use Grails’ remoteFunction to remotely call ajaxGetGenotypeBySpecies in the controller. That particular action just fetches all genotypes for a particular species, and renders the list as a JSON object which, in turn, is given back to the calling JavaScript.

    // use Grails' converters to be able to render JSON
    import grails.converters.*

    ...

    /**
     * Fetch all genotypes for a species and render them as JSON
     * @param int species id
     * @return string JSON response
     */
    def ajaxGetGenotypeBySpecies = {
        // find species by id
        def species = Species.get(params.id)

        // fetch genotypes of a particular species
        def genotypes = Genotype.findAllBySpecies(species, [sort:'name',order:'asc'])

	// render as JSON
        render genotypes as JSON
    }

When the call has been a success, the updateSelect(\’genotype_id\’…) JavaScript function is executed, stating we would like to update the select element with genotype_id as id.

/**
 * Update one select based on another select
 *
 * @author you
 * @see     http://www.grails.org/Tag+-+remoteFunction
 * @param   string  select (form) name
 * @param   string  JSON data
 * @param   boolean keep the first option
 * @param   int     selected option
 * @param   string  if null, show this as option instead
 * @void
 */
function updateSelect(name,data,keepFirstOption,selected,presentNullAsThis) {
    var rselect = $('#'+name).get(0);
    if (data) {
        // remove old options
        var start = (keepFirstOption) ? 0 : -1;
        var i = rselect.length;
        while (i > start) {
            rselect.remove(i);
            i--;
        }

        // add new options
        $.each(items,function() {
            var i = rselect.options.length
            rselect.options[i] = new Option(
                (presentNullAsThis && this.name == null) ? presentNullAsThis : this.name,
                this.id
            );
            if (this.id == selected) rselect.options[i].selected = true;
        });
    }
}

Now that the whole chain is working, we have one last thing to do. When the user enters the page the first time, the species list is populated and the genotype list is empty. However, when he has already previously selected a species, we would like the genotype list to be populated and properly selected as well. In order to do this we will call do the following when a species is available:

    <g:if test="${species}"><script><g:remoteFunction action="ajaxGetGenotypeBySpecies" params="'id=${species.id}'" onSuccess="updateSelect('genotype_id',data,false,${genotype?.id},'default')" /></script></g:if>

And we’re done! :)

ps. Note that this is only one way to accomplish creating dependent selects. There are several other ways to accomplish the same result