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:

   $this->add(array(
            'type' => 'ZendFormElementCollection',
            '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:

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:

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

and the adding button:

<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).

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

function remove_button(e)
{
    $(e).parent().parent().parent().parent().remove();
    return false;
}
$(document).ready(function(){
    $('#element_name_div').each(function(index,value){
                $('').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:

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).

1 thought on “Using FormCollections in ZF2”

Leave a Reply

Your email address will not be published. Required fields are marked *