Dynamically Adding Elements to Zend_Form
Posted on January 19th, 2009 in Development | 38 Comments »
There have been some requests on the Zend Framework mailing lists for information on how to dynamically add elements to Zend_Form. This is something that I’ve been looking into myself, and I’d like to share what I’ve come up with.
Please note that this code is a proof of concept / request for peer review detailing the work that I’ve done to date, and not an example of what I might consider the best way to address this use case. Special thanks go to Cory Wiles who helped me think things through when I first started giving this a go.
First, let’s do a high level walk through of what the code is going to do, then take a look at the code, and wrap up with a live example.
High Level Overview
The form in this example extends Zend_Form and consists of a hidden element that stores an ID, a single text element, buttons for adding and removing dynamic elements, and a submit button. The add and remove buttons are used to trigger a jQuery script that adds and removes dynamic elements. The jQuery script uses the value of the hidden ID element to set element order and make the dynamic element names and IDs unique.
The form class consists of the standard init() method for building the form and two custom methods for dealing with dynamic elements. There is a preValidation() method, called after the form is submitted but before it is validated, that searches the submitted form data for dynamically added fields. If any new fields are found, the addNewField() method takes care of adding the new fields to the form.
jQuery is used to request the new form element from the form’s Controller via Ajax, utilizing the AjaxContext action helper. jQuery is also used to find the most recently added dynamic element, allowing for easy removal of dynamically added elements from the form.
The action controller contains the action that displays the form, and it also has a newfieldAction() that utilizes the AjaxContext to return markup for new fields.
The Zend_Form Subclass
Let’s start with the code for the form. The most important item here is that each form element has its order property set. You can see the huge jump in the order between the “name” element and the “addElement” button. This gap occurs so the dynamic elements can be placed exactly where I want them and so they’ll maintain their position in the form once they’ve been added to the form object.
public function init() { $this->addElement('hidden', 'id', array( 'value' => 1 )); $this->addElement('text', 'name', array( 'required' => true, 'label' => 'Name', 'order' => 2, )); $this->addElement('button', 'addElement', array( 'label' => 'Add', 'order' => 91 )); $this->addElement('button', 'removeElement', array( 'label' => 'Remove', 'order' => 92 )); // Submit $this->addElement('submit', 'submit', array( 'label' => 'Submit', 'order' => 93 )); }
Action Controller
The action that displays the form is straightforward. If you’ve ever done any work with Zend_Form, I’m sure you recognize what’s going on here. The only thing to note is the $form->preValidation() method. That’s where the magic happens. We’ll get to that in a bit.
/** * Shows the dynamic form demonstration page */ public function dynamicFormElementsAction() { $form = new Code_Form_Dynamic(); // Form has not been submitted - pass to view and return if (!$this->getRequest()->isPost()) { $this->view->form = $form; return; } // Form has been submitted - run data through preValidation() $form->preValidation($_POST); // If the form doesn't validate, pass to view and return if (!$form->isValid($_POST)) { $this->view->form = $form; return; } // Form is valid $this->view->form = $form; }
Next comes the controller’s newfieldAction(). This action utilizes the AjaxContext action helper to pass the new field’s markup back to the form view.
/** * Ajax action that returns the dynamic form field */ public function newfieldAction() { $ajaxContext = $this->_helper->getHelper('AjaxContext'); $ajaxContext->addActionContext('newfield', 'html')->initContext(); $id = $this->_getParam('id', null); $element = new Zend_Form_Element_Text("newName$id"); $element->setRequired(true)->setLabel('Name'); $this->view->field = $element->__toString(); }
jQuery
The jQuery script is also fairly straightforward. I attach event listeners to the “Add” and “Remove” buttons that call the ajaxAddField and removeField methods respectively.
The ajaxAddField method makes a post request to the newfieldAction using jQuery’s .ajax method, passing in the current value of the hidden ID element. On success, the new element’s markup is added to the form, and the ID is incremented and stored in the hidden ID element.
The removeField method finds the last element in the page with the class dynamic, removes it, then decrements the current ID and stores the new value in the hidden ID element.
<script type="text/javascript"> $(document).ready(function() { $("#addElement").click( function() { ajaxAddField(); } ); $("#removeElement").click( function() { removeField(); } ); } ); // Get value of id - integer appended to dynamic form field names and ids var id = $("#id").val(); // Retrieve new element's html from controller function ajaxAddField() { $.ajax( { type: "POST", url: "<?=$this->url(array('action' => 'newfield', 'format' => 'html'));?>", data: "id=" + id, success: function(newElement) { // Insert new element before the Add button $("#addElement-label").before(newElement); // Increment and store id $("#id").val(++id); } } ); } function removeField() { // Get the last used id var lastId = $("#id").val() - 1; // Build the attribute search string. This will match the last added dt and dd elements. // Specifically, it matches any element where the id begins with 'newName<int>-'. searchString = '*[id^=newName' + lastId + '-]'; // Remove the elements that match the search string. $(searchString).remove() // Decrement and store id $("#id").val(--id); } </script>
Zend_Form: preValidation() and addNewField()
Now on to the fun stuff. All of the code up to this point is present to support what happens in the form’s preValidation() method. Remember that preValidation() is called after the form has been submitted but before the form is validated. preValidation() searches through the submitted form’s data for new fields. If it finds any new fields, it calls addNewField() and adds the new fields to the form object. By adding the new form fields to the form object before validation, any filters and validators attached to the new fields will be run as if those fields had always existed in the form object.
/** * After post, pre validation hook * * Finds all fields where name includes 'newName' and uses addNewField to add * them to the form object * * @param array $data $_GET or $_POST */ public function preValidation(array $data) { // array_filter callback function findFields($field) { // return field names that include 'newName' if (strpos($field, 'newName') !== false) { return $field; } } // Search $data for dynamically added fields using findFields callback $newFields = array_filter(array_keys($data), 'findFields'); foreach ($newFields as $fieldName) { // strip the id number off of the field name and use it to set new order $order = ltrim($fieldName, 'newName') + 2; $this->addNewField($fieldName, $data[$fieldName], $order); } } /** * Adds new fields to form * * @param string $name * @param string $value * @param int $order */ public function addNewField($name, $value, $order) { $this->addElement('text', $name, array( 'required' => true, 'label' => 'Name', 'value' => $value, 'order' => $order )); }
Live Example
If you’d like to see working version of this proof of concept, please visit the live example at code.jeremykendall.net.
Summary
The ability to dynamically add form fields to Zend_Form is a feature I’d really like to see added to Zend_Form. If I were talented enough, I might attempt to make a formal proposal myself. In the meantime, what I’ve come up with can perhaps serve as a starting point for adding very simple elements to very simple forms.
Thanks again to Cory Wiles for helping me work out some of the kinks during the planning phase. Any mistakes, bad practices, or egregious coding errors are the result of my implementation, not his insight and suggestions.
Request for Comments / Peer Review
If you’ve made it this far, I’m grateful to you for hanging in with me. If you have suggestions for improvements to the code, an implementation of your own, or if you see mistakes I’ve made or poor practices that I’ve employed, I’d appreciate your input. Thank you in advance for taking the time to discuss this concept with myself and with the ZF community at large.
Full Controller, Form, and View Code
If you’re interested in the complete code for the controller, form, and views, I’ve posted them over at pastebin. Follow the links below to view / grab the code.
- Action Controller (FormsController)
- Form (Code_Form_Dynamic)
- Form view (dynamic-form-elements.phtml)
- Ajax view (newfield.ajax.phtml)
Further reading:









38 Responses
It would be nice to see the JS code converted to a jQuery plugin, where the Ajax action URL to retrieve the new field could be injected from the calling end. This would maximize the reusability of the code by removing its reliance on PHP and namespacing it (within the plugin) to prevent naming collisions.
I’m starting to think Zend_Form needs (another) complete overhaul.
Sure, the design is proper, but it’s a pain.
Before Zend_Form, I would do this all the time:
input name=”email[]”
input name=”email[]”
or
input name=”email[1234]”
input name=”email[1235]”
It was easy to add inputs using javascript, and easy to iterate over the inputs on the back-end.
But in Zend_Form, you need to create a SubForm.
And as you’ve noted above, adding more email inputs dynamically is a non-trivial matter.
Custom-looking forms and forms where you need 2 columns of labels & inputs (fairly standard) is also quite tricky.
Makes me contemplate going back to non-OO forms and using Zend’s validators & filters manually.
Hey Derek,
I relate to your frustrations. It can be tricky to accomplish custom functionality and custom decoration with Zend_Form. I’ve found that with a little extra time on the front end to customize functionality, I get a lot of mileage out of reusing that custom functionality on other projects.
For example, I spent some time working out custom decoration to mimic the look and feel of Wufoo’s forms with Zend_Form. Once I got that worked out, I’ve used it over and over with great results.
Personally, I’d *never* go back to non-OO/handmade forms. Customization takes more effort on the front end, but the benefits, IMO, far outweigh initial frustrations.
Nice work!!!
Thanks ^O^
Really useful, I was having a lot of problems constructing a dynamic form. Just one remaining error I don’t manage to solve: newfieldAction renders the whole site template: how could I make it to only show the form element, so that when it gets inserted in the site the only thing added is the form field, and not the whole site template?
Oh, it was the format parameter. Now it works.
I’m trying to achieve the same as your article but with a Zend _Dojo_Form. I could finally add dynamic elements, but all the “tundra style” for the new elements has gone.
Any ide?
Thanks
Thanks for your article!
I was wondering, how necessary is the maintenance of the ‘ID’ variable?
What if the original form element was called “name[]” and you made sure to call Zend Form Element’s setBelongsTo(true) method?
Thereafter, each dynamic element could be added with the same name of “name[]“?
To remove an element, just pop.
I’m new to Zend, so help me out!
What I have done in the past is very similar – however – I did something like this for a form that I always wanted to have one empty field (even if the JS was broken, submitting the form would generate another field to enter into)
This also demostrates how to do this with a “multi column” form.
There is >NO$v)
{
if (!isset($this->$k)) $this->addSubForm(new SNTrack_Form_ContainerEdit_OptionLine(array(“sntrackcontainer” => null)), $k);
if (substr($k, 0, 4) == ‘line’)
{
$this->_line = max($this->_line, substr($k,4) + 1); // make sure the line number exceeds previously used line numbers
}
}
return parent::isValid($data);
}
and then in ::render() – take care of making sure you have at least _numEmpty empty rows.
public function render()
{
$x = 0;
foreach ($this->getSubForms() as $form)
{
if ($form->isEmpty()) {
$x++;
}
}
for (; $x_numempty; $x++)
{
$this->addSubForm(new SNTrack_Form_ContainerEdit_OptionLine(array(“sntrackcontainer” => null)), $this->getNextLine());
}
return parent::render();
}
What I have done in the past is very similar – however – I did something like this for a form that I always wanted to have one empty field (even if the JS was broken, submitting the form would generate another field to enter into)
This also demostrates how to do this with a “multi column” form.
There is no need for the preValidation() though – you can just override isValid(), and then in ::render() – take care of making sure you have at least _numEmpty empty rows: pastebin code sample
Apologies for the previous comment that got badly formatted.
I’ve made some test and it seems that is possible to make Zend_Form_Element_MultiText and Zend_Form_View_Helper_MultiText
Using this it’s possible to generate form elements with name “myfield[]” …
By this way you can avoid subform !
I did more like Sebastian and created an element and view helper for generating additional inputs for existing elements instead of whole new elements.
The number of inputs depends on the number of items in the (comma-delimited) value string; my javascript generates that value string, an ajax call populates the element and echos it, and the callback function replaces the old element with the result.
Sébastien, can you please tell us more about your implementation of Zend_Form_Element_MultiText and Zend_Form_View_Helper_MultiText ?
Nice.
But please fix:
Notice: Zend_Loader::Zend_Loader::registerAutoload is deprecated as of 1.8.0 and will be removed with 2.0.0; use Zend_Loader_Autoloader instead in /home/jkendall/phplib/Zend/library/Zend/Loader.php on line 207
Name
@umpirsky Thanks for the heads up on that one. That’s what I get for updating ZF without testing my code afterwards
Lemme know if I missed anything.
@jeremy Works perfect now. I like the idea to fetch element through ajax.
Awesome. Thanks for the kudos.
Thanks for article. I’m Zend newbie and this article is what I’m looking for.
Good job
@Socan Thanks for the kudos! Glad it helped.
If you’re using MVC layouts you need to add the following code to the newFieldAction method:
$layout = Zend_Layout::getMvcInstance();
$layout->disableLayout();
Otherwise you get your element surrounded by your layout
Thank you for this jeremy. I’ve been able to do this for a custom multiple element now!
http://forums.zend.com/viewtopic.php?f=69&t=4599
Fantastic post you have established here! The world wide web is full of unsuitable authorship and I was grabbed by your clarity. Your determinations are exact and I will directly subscribe to your rss feed to remain up to date with your up future postings. Yes! I accept it, your publishing style is exceptional and i will now work harder on bettering mine.
I really like your example and try to implemented but once I use groups in my fields the example doesn’t work properly. Any ideas??
This is a great post and it has helped me with my project. I would also like to incorporate dojo elements such as the ValidationTextBox and decorators but I am having difficulty. The elements appear on the screen out of order, and sometimes I cannot get the new box to appear. The overall behavior is quirky to say the least. For now I will keep hacking away…
It works prefectly Thank you.
but can you shed me some light how to retrieve the dyanamic elements value once is submitted succesfully. Do i have to write a for loop ?Thanks so much in advance!
I worked out is the id element. But I’m experiencing another issue when I put the javascript code externally to .js file. It doesn’t seem to work when i have the javascript code on a .js file ps i removed the tag
Can anybody shed me some light please
Cheers
[...] (custom elements) http://www.jeremykendall.net/2009/01/19/dynamically-adding-elements-to-zend-form/ [...]
Hi Jeremy,
thanks for the example. Currently trying to implement this but when clicking on “Add Element” it always proced the following JS error:
Component returned failure code: 0×80070057 (NS_ERROR_ILLEGAL_VALUE) [nsIXMLHttpRequest.open]
Can anyone help me out here?
@Maria If I remember correctly, I was never able to get this working for groups. Since groups weren’t part of my original use-case, I never solved that part of the puzzle. Sorry!
@Christina I hope you’re able to get that one resolved. Sounds like a pain. Not having worked with Zend + Dojo, I don’t have any suggestions off the top of my head
@Ken I’m not sure what’s going on with your JavaScript issue. Code in the page and code in an external file should work in the exact same way. Make sure that you’re actually including the .js in your page. I use the FF Web Developer tool bar for that (Information -> View JavaScript). I can’t tell you how many times that’s saved my bacon.
@Oliver Make sure you’re using the same version of jQuery as in the example (at the time of writing that was jQuery 1.3.2). If you’ve copied and pasted code, make sure that it actually matches the original code (how many times have I made that mistake?). Let me know if you’re still having problems. Maybe I can help out.
Hey Jeremy,
thanks for your feedback. I’m implementing this on jQuery 1.4.2 and ZF 1.10.3. I was able to adapt your example to my needs as far as it comes to adding elements to the form. This is working just fine now. Removing though doesn’t work properly at the moment. The id is correctly reduced but the form element that shall be removed is still displayed. Didn’t found the reason for that yet…
@Oliver Gotcha. I need to update the article to reflect the latest versions of jQuery and ZF. If you beat me to the fix, let me know, otherwise I’ll post the updated version here soon. Er, soon-ish.
What is strange about the remove not working for the added elements is, that the id is reduced correctly. It seems that for some strange reason the id of the elements are not found by the remove function. If I add a manual div/id/whatever and add a remove on that in the same function it works as expected. But not with the field elements. That’s something I don’t get at the moment
Hey Oliver,
I’m working on re-writing the example for ZF 1.10.3 and jQuery 1.4.2. So far, everything has worked just fine. What browser & OS are you working with? I’ve tested it on Firefox 3.5.9 and Google Chrome 5.0.342.9 beta on Ubuntu 9.10.
If you haven’t gotten it working yet, let me know and I’ll zip up the new source code for you.
Hi!
Thank you for this!
But i don’t know a lot about jQuery, and i would like to know if it’s possible to have a zip with the whole code of this tutorial. It will be very helpfull !!!
Thanks a lot !
To remove the element try adding
$element->setAttrib(“id”,”newElement$id-”);
after your $element = new Zend_Form_Element_Text(“newName$id”);
this will make sure that the id of the element is found by your javascript.
Hi, I’m new to Zend and i’m having a problem with this project. When i click submit the dynamic form elements added disappear. Can anyone tell me what I’m doing wrong?
Thank you
Found out what the problem was. I forgot the ! in if(!$this->_request->isPost()) {
$this->view->form = $form;
return;}
Thank you for the post.
Is there any easy way to start out with more then one field-set as default? I mean I want it to start on 10 fields and then be able to add more.