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:
<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:
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:
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:
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
$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:
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:
// 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:
For the newField action you need a viewscript with:
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??