How can I render an entity (choice <select>)

2019-02-27 18:32发布

问题:

Symfony renders an entity field type like a choice dropdown - a select, basically. However, the CSS framework that I'm using defines a sort of 'select' as a ul and li as the options. The Custom Field Type documentation gives no help on this scenario.

I'm converting my code from manual HTML rendering of the form dropdown to symfony form's version using twig and form_widget(). However, I want a ul and li instead of a select.

The manual way of creating my dropdown is:

<ul class='dropdown-menu'>
    {% for locator in locators %}
        <li>
            <a href="#" data-id="{{locator.getId() }}">
                {{ locator.getName() }}
            </a>
        </li>
    {% endfor %}
</ul>

That's how I would render my dropdown manually before using symfony forms. It looks like this:

I like it. I think it looks awesome. Now, if I'm using Symfony forms, I can just use this instead:

{{ form_start(form) }}
    {{ form_widget(form.locator) }} {# This is my locator dropdown #}
    {{ form_widget(form.target) }} {# Ignore this #}
{{ form_end(form) }}

The problem is that this renders this instead:

I can't add my custom CSS here because this is rendered as a select instead of an unordered list and lis.

In case it may help, here's my form type being built:

/**
 * {@inheritDoc}
 */
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('target')
            ->add('locator', 'entity', [
                'class'         => 'Application\Model\Entity\Locator',
                'query_builder' => function(EntityRepository $repo) {
                    return $repo->createQueryBuilder('e');
                },
                'empty_value'   => 'Locator'
            ])
            ->add('save', 'submit', ['label' => 'Save']);

    $builder->setAction($this->urlGenerator->generate('page_create_element', [
        'suiteId' => $options['suiteId'], 'pageId' => $options['pageId']
    ]))->setMethod('POST');
}

The Question: Is there any way I can have the form commands above auto-generate my ul / li requirement instead of selects, or do I have to render this manually instead and ignore the symfony forms component for this?

回答1:

Thanks to some of the posters above, there was some information from Form Theming, but it wasn't exactly enough to go along with so I had to do a little bit of digging on github.

According to the documentation, Symfony uses twig templates to render the relevant bits of a form and it's containing elements. These are just {% block %}s in twig. So the first step was to find where a select button is rendered within the symfony codebase.

Form Theming

Firstly, you create your own theme block in it's own twig file and you apply this theme to your form with the following code:

{% form_theme my_form_name 'form/file_to_overridewith.html.twig %}

So if I had overridden {% block form_row %} in the file above, then when I called {{ form_row(form) }} it would use my block instead of Symfony's default block.

Important: You don't have to override everything. Just override the things you want to change and Symfony will fall back to it's own block if it doesn't find one in your theme.

The Sourcecode

On github I found the source code for Symfony's "choice widget". It's a little complex but if you follow it through and experiment a little bit you'll see where it goes.

Within the choice_widget_collapsed block, I changed the select to uls and options to lis. Here's the theme file I created, note the minor differences described above:

{# Symfony renders a 'choice' or 'entity' field as a select dropdown - this changes it to ul/li's for our own CSS #}

{%- block choice_widget_collapsed -%}
    {%- if required and empty_value is none and not empty_value_in_choices and not multiple -%}
        {% set required = false %}
    {%- endif -%}
    <ul {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
        {%- if preferred_choices|length > 0 -%}
            {% set options = preferred_choices %}
            {{- block('choice_widget_options') -}}
            {%- if choices|length > 0 and separator is not none -%}
                <li disabled="disabled">{{ separator }}</li>
            {%- endif -%}
        {%- endif -%}
        {%- set options = choices -%}
        {{- block('choice_widget_options') -}}
    </ul>
{%- endblock choice_widget_collapsed -%}

{%- block choice_widget_options -%}
    {% for group_label, choice in options %}
        {%- if choice is iterable -%}
            <optgroup label="{{ group_label|trans({}, translation_domain) }}">
                {% set options = choice %}
                {{- block('choice_widget_options') -}}
            </optgroup>
        {%- else -%}
            <li value="{{ choice.value }}"{% if choice is selectedchoice(value) %} selected="selected"{% endif %}><a href="#">{{ choice.label|trans({}, translation_domain) }}</a></li>
        {%- endif -%}
    {% endfor %}
{%- endblock choice_widget_options -%}

Rendering

Now I can render my form with the following:

{{ form_widget(form.locator, {'attr': {'class': 'dropdown-menu'}}) }}

This uses my theme for the choice dropdown which contains ul and li tags instead of select and option ones. Pretty simple once you know where to look for the original code! The rendered HTML:

<ul id="elementtype_locator" name="elementtype[locator]" required="required" class="dropdown-menu">
    <li value="1"><a href="#">id</a></li>
    <li value="2"><a href="#">name</a></li>
    <li value="3"><a href="#">xpath</a></li>
</ul>

I also had to remove one of the lines that put 'Locator' at the top of the dropdown as there were four dropdown choices (including the empty_data one) instead of three.