Reputation: 18733
I am trying to follow the Symfony 2.7 docs to create Embed a Collection of Forms using a custom Collection Prototype.
Problem is, that I am not able to create a custom collection prototype as described in the docs.
As in the example there are two simple classes:
A Task
class that manages the description of the task and additionally any number of tags, represented by its own Tag
class
class Task {
protected $description;
protected $tags;
public function __construct() {
$this->tags = new array();
}
// Getter & Setter for description + additional addTag & removeTag methods
// ...
// Tags getter
public function getTags() {
return $this->tags;
}
}
class Tag {
protected $name;
// ... setName(...), getName()...
}
These are the custom form types:
class TaskType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('description');
$builder->add('tags', 'collection', array('type' => new TagType()));
}
public function getName() {
return 'task';
}
// ...
}
class TagType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name');
}
public function getName() {
return 'tag';
}
// ...
}
Twig file to render the form
{{ form_start(form) }}
{# render the task's only field: description #}
{{ form_row(form.description) }}
{# render tags - use table instead of ul as in example #}
<div class="table-responsive">
<table class="table">
<thead>
<th>{{ 'task.tag.headline'|trans }}</th>
</thead>
<tbody class="tags-container" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
{{ form_row(form.rules) }}
</tbody>
</table>
</div>
{{ form_end(form) }}
This works fine and renders the tags
list inside the table. However, this code uses the default prototype, that (of course) does not create table rows for the different tags.
I tried to add the code to use a custom prototype as described in the docs. How ever the docs does not say anything about where to add this code or how to use it:
Twig code WITH custom prototype code
{{ form_start(form) }}
{# Custom Prototype Code from docs #}
{% form_theme form _self %}
{% block _tags_entry_widget %}
<tr>
<td>{{ form_widget(form.name) }}</td>
</tr>
{% endblock %}
{# render the task's only field: description #}
{{ form_row(form.description) }}
{# render tags - use table instead of ul as in example #}
<div class="table-responsive">
<table class="table">
<thead>
<th>{{ 'task.tag.headline'|trans }}</th>
</thead>
<tbody class="tags-container" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
{{ form_row(form.rules) }}
</tbody>
</table>
</div>
{{ form_end(form) }}
Using the custom prototype code like this results in the error:
Method "name" for object "Symfony\Component\Form\FormView" does not exist in "MyAppBundle:Task:task.html.twig"
This sounds reasonable, since name
belongs to the Tag
class and not to the Task
class.
Problem 1: How to use/access the Tag
form inside the template?**
I removed <td>{{ form_widget(form.name) }}</td>
from the prototype template and replaced it with <td>Test</td>
to see if the template is used. The result: The template is NOT used and has no effect.
Problem 2: What is the correct way to set/activate the prototype template?
I found other threads dealing with prototype question/problems. The answers propose different solutions using macros, external twig files, etc. Since the Symfony docs seems to offer a solution within the same file without using hacks like macros, I would like to know implement this solution.
Upvotes: 2
Views: 4357
Reputation: 9782
This is working for me - I am sorry - it is Symfony 3, but you should be able to translate.
In the buildForm method of my PersonType class, I have a CollectionType of emails
->add( 'emails', CollectionType::class, [
'label' => 'common.email',
'entry_type' => AppEmailType::class,
'by_reference' => true,
'required' => false,
'label' => false,
'empty_data' => null,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'mapped' => false,
'prototype_name' => '__email__'
] )
The template to render the PersonType includes the emails form like so
{% include 'common/emails.html.twig' with {'form': form.emails } %}
common/emails.html.twig
This template is the container for the collection
<div class="emails">
<span class="sub-form-legend">{{'common.email'|trans}}</span>
{{form_row(form)}}
{% if form.vars.allow_add %}
<div class="add-one-more-row">{{ 'common.add_one_more'|trans}}</div>
{% endif %}
</div>
In fields.html.twig, I have an entry_row template specific to the form, which uses a common email template defined in the same file.
{% block _user_person_emails_entry_row %}
{{ block('_emails_entry_row') }}
{% endblock %}
{% block _emails_entry_row %}
{% spaceless %}
<div class="form-row email">
<span class="type-select">{{ form_widget(form.type) }}</span>
<span class="input">{{ form_widget(form.email) }}</span>
<span class="comment">{{ form_widget(form.comment) }}</span>
<span class="remove-form-row" title="{{'common.remove'|trans}}" id="email-__email__"><i class="fa fa-remove"></i></span>
</div>
{% endspaceless %}
{% endblock %}
To find out the name, trace the names of your forms and prefix them with an underscore. Use {{dump(form)}} to get the unique name.
The HTML will be placed in the data-prototype attribute.
Upvotes: 3