Andrei Herford
Andrei Herford

Reputation: 18733

Customize data-prototype attribute in Symfony 2 forms

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

Answers (1)

user2182349
user2182349

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

Related Questions