Symfony2 Community Series: Adding variables to twig output

The rest of this blog post was written by @mablea.

Yesterday @userfriendly posted a interesting Question on Freenode/#symofony:

He asked about how to add class attributes to the `option` Tags of a EntityType.php form field. After looking at the code we found out that this is not possible with the native behavior of the type EntityType or with any `select` field.

Then I had the idea to work the variables I can access without making changes to the Symfony2 Modules. Here is the solution:

I introduced two new methods to the Entity class.

    <?php
    /* /src/Acme/DemoBundle/Entity/Category.php */
 
    /*  (...) */
 
    /**
     * @return boolean
     */
    public function isTopLevel()
    {
        return $this->getParent() == null ? true : false;
    }
 
 
    /**
     * return array
     */
    public function getCategoryTypeTitle()
    {
        return array('isTopLevel' => $this->isTopLevel(), 'label' => $this->__toString());
    }

Since the default Twig template for the rendering asumes the given label is a `string` we need to introduce a new FormType called `CategoryType.php` in this case.

    <?php
    // /src/Acme/DemoBundle/Form/CategoryType.php
 
    namespace Acme\DemoBundle\Form\Type;
 
    use Doctrine\Common\Persistence\ObjectManager;
    use Symfony\Component\Form\AbstractType;
    use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
 
    class CategoryType extends AbstractType
    {
        public function getName()
        {
            return 'category';
        }
 
        public function getParent()
        {
            return 'entity';
        }
    }

Next, we register the new form type in our `config.yml`:

This step is optional, but if we want to call our new FormType in string form, like the built-in ones (´’entity’, ‘coiche’, .. ´), we need it.

    # /app/config/config.yml
    services:
        form.type.category:
            class: Acme\DemoBundle\Form\Type\CategoryType
            tags:
                - { name: form.type, alias: category }

Now we overwrite the twig template for our Form Type using the ´fields.html.twig´:

    {# /src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}
    
    {% block category_choice_widget_options %}
    {% spaceless %}
    {% for index, choice in options %}
    {% if _form_is_choice_group(choice) %}
    <optgroup label="{{ index|trans({}, translation_domain) }}">
        {% for nested_choice in choice %}
        <option value="{{ nested_choice.value }}"{% if _form_is_choice_selected(form, nested_choice) %} selected="selected"{% endif %}>{{ nested_choice.label|trans({}, translation_domain) }}</option>
        {% endfor %}
    </optgroup>
    {% else %}
    <option {% if choice.label.isTopLevel %}class="isTopLevel"{% endif %} value="{{ choice.value }}"{% if _form_is_choice_selected(form, choice) %} selected="selected"{% endif %}>{{ choice.label.label|trans({}, translation_domain) }}</option>
    {% endif %}
    {% endfor %}
    {% endspaceless %}
    {% endblock category_choice_widget_options %}

The important part is overwritten in the block above. Look at the ´\

    {# /src/Acme/DemoBundle/Resources/views/Form/fields.html.twig #}

    {% block category_choice_widget_collapsed %}
    {% spaceless %}
    <select {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
        {% if empty_value is not none %}
        <option value="">{{ empty_value|trans({}, translation_domain) }}</option>
        {% endif %}
        {% if preferred_choices|length > 0 %}
        {% set options = preferred_choices %}
        {{ block('choice_widget_options') }}
        {% if choices|length > 0 and separator is not none %}
        <option disabled="disabled">{{ separator }}</option>
        {% endif %}
        {% endif %}
        {% set options = choices %}
        {{ block('category_choice_widget_options') }}
    </select>
    {% endspaceless %}
    {% endblock category_choice_widget_collapsed %}
 
    {% block category_widget %}
    {% spaceless %}
    {% if expanded %}
    <ul {{ block('widget_container_attributes') }}>
        {% for child in form %}
        <li>
            {{ form_widget(child) }}
            {{ form_label(child) }}
        </li>
        {% endfor %}
    </ul>
    {% else %}
    {# just let the choice widget render the select tag #}
    {{ block('category_choice_widget_collapsed') }}
    {% endif %}
    {% endspaceless %}
    {% endblock %}

Last step is to tell the form component where our template is on the filesystem:

    # /app/config/config.yml
    
    twig:
        # ...
        form:
            resources:
                - 'AcmeDemoBundle:Form:fields.html.twig'

Now, in our `buildForm()` method we can use the new CategoryType like this:

        // add your custom field
        $builder->add('category', 'category', array(
            'class' => 'AcmeDemoBundle:Category',
            'property' => 'CategoryTypeTitle'
        ));

The `property` option points to the method used to get the label. But we return our array holding the label and a boolean value (`isTopLevel`). This works without errors so far I can say. But I didn’t make any unit tests our deep analysis on it. Only tested with dev-master (v2.1x)!

This method is hackish. But until version 2.2 Symfony2 will not support this feature by default. At the moment there is a feature freeze on Form Component until the version 2.1 of Symfony2 is out.

You could also use this method to add ´data-´ attributes to your choices and read them out with Jquery on the client side.

I hope you like thi sone shared with you in agreement with its author.
Symfony2 community FTW!

One thought on “Symfony2 Community Series: Adding variables to twig output

  1. I searched for 2 days how to do this. Thanks a lot !
    Note : in Symfony 2.3, just use {% if choice is iterable %} instead of {% if _form_is_choice_group(choice) %}

Leave a Reply to N2 Cancel reply

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