Dynamic forms using Zend_Form

While most forms contain fixed fields there are occasions when you need a form to be dynamic and adjust itself based on user input. The adjustment could be as simple as altering the options in a drop down list or as complex as adding/removing fields. In this post I’m going to cover how to create a dynamic form using Zend_Form and jQuery. I’ll use the example of a registration form that prompts the user for their country and state. The requirements are pretty simple:

  1. It should only prompt for a state if the country has states.
  2. The state list should only show states for the selected country.
  3. The form should degrade gracefully so it works without Javascript

To start I’m going to create a World class. This class has two functions. The first returns a list of countries. The second returns a list of states for a specified country or a list of all countries that have states plus the states in those countries. You might like to retrieve this information from a database but for simplicity I’ll hard code the information into the class.

class World
{
    static private $_countries = array(
                       "AU" => "Australia",
                       "NZ" => "New Zealand");

    static private $_states = array(
		        "AU" => array(
		            "ACT" => "Australian Capital Territory",
		            "NSW" => "New South Wales",
		            "NT" => "Northern Territory",
		            "QLD" => "Queensland",
		            "SA" => "South Australia",
		            "TAS" => "Tasmania",
		            "VIC" => "Victoria"));

    public function getCountries()
    {
        return self::$_countries;
    }

    public function getStates($country = null)
    {
        if ($country === null) {
            return self::$_states;
        }
        if (array_key_exists($country, self::$_states)) {
            return self::$_states[$country];
        }
        return null;
    }
}

Next I’ll create a class for the form (RegForm) by extending Zend_Form. This gives our registration form all of the advantages you get from Zend_Form including input filtering and validation. The form elements will be added in the constructor so that creating a new form is all you need to do to use it.

As our form needs to adapt based on user input the constructor needs to accept the user input as one of its parameters. This allows us to adjust the form elements based on the user input. All code using these parameters needs to be extremely carefulĀ as the user input has not been filtered or validated yet.

class RegForm extends Zend_Form
{
    public function __construct($world, $params)
    {
        parent::__construct();

        $countries = $world->getCountries();
        $countryKeys = array_keys($countries);
        $thisCountry = isset($params['country']) ? $params['country'] : $countryKeys[0];
        $states = $world->getStates($thisCountry);

        $country = new Zend_Form_Element_Select('country');
        $country->setLabel('Country')
                ->setMultiOptions($countries)
                ->setValue($thisCountry)
                ->setRequired(true);
        $this->addElement($country);

        $state = new Zend_Form_Element_Select('state');
        $state->setLabel('State');
        if ($states !== null) {
            $state->setMultiOptions($states)
                  ->setRequired(true);
        } else {
            $state->setRegisterInArrayValidator(false);
        }
        $this->addElement($state);

        $submit = new Zend_Form_Element_Submit('submit');
        $submit->setValue('Add User')
               ->setRequired(false);
        $this->addElement($submit);
    }
}

There are two important things that RegForm does:

  1. The state options are adjusted to match the selected country.
  2. The state is not validated if the country does not contain states.

This means that our form will work exactly the way you expect Zend_Form to work. Without Javascript if you select a country and submit the form then the state list will adjust and display an error that the previously selected state was invalid. After you select a valid country and state the form will validate. If the selected country does not have states then the form with validate regardless of the selected state (providing there are no other errors).

I then need to create the controller.

class AccountController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $params = $this->_getAllParams();
        $world = new World();
        $form = new RegForm($world, $params);
        if ($this->_request->isPost() && $form->isValid($params)) {
            // The form was valid!!
        }
        $this->view = $form;
    }
}

Finally we can add Javascript to alter the form in browser. If you’re using the forms render() function then the Javascript will look something like this.

    var countries = <?php echo json_encode($this->world->getCountries()); ?>;

    var states = <?php echo json_encode($this->world->getStates()); ?>

    function updateStates() {
        var state = $("#state");
        var country = $("#country");
        var hasStates = false;
        jQuery.each(states, function (cc, slist) {
            if (cc == country.val()) {
                hasStates = true;
                state.html('');
                jQuery.each(slist, function (code, name) {
                    state.append('<option value="' +  code + '">' + name + '</option>');
                });
            }
        });
        if (hasStates) {
            $("#state-label").css("display", "block");
            $("#state-element").css("display", "block");
        } else {
            $("#state-label").css("display", "none");
            $("#state-element").css("display", "none");
        }
    }
    $().ready(function () {
        $("#country").change(function () {
            updateStates();
        });
        var state = $("#state");
        if (state.val() == null) {
            $("#state-label").css("display", "none");
            $("#state-element").css("display", "none");
        }
    });

This code takes care of hiding the states if they are not required and adjusting the states based on the selected country without needing to submit the form. With minimal effort I’ve been able to create a dynamic form using Zend_Form to do most of the hard work. Users with Javascript enabled get an A grade experience while those without Javascript can still use the form.

3 thoughts on “Dynamic forms using Zend_Form

  1. Pingback: Rich Buggy’s Blog: Dynamic forms using Zend_Form | Webs Developer

Leave a Reply