Using FormCollections in ZF2

FormCollection is nice when you have a form with an element that can take multiple values. The ZF2 implementation is not bad, it has some drawbacks ( lack of choosing how to render the element ), but does a decent job in the end. However, the documentation is not very explicit in the implementation. Beside setting up in the php code, some javascript code is also needed.

As as showed in a previous example, styling a FormCollection requires extending the default ZF2 classes. Going forward with a full implementation takes some JS to throw it and it should work ok for if you create something or editing it after that.

Let’s consider the scenario where you have a collection. You start with it empty, you can add new elements, but also you can remove it. The applies for both create and edit pages. So the HTML of an element from the collection will need an input but also a remove button.

For the FormCollection you need to add 3 things: the collection rendered, a button to add new elements and some JS that will take care of adding the remove button to each element.

No addition is needed in the Controller ( where you define the form, do validation etc.). If you populate the form, the Collection renderer will add you in view all the existing elements, however you need to add the remove buttons yourself.

In the Form class ( I use Factory-backed extension, guess it’s a habit from ZF1), the declaration of a Collection element you look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
   $this->add(array(
            'type' => 'Zend\Form\Element\Collection',
            'name' => 'element_name',
            'options' => array(
 
                'count' => 0,
                'should_create_template' => true,
                'allow_add' => true,
                'allow_remove' => true,
                'template_placeholder' => '__element_name__',
                'target_element' => new CustomFieldset('element_name', 'Label')
            )
        ));

The CustomFieldset is an extended Fieldset. In the ZF2 docs there is an example of an extended fieldset. Mine is a bit modified, with several parameters for flexibility:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CustomFieldset extends Fieldset implements InputFilterProviderInterface
{
 
    public function __construct($name, $label, $type = 'text')
    {
        parent::__construct($name);
        $this->setHydrator(new ClassMethodsHydrator())->setObject(new CustomEntity());
        $this->add(array(
            'name' => 'name',
            'type' => $type,
            'attributes' => array(
                'required' => 'required'
            ),
            'options' => array(
                'label' => $label
            )
        ));
    }
    public function getInputFilterSpecification()
    {
        return array(
            'name' => array(
                'required' => true,
            )
        );
    }
 
}

Of course, this is a bit incomplete, if the element needs to be a Select, you should add more code to allow setting possible values, or you can make a different Fieldset.
The CustomEntity would be simple hydrate class, with name and value as properties, their get/set methods and simple getArrayCopy()/populate() methods.
Now we need to get into the view. As I said there are 3 things we need to add. The first 2 are very simple, the collection renderer:

1
2
3
4
5
<div id="element_name_div">
<?php 
	echo $this->formCollection($form->get('element_name_div'));
?>
</div>

and the adding button:

1
2
<button onclick="return add_field('element_name_div','element_name_div')"
					class="btn btn-primary">Add new element</button>

As you can see, the button will use a js function to add a new element. What will do is take the template generated by formCollection renderer, add the remove button in this HTML code and insert everything it in the container that surrounds the collection. Since we have an form element with multiple values, we need to change the name for each element, by counting how many elements we have ( dynamic added or from default).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function add_field(name,index) {
    var currentCount = $('#'+name+'  input').length ;
    var template = $('#'+name+'  > span').data('template');
    var re = new RegExp("__"+index+"__","g");
    template = $($(template.replace(re, currentCount)));
    c = template.find('div.col-sm-5');
    d = template.find('div.input-group');
	var b = $(' <span class="input-group-btn"><button class="btn btn-default" >remove</button></span>');
	b.click(function (){$(this).parent().parent().parent().remove();});
    b.appendTo(d);
    d.appendTo(c);
    c.appendTo(template);
    $('#'+name+' ').append(template);
    return false;
}

In my case I use a modified generated HTML for the element ( like I showed in a previous post), if you use the default rendering, the code will be simpler.Here I set an event on the remove button, but you can very well just use an onclick attribute that will call a remove function ( this might be better in case your collection is not allowed to be empty, so you can do a check, see below).
For the editing page, you need to add the remove buttons in case the collection already has some elements. So when document is ready, iterate all elements and insert the button

1
2
3
4
5
6
7
8
9
10
function remove_button(e)
{
    $(e).parent().parent().parent().parent().remove();
    return false;
}
$(document).ready(function(){
    $('#element_name_div').each(function(index,value){
                $('<span class="input-group-btn"><button class="btn btn-default" onclick="return remove_button(this)" >remove</button></span>').appendTo($(this));
           });
});

In case you have a collection which has condition that it should have at least one ( or more) elements, you need to modify the remove_button() to check how many children the container div should have. Something like:

1
2
3
4
5
6
7
8
9
function remove_button(e)
{
	if($(e).children().length>2){
		   $(e).parent().parent().parent().parent().remove();
	}else {
		  alert('You need at least element!');
	}
	return false;
}

Please note that how you make the selection remove depends on the HTML structure you have. In my case, the remove button was surounded by 4 tags ( as I was using Bootstrap HTML).

Custom rendering of FormCollection Elements

A problem with ZF2 forms is that you can’t actually configure the rendering of a form element. Same (stupid) story that was in ZF1 it’s in ZF2 as well. ┬áIf you want to change the rendering the only (idiot) solution is to extend the original rendering helper and that means override the render() function, put the whole code from the original render() function ( and edit it with your changes). The issue is you copy too much code from the framework itself and you should maintain it.

Problem: need to modify the rendering to adapt the HTML to be compatible for Bootstrap3.
Solution:

Extend FormCollection and FormRow. Only one thing to change for the extended FormCollection, just set the $defaultElementHelper:

1
2
3
4
5
6
7
8
9
10
<?php
namespace Application\Form\View\Helper;
 
use Zend\Form\View\Helper\FormCollection;
 
class FieldCollection extends FormCollection
{
    protected $defaultElementHelper = 'fieldRow';
 
}

The new FieldCollection will use the view helper called fieldRow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
namespace Application\Form\View\Helper;
 
use Zend\Form\View\Helper\FormRow;
use Zend\Form\ElementInterface;
 
class FieldRow extends FormRow
{
 
public function render(ElementInterface $element)
    {
   //.... copy code from FormRow and modify with your changes
  }
}

To be able to call the new helpers, you need to register then as invokables in Module.php

1
2
3
4
5
6
7
8
9
  public function getViewHelperConfig()
    {
        return array(
            'invokables' => array(
                'fieldCollection' => 'Application\Form\View\Helper\FieldCollection',
                'fieldRow' => 'Application\Form\View\Helper\FieldRow'
            ),
           ....
}

My changes were mostly where the markup is created, so in my new FormRow renderer I have something like:

1
2
 $markup = '<div class="form-group">'. $labelOpen . $label. $labelClose . 
                                    '<div class="col-sm-5"><div class="input-group">'.$elementString .'</div>'.'</div>'.'</div>' ;

and I also set a default class for the label element

1
 $labelAttributes = array('class'=>'control-label  col-sm-5');

Now in the HTML template we just use

1
2
3
<?php 
    echo $this->fieldCollection($formElement);
?>