So a common thing that I’ve always done in my forms is create a group of inputs that can be dynamically created again (through JS). For example I would allow users to add N number of contacts. To add a new one the user would click on “add” and JS would do its DOM creation and add all the new elements with name=”myname[]“. I wanted to do this with Zend and still have the JS generated content reshow up on a form submission. I’ve searched forever on how to do this, so hopefully this helps someone. (It’s probably not the best way, but it does work.)

These two tutorial helped greatly:

http://weierophinney.net/matthew/archives/212-The-simplest-Zend_Form-decorator.html (custom elements)
http://www.jeremykendall.net/2009/01/19/dynamically-adding-elements-to-zend-form/ (ajax)

Form HTML to reproduce:

<ul id="person">
<li>
<label for="personFirstName">First Name:</label><input id="personFirstName" name="person[firstName][]" type="text" value=""/>
<label for="personLastName">Last Name:</label><input id="personLastName" name="person[lastName][]" type="text" value=""/>
<label for="personEmail">Email:</label><input id="personEmail" name="person[email][]" type="text" value=""/>
</li>
</ul>
<ul id="person">
<li>
<label for="personFirstName">First Name:</label><input id="personFirstName" name="person[firstName][]" type="text" value=""/>
<label for="personLastName">Last Name:</label><input id="personLastName" name="person[lastName][]" type="text" value=""/>
<label for="personEmail">Email:</label><input id="personEmail" name="person[email][]" type="text" value="ssss"/>
</li>
</ul>

The Person Element Class:

<?php
    class My_Form_Element_Person extends Zend_Form_Element_Xhtml {
       
       protected $_fname;
       protected $_lname;
       protected $_email;
       protected $_emailLabel = ‘Email:’;
       protected $_fnameLabel = ‘First Name:’;
       protected $_lnameLabel = ‘Last Name:’;
       protected $_fnameID;
       protected $_lnameID;
       protected $_emailID;
       
       public function __construct($spec, $options = null)
        {
            $this->addPrefixPath(
                ‘My_Form_Decorator’,
                ‘My/Form/Decorator’,
                ‘decorator’
            );
           
                   
            parent::__construct($spec, $options);
          $this->_fnameID = parent::getID().‘FirstName’;
          $this->_lnameID = parent::getID().‘LastName’;
          $this->_emailID = parent::getID().‘Email’;       
        }
       
       public function loadDefaultDecorators()
        {
            if ($this->loadDefaultDecoratorsIsDisabled()) {
                return;
            }
            $decorators = $this->getDecorators();
            if (empty($decorators)) {
                $this->addDecorator($this->getDecoratorName())
                     ->addDecorator(‘Errors’)
                     ->addDecorator(‘Description’, array(
                        ‘tag’ => ‘p’,
                        ‘class’ => ‘description’)
                     )
                     ->addDecorator(‘HtmlTag’, array(
                        ‘tag’ => ‘ul’,
                        ‘id’  => $this->getName())
                     );
            }
        }
       
        protected function getDecoratorName(){
           return ‘Person’;
        }
       
        public function getLabel($type){
           
           if(isset($type)){
              switch($type){
                 case ‘firstName’:
                    return $this->getFirstNameLabel();
                 case ‘lastName’:
                    return $this->getLastNameLabel();
                 case ‘email’:
                    return $this->getEmailLabel();
              }
           }   
           else   {
              return parent::getID();
           //   throw new Exception(‘Please choose a label.’);
           }
                       
        }
       
       public function getID($type){
     
           if(isset($type)){
              switch($type){
                 case ‘firstName’:
                    return $this->getFirstNameID();
                 case ‘lastName’:
                    return $this->getLastNameID();
                 case ‘email’:
                    return $this->getEmailID();
              }
           }   
           else   {
              return parent::getID();
           //   throw new Exception(‘Please choose a label.’);
           }
                       
        }
       
        public function getFirstNameID(){
           return $this->_fnameID;
        }
       
        public function getFirstNameLabel(){
           return $this->_fnameLabel;
        }
       
       public function setFirstName($value){
          $this->_fname = $value;
          return $this;
       }
       
       public function getFirstName(){
          return $this->_fname;
       }
       
       public function getLastNameID(){
          return $this->_lnameID;
       }
       
       public function getLastNameLabel(){
           return $this->_lnameLabel;
        }
       
       public function setLastName($value){
          $this->_lname = $value;
          return $this;     
       }
       
       public function getLastName(){
          return $this->_lname;     
       }
       
       public function getEmailID(){
          return $this->_emailID;
       }
       
       public function getEmailLabel(){
           return $this->_emailLabel;
        }
       
       public function setEmail($value){
          $this->_email = $value;
          return $this;
       }
       
       public function getEmail(){
          return $this->_email;
       }
       
       public function setValue($values){
          if(isset($values[‘firstName’]) && isset($values[‘lastName’]) && isset($values[‘emailName’])){
             $this->setFirstName($values[‘firstName’]);
             $this->setLastName($values[‘lastName’]);
             $this->setEmail($values[‘email’]);
          }
          else {
             // What till validation, could add it here?
    ;         //throw new Exception(‘Invaild Person, please provide a first name, last name and email.’);
          }
         
          return $this;
       }
       
       public function getValue(){
          return $this->getFirstName().‘ ‘.$this->getLastName().‘, ‘.$this->getEmail();
       }
       
    }

The People Decorator class:

<?php
    class My_Form_Decorator_Person extends Zend_Form_Decorator_Abstract {
       
       protected $_format = ‘<label for="%s">%s</label><input id="%s" name="%s[]" type="text" value="%s"/>’;
       
       public function render($content){
          $el = $this->getElement();
          if(!$el instanceof My_Form_Element_Person){         
             return $content;
          }
         
          $view = $el->getView();
          if(!$view instanceof Zend_View_Interface){         
             return $content;
          }
          $markup = $this->wrap($this->renderFirstName($el).$this->renderLastName($el).$this->renderEmailName($el));
         
          switch ($this->getPlacement()) {
                case self::PREPEND:
                    return $markup . $this->getSeparator() . $content;
                case self::APPEND:
                default:
                    return $content . $this->getSeparator() . $markup;
            }
       }
       
       protected function renderFirstName($el){
          return $this->renderEl(‘firstName’, $el->getFirstName(), $el);
       }
       
       protected function renderLastName($el){
          return $this->renderEl(‘lastName’, $el->getLastName(), $el);
       }
       
       protected function renderEmailName($el){
          return $this->renderEl(‘email’, $el->getEmail(), $el);
       }
       
       protected function renderEl($type, $value, $el){
          $name = $el->getFullyQualifiedName();
          return sprintf($this->_format, $el->getID($type), $el->getLabel($type), $el->getID($type), $name.‘['.$type.']‘, $value);     
       }
       
       protected function wrap($markup) { return ‘<li>’.$markup.‘</li>’; }
    }

Here is the DynamicController that handles the AJAX JS creation:

<?php
    class DynamicController extends Zend_Controller_Action {
       
       public function indexAction(){
         
       }
       
       /**
        * Ajax action that returns the dynamic form field
        */

       public function newfieldAction() {
       
       // Do this unless you want the layout too :-P
         $layout = Zend_Layout::getMvcInstance();
         $layout->disableLayout();
         $ajaxContext = $this->_helper->getHelper(‘AjaxContext’);
         $ajaxContext->addActionContext(‘newfield’, ‘html’)->initContext();
           
         $id = $this->_getParam(‘id’, null);
       
         $element = new My_Form_Element_Person(‘person’);
           
         // What is returned thru ajax
         $this->view->field = $element->__toString();
       }
    }

My JS script (I use prototype) You can see how to do it with JQuery from on of the above tutorials:

Event.observe(‘addElement’, ‘click’,
          function() {
              ajaxAddField();
           }
        );
    // Get value of id – integer appended to dynamic form field names and ids
    var id = $("id").getValue();
    // Retrieve new element’s html from controller
    function ajaxAddField() {
       
       // Update the database based on the Primary key with the new value.
       new Ajax.Request(‘/apply/index.php/dynamic/newfield’,
       {
          method: ‘post’,
          parameters: {‘id’:‘id=’+id},
          onSuccess: function(el){
             $("addElement-label").insert(el.responseText);
                // Increment and store id
               $("id").val(++id);
          }
       });   

    }

The prevalidation method (in your Form class) that captures the dynamic content on submit:

public function preValidation(array $data) {
        // Search $data for dynamically added fields using findFields callback
        $fnames = $data[‘person’][‘firstName’];
        $lnames = $data[‘person’][‘lastName’];
        $emails = $data[‘person’][‘email’];
       
        // For each person
        for($i=0;$i<count($fnames);$i++){
           $el = new My_Form_Element_Person(‘person’);
           $el->setFirstName($fnames[$i]);
           $el->setLastName($lnames[$i]);
           $el->setEmail($emails[$i]);
           $this->persons[] = $el;
        }
      }
    // Used to redisplay the new content.
      public function getPersons(){
         
            $els = $this->persons;
          $xhtml = ;
          foreach($els as $el => $e){
             $xhtml .= $e->__toString(); // Add HTML
          }
          return $xhtml;
      }

In my main view I display all of the people by:

<?=$this->form->getPersons() ?>

For the newField action you need a viewscript with:

<?= $this->field ?>

That’s all of the pieces. If I get time better I’ll try to explain how to put them together. Please read this tutorial first, it will help you set up the AJAX stuff. Then just add a button element and a container and everything should work.

What do you think??

Leave a Reply

(required)

(required)