AJAX Select Chaining with Catalyst

From Dev411: The Code Wiki

A common web design practice is to alter the contents of one select element based on the selected value of another select element. This can be accomplished with Perl using HTML::Prototype. The example provided here uses Catalyst, Catalyst::Plugin::Prototype, Catalyst::View::TT, HTML::Prototype, and Template Toolkit.

The example form contains two select elements, one to select a country and another to select cities in that country. When a country is selected, an AJAX call will retrieve the cities associated with that country and present them in the select cities element.

This example has code in three places:

  • the page template displaying the select elements,
  • the controller displaying the page and providing the AJAX response, and
  • the template for the AJAX response, which is plain html in this example as opposed to XML or JSON.

The most interesting code is in the TT page template with the prototype.observe_field method call.

NOTE: This code delivers a full HTML select element which Prototype.js assigns to an innerHTML attribute. While this method is very easy from an AJAX perspective since it assigns the value to innerHTML, a side-effect of this method is that the new select element will be disconnected from the DOM and will not be submitted with a form submit. An easy solution is to assign the new select's value to a hidden input element before submitting. The other alternatives is to send back the select data as JSON and have the client build the select using DOM methods.

TT page template

This page should be named page.tt and placed in your template path.

The prototype.observe_field url parameter is set to /MyApp/selectchain/setSelectCities. It should be changed if yours is in a different location.

You'll notice that a div element selectCity0 has been created around the selectCity select element. This is to accomodate the IE select innerHTML bug (http://support.microsoft.com/default.aspx?scid=kb;en-us;276228).

For a live site, you'll also want to switch out the prototype.define_javascript_functions method for JS includes.

<html>
<head>
[% c.prototype.define_javascript_functions %]
</head>
<body>
<form>

<select id="selectCountry">
  <option>[ Select Country ]</option>
  <option value="cn">China</option>
  <option value="de">Germany</option>
  <option value="uk">United Kingdom</option>
</select>

<div id="selectCity0">
  <select id="selectCity">
    <option>[ Select Country First ]</option>
  </select>
</div>

[% c.prototype.observe_field('selectCountry',{
  url    => '/MyApp/selectchain/setSelectCities',
  with   => "'country='+document.getElementById('selectCountry').value",
  update => 'selectCity0',
}) %]

</form>
</body>
</html>

Catalyst controller

There are two methods in the controller:

  • displayForm which displays the form with the two select elements, Prototype JavaScript, and the prototype.observe_field method.
  • setSelectCities which is called by the prototype.observe_field method and responds with with a HTML select element of corresponding cities.
package MyApp::Controller::SelectChain;
use strict;
use base 'Catalyst::Controller';

# Display the select elements

sub displayForm: Local {
  my ( $self, $c ) = @_;
  $c->stash( template => 'page.tt'); # TT page template
}

# Provide the AJAX response

sub setSelectCities : Local {
  my ( $self, $c ) = @_;

  my $country = $c->request->param('country');
  my $cities = {
    cn => [qw/Beijing Nanjing Shanghai/],
    de => [qw/Berlin Munchen Zuffenhausen/],
    uk => [qw/Leeds London Manchester/]
  };
  my $items;

  if ( exists $cities->{$country} ) {
    $items = ['[ Select City ]'];
    push @$items, @{$cities->{$country}};
  } else {
    $items = ['[ Select Country First ]'];
  }

  $c->stash(
    select => {
      id => 'selectCity',
      items => $items
    },
    template => 'select.tt' # TT response template
  );
}

sub end : Private {
  my ( $self, $c ) = @_;
  $c->forward('MyApp::View::TT');
}

1;

TT response template

This page should be named select.tt and placed in your template path.

A simple partial HTML response is all that's required, as opposed to XML or JSON.

<select id="[% select.id %]">
  [% FOREACH item=select.items %]
  <option>[% item %]</option>
  [% END %]
</select>