This article is based on the following doc articles:
http://symfony.com/doc/current/cookbook/form/form_collections.html
http://symfony.com/doc/2.7/reference/forms/types/collection.html
http://toni.uebernickel.info/2012/03/15/an-example-of-symfony2-collectiontype-form-field-prototype.html
In my example below I will be using
Doctrine's
One-To-Many Bidirectional relation. Let's say we have a
Feedback form to which you can add none or many
Actions to be taken. Feedback form contains information like name and email of the person and Action form contains information like action description, deadline date, importance level. I will eliminate any details which are not relevant to the subject (like all the mappings, validations etc)/
Entities:
Feedback
<?php
class Feedback
{
/**
* @ORM\OneToMany(targetEntity="\MyBundle\Entity\Action", mappedBy="fkFeedback", cascade={"persist","remove"})
*/
private $actions;
private $name;
private $email;
public function __construct() {
$this->actions = new ArrayCollection();
}
public function getActions()
{
return $this->actions;
}
public function addAction(\MyBundle\Entity\Action $action)
{
$action->setFkIdFeedback($this);
$this->actions->add($action);
return $this;
}
public function removeAction($action)
{
if ($this->actions>contains($action)) {
$this->actions->removeElement($action);
}
return $this;
}
}
Action
<?php
class Action
{
private $actionDesc;
private $importance;
private $deadlineDate;
/**
* @ORM\ManyToOne(targetEntity="Feedback", inversedBy="actions")
* @ORM\JoinColumn(name="fk_feedback", referencedColumnName="id", nullable=FALSE )
*/
private $fkFeedback;
}
Note mappedBy and inversedBy properties. Also on the One side I have added information regarding how Doctrine should persist the associated entities: cascade={'persist'}.
Also 'addAction()' function makes sure the changes are reflected in both sides of the relation.
Forms
<?php
class FeedbackType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name','text',array('label' => false, 'required' => false, 'attr'=> array('placeholder'=> 'Last name')))
->add('email','email',array('label' => false, 'required' => false, 'attr'=> array('placeholder'=> 'Email')))
->add('actions', 'collection', array(
'type' => new ActionType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'cascade_validation' => true,
'error_bubbling' => false,
));
}
/**
* @param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\Feedback',
'cascade_validation' => true,
));
}
/**
* @return string
*/
public function getName()
{
return 'mybundle_feedback';
}
}
On the FeedbackType class (One side from the One-To-Many relation) I set a collection of ActionType forms. Also 'error_bubbling' to false because I want to get errors from Action form under each field and not only at the form level: form_error(form) .
The
ActionType class has nothig special in it, I will not listed here.
Allowing the user to dynamically add new Actions means that you'll need to
use some JavaScript.
The option 'a
llow_add
' is needed to this flexibility to be possible.
In addition to telling the field to accept any number of submitted objects, the
allow_add
also makes a
"prototype" variable available to you. This "prototype"
is a little "template" that contains all the HTML to be able to render any
new "tag" forms.
If you render the entire form you will see in a HTML data-* attribute the prototype for creating new Actions. From here on I will not follow the example found in official documentation, instead I will render the form and use jQuery code like in this
blog article.
Rendering the form
The prototype macro
In the situation when by default I want to render 2 actions to be filled and allow dynamically to be added other there may be differences between how the widget is rendered and how the prototype is rendered. The below Twig macro handles this issue.
This macro renders the
prototype
and the actual
widget
the same way.
Therefore the resulting usage code is very little and required javascript code just works for all collections.
{% macro widget_prototype(widget, remove_text) %}
{% if widget.vars.prototype is defined %}
{% set form = widget.vars.prototype %}
{% set name = widget.vars.prototype.vars.name %}
{% else %}
{% set form = widget %}
{% set name = widget.vars.full_name %}
{% endif %}
<div data-content="{{ name }}" class="panel panel-default">
<div class="panel-body">
{{ form_row(form.actionDesc) }}
{{ form_row(form.importance) }}
{{ form_row(form.deadlineDate, {'attr':{'class':'actions-deadline'}}) }}
<a class="btn btn-danger" data-related="{{ name }}">{{ remove_text }}</a>
</div>
</div>
{% endmacro %}
As an example I customized the class attribute for
deadlineData to be able to identify them for adding
Datepicker jQuery UI plugin. The values
widget.vars.full_name and
widget.vars.name are related to form object
not to my actual Entity fields (name, email).
The actual form rendering:
I am not rendering the form using form_start, form_end in order to get more control on what is displayed. If you are using Bootstrap you can use one of the Twig form layouts offered by Symfony team:
{% form_theme form 'MyBundle:Form:bootstrap_3_layout.html.twig' %}
<div class="row">
<div class="col-sm-3"></div>
<div class="col-sm-6">
<h3 style="color:white;">Feedback</h3>
<form name="docsbundle_feedback" method="POST">
{{ form_errors(form) }}
<fieldset>
{{ form_row(form.name) }}
{{ form_row(form.email) }}
<hr>
<h3 style="color:white">Actions</h3>
<div>
<div id="actions" data-prototype="{{ _self.widget_prototype(form.actions, 'Remove action')|escape }}">
{% for widget in form.actions.children %}
{{ _self.widget_prototype(widget, 'Remove action') }}
{% endfor %}
</div>
<a id="add-action" class="btn btn-info" data-target="actions">Add action</a>
</div>
<hr>
{{ form_widget(form._token) }}
{{ form_widget(form.submit, {'attr':{'class':'btn-info'}}) }}
</fieldset>
</form>
</div>
<div class="col-sm-3"></div>
</div>
jQuery snippet:
The below jQuery take care to allow adding and removing Actions.
{% block javascripts %}
<script>
$('#add-action').click(function(event) {
var collectionHolder = $('#' + $(this).attr('data-target'));
var prototype = collectionHolder.attr('data-prototype');
var form = prototype.replace(/__name__/g, collectionHolder.children().length);
collectionHolder.append(form);
return false;
});
$('#actions').on('click','.btn-danger', function(event) {
var name = $(this).attr('data-related');
$('*[data-content="'+name+'"]').remove();
return false;
});
</script>
{% endblock %}
Controller
In controller I am persisting only the Feedback entity, and with the option cascade={'persis'}, Doctrine knows to take care of persisting the associated Action objects
<?php
public function feedbackAction(Request $request)
{
$entity = new Feedback();
$empty_action = new Action();
$entity->addAction($empty_action);
$form = $this->feedbackCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
.....
// persist the $entity
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
...