Dynamic Input Boxes with Zend Framework 1.12

I recently ran into a scenario where I needed to have the ability to use a dynamically created number of text inputs on a Zend Framework Form. My form will be used to ship an unknown number of packages. The user will enter the weight for each package in its own input element on the form. If the user needs another package, the user will click "Add Package" until the shipment is done and ready to be processed. In the end, we may have a shipment with one package, or a shipment with ten, we just dont know. Here's how I did it:

The Zend_Form itself will only have an empty subform to start. This is where we will place packages when the form is submitted. So, in my ship form file:

$packages = new Zend_Form_SubForm();
$this->addSubForm($packages, 'packages');

However, since a shipment must have at least one package, inside my controller I check if the for has been POSTed and if not, call the buildPackages() method in the ship form file:

$form->buildPackages(array(1=>'initialize'));

Here, I send the buildPackages() method an array  with the key of the package number and the string 'initialize'; I suppose I could send any string, but "initialize" seemed to be more verbose.

I will show you my buildPackages method later, but for now, this takes care of the initialization of the form with one input box configured for us in the subform named 'packages'.

To display these packages I used a custom decorator that handles the entire form. Within my ship form file, I tell Zend_Form to use my _partials/shipform.phtml file to render the entire form:

$this->setDecorators(array(
                         'PrepareElements',
                            array(
                                'ViewScript',
                                 array('viewScript' => '_partials/shipform.phtml', 'shipment' => $this->_shipment)
                            )
                      ));

Within this decorator, I loop through each element within the subform 'packages' to display each package:

    <?php
    foreach ($this->element->getSubForm('packages') as $index=>$package){
        ?>
        <tr>
            <td class='packageNumLabel'>Package #<?php echo $index;?></td>
            <td colspan="3" ><?php echo $package;?></td>
        </tr>
    <?php
    }
    ?>

Remember, initially I only have 1 package.

I added a button element to the directly to the decorator: 

<input type="button" class='actionbutton' id="addPackage" value='Add Package'/>

Now, I need a javascript method to add more text elements to the form. This also adds a remove button. Afterall, since we are making each text field required, we need a way to remove the unused text inputs. I was able to accomplish this using javascript cloning and jQuery:

        var actionButton = $("#addPackage");
        if (actionButton.length){
            $(actionButton).click(addPackage);
            $(".actionbutton-inline").click(removePackage);
            addRemoveButtons();
        }

/**
 * addPackage function
 * called by "add Package" button
 * clones the last row of the shipment,
 * changes the input element's name and id
 */
function addPackage(){
    var tbody = $(this).closest('tbody');
    var row = $('tr', tbody).last().clone();
    var weightInput= $('input[type="text"]', row);
    var newPackageCount =  parseInt($(weightInput, row).data('package-number')) + 1;
    var tdForLabel = $('td:first', row);
    tdForLabel.text(tdForLabel.text().replace(/[0-9]+/g, newPackageCount));
    var input = $('input[type="text"]', row);

    $(weightInput).attr({
               'id':$(weightInput).attr('id').replace(/[0-9]+/g, newPackageCount),
               'name':$(weightInput).attr('name').replace(/[0-9]+/g, newPackageCount),
               'data-package-number':newPackageCount
               });
    $(weightInput).val('');
    $(weightInput).parent().attr('id','packages-'+newPackageCount+'-element');
    $(weightInput).siblings().remove();
    //$(weightInput).parent().siblings().remove();

    var removeButton = createRemoveButton();

    $(weightInput).after(removeButton);

    $(tbody).append(row);
}

 I have something that looks like this:

It's time to submit the form and have Zend_Form validate the inputs as digits and less than 100 pounds for each package.

Since the form posts to itself, all we really need to do is call the buildPackages() method before we check if the form is valid using the isValid(). Inside my controller:

$form  = new Application_Form_Ship();
$form->setAction('/pressjob/ship/detailsid/' . $shippingId);
$request = $this->getRequest();
if ($request->isPost()) {
    $postData = $request->getPost();
    $form->buildPackages($postData['packages']);
    if ($form->isValid($request->getPost())) {
.. process form here ..}
} else {
    $form->buildPackages(array(1=>'initialize'));
}

And the buildPackages() method in the ship form iterates through an array and adds an elements to the form. When the form is POSTed, we send the ['packages'] element as the array. When it's not, we know that it needs to be initialized and therefor we send it the array(1=>'initialize') array. Either case, the method accepts an array and loops through it using the key as the elements name. Inside my ship form:

     /**
     * buildPackages
     * builds elements based on POST data or
     * an initializing array when no posted data:
     *  $service->buildPackages(array (1=>'initialize'));
     *
     * @param array $data of package elements from dynamic form
     */
    public function buildPackages(array $data)
    {
        foreach($data as $index=>$value){
            $elementName = (string) ($index);
            $packagesSubForm = $this->getSubForm('packages');
            $packagesSubForm->addElement('text', $elementName, array(
                                                           'class'               => 'required package',
                                                           'required'            => true,
                                                           'validators'          => array(
                                                               array('Digits'),
                                                               array('LessThan', 'max', self::MAX_PACKAGE_WEIGHT,
                                                                     "notLessThan"=>"Oy vey! Less than 100, please."),
                                                           ),

                                                           'data-package-number' => $elementName,
                                                      ));
            $thisElement = $packagesSubForm->getElement($elementName);
            $thisElement->removeDecorator('label')->removeDecorator('HtmlTag');
        }
    }

Since the validators are on each element, and each element is part of the Zend_Form, using the Zend isValid() method works as it should:

There you have it. Dynamically created, fully Zend_Form validated and filtered. And the beauty of this, since we have added these elements to the form, whenever the form does not validate, Zend_Form kindly remembers the all of the created form's package elements along with their values as if I had hard coded them into the form file itself! No need to write extra code to make the form "sticky". Here is the final version of my form:

I hope this helps another Zend Framework user out there! Let me know if it did or if you have any questions.

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer