Complex custom elements in Zend_Form

Tags:

I’ve sometimes needed custom elements in forms – say two fields for inputting two numbers. For something simple like that, adding two separate elements usually suffice, but when one of my clients wanted a field with multiple checkboxes, but only specific pairs could be selected, it started getting too complex.

Until then, I had kind of avoided making those custom elements, as there didn’t seem to be much information on them in the Zend_Form documentation. I decided it was time to find out how to do it!

Alternatives

The alternatives would be creating custom view helpers to output the custom form elements, and using the viewscript decorator. Creating a custom view helper would also require a custom form element class, and it would be a bit tricky.

The viewscript decorator can be used to easily output elements with totally custom markup. You can even have the viewscript insert some JavaScript code into the inlineScript or headScript helpers. You could do this with just one of the default form elements, but in my case I required a custom element too.

I think the viewscript approach is the most flexible and simplest to implement, so I chose to go with that.

Implementing a custom time element

As an example, let’s look at how to implement a custom time element.

We’ll want to store the time in a database, in the following format: hh:mm:ss. We also want to make the user type each number in a separate box, so that’ll be three input boxes.

We could achieve this by simply adding three elements, but what if we needed to use this time box in more than one or two forms? We’d have a lot of forms where we’d need a same looking time field, and if we ever wanted to change how it works, we’d need to go through them all and manually change those three, so it’s much better to just make it a single element that we can change in one shot if required.

First, let’s look at the new form element class:

<?php
class CU_Form_Element_Time extends Zend_Form_Element 
{
    public function init()
    {
        parent::init();
        $this->addDecorator('ViewScript', array(
            'viewScript' => 'time.phtml'
        ));
 
        $this->addValidator(new Zend_Validate_Regex('/^[0-9]+:[0-9]+:[0-9]+$/'));
    }
 
    public function setValue($value)
    {
        if(is_array($value))
        {
            @list($hours, $minutes, $seconds) = $value;			
            $value = sprintf('%s:%s:%s', $hours, $minutes, $seconds);
        }
 
        return parent::setValue($value);
    }
}

In the init method, we set the viewscript’s name and add a validator, which makes sure the value fits into our format. Then, we override setValue, and in case of an array, we modify it into a string. Why? Because in our view script, we will have three boxes and they will be sent as an array in POST, and when the element is being validated, it will call setValue to set its value first.

We could also override isValid and add similar code to it, but I decided to use setValue, as now we could manually set the value from an array as well.

Next, the view script:

<?php @list($hours, $minutes, $seconds) = explode(':', $this->element->getValue()); ?>
<dt>
    <?= $this->formLabel($this->element->getName(), $this->element->getLabel()); ?>
</dt>
<dd>
    <input id="<?= $this->element->getName(); ?>" 
           type="text"
           name="<?= $this->element->getName(); ?>[]" 
           value="<?= $hours; ?>" />
 
    <input type="text" 
           name="<?= $this->element->getName(); ?>[]" 
           value="<?= $minutes; ?>" /> 
 
    <input type="text" 
           name="<?= $this->element->getName(); ?>[]" 
           value="<?= $seconds; ?>" />
 
    <?php if(count($this->element->getMessages()) > 0): ?>
        <?= $this->formErrors($this->element->getMessages()); ?>
    <?php endif; ?>
</dd>

First, we explode the values for the fields from the element’s value. The rest is quite usual. We need to add the dt and dd elements in, as the other decorators will not run with the viewscript decorator. Note how the name attributes have [] in the end? This makes the values from all of the inputs get placed in an array, so that we can easily access them in the element class.

To make the element work in your forms, you’ll need to add the path to it in the form prefix paths:

$this->addPrefixPath('CU_Form_Element_', 'CU/Form/Element/', Zend_Form::ELEMENT);

To make the viewscript work, you’ll need to place it in one of your views/scripts directories. It might be a good idea to create a common viewscript dir, say, application/views/scripts.

Conclusion

While not always necessary, custom form elements can make things easier to maintain and reuse. They may be a bit tricky to figure out at first, but after you get the idea, they are easy to create – just like Zend_Form decorators.

Further reading:
Autogenerating forms from Doctrine models
Another idea for using models with forms