Skip to content

Instantly share code, notes, and snippets.

@mablae
Created July 21, 2012 18:25
Show Gist options
  • Save mablae/3156670 to your computer and use it in GitHub Desktop.
Save mablae/3156670 to your computer and use it in GitHub Desktop.

Adding variables to twig output

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 at all with the native behavior of the 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 builtin 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 ´<option {% if choice.label.isTopLevel %}class="isTopLevel"{% endif %}´ and ´label.label|trans({}´ parts of the code. We access the the label template variable as an array. With this method we can transport more variables than just key=>label. Here we overwrite the inherited templates with our own ones, to use the block above:

{# /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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment