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.