6

Before Symfony 2.7, the attr value for a choice field applied only to the field itself, i.e. the <select> element that was rendered. I used this to apply classes to this element to style it.

In Symfony 2.7 this behavior was changed. Now, all <option> children of the <select> element also get the same attributes (commit of the change) and therefore classes.


For some clarification, let this be the code:

<?php echo $view['form']->widget($form['myField'], ['attr' => ['class' => "text ui-widget-content ui-corner-all"]]); ?>

Then this is the output of Symfony <=2.6:

<select class="text ui-widget-content ui-corner-all" name="myField">
    <option value="1">Option 1</option>
    <option value="2">Option 2</option>
</select>

And this is the output of Symfony >= 2.7:

<select class="text ui-widget-content ui-corner-all" name="myField">
    <option value="1" class="text ui-widget-content ui-corner-all">Option 1</option>
    <option value="2" class="text ui-widget-content ui-corner-all">Option 2</option>
</select>

The classes I apply are not suitable for <option> elements as they define borders and the like for the actual field. Note that these are classes defined by jQuery UI so I can't easily change their definition.

What is the easiest way to avoid applying these classes to all <option> elements of a choice field while still applying it to the <select> element?

Chris
  • 6,914
  • 5
  • 54
  • 80
  • Maybe it's a bug. Cause I couldn't find anything in the UPGRADE-2.7 for it, and that functionality is what the `'choice_attr'` option is supposed to do. – user2268997 Aug 17 '15 at 14:13
  • @user2268997 Yes, I couldn't find anything either but looking at the code I linked it seems very intentional to me. – Chris Aug 17 '15 at 14:18
  • I don't know much about php templates, but it seems like 'attr' is resolved from `$choice` which is a `ChoiceView` instance, which is created by the `DefaultChoiceListFactory`, and the `choice_attr` option is passed to that factory in the `ChoiceType` definition.( if you don't supply own `ChoiceList` ofcourse) – user2268997 Aug 18 '15 at 03:17

2 Answers2

5

Thanks to the comment about choice_attr by @user2268997 I found the related blog post New in Symfony 2.7: Choice form type refactorization which details the use of the (as of now undocumented) choice_attr option.

It seems Symfony merges the attributes in choice_attr with the ones in attr when rendering the field. This means we need to overwrite the class attribute in choice_attr.

I tried doing this in the code next to where I define attr but had no luck. It seems you need to do this in your form type definition. Here is an excerpt from my form after adding the choice_attr option:

namespace MyBundle\Form;

public function buildForm(FormBuilderInterface $builder, array $options) {
    $builder
        ->add('roles',
            'entity',
            [
                'class' => 'MyBundle:Role',
                'choice_label' => 'name',
                'multiple' => true,
                'choice_attr' => function () { return ["class" => ""]; }
            ]);
}

The result is as I had hoped. I will probably also refactor this to my own custom form type so I do not need to repeat it all over my bundle.


I have now decided to create a custom choice type with the desired behavior described above and use that one throughout my application.

Here is my choice type:

use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ChoiceNoOptAttrType extends ChoiceType {
    public function configureOptions(OptionsResolver $resolver) {
        parent::configureOptions($resolver);

        $resolver->setDefault("choice_attr", function () { return ["class" => ""]; });
    }
}

I did not feel like refactoring all my existing forms to use this new type, so instead I opted to replace the Symfony-provided choice type with mine. This can be achieved by modifying the service configuration for the choice form type. To do this, I created a compiler pass for my bundle.

Further reading: Creating a Compiler Pass

namespace MyBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class MyCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition("form.type.choice");
        $definition->setClass('MyBundle\Form\ChoiceNoOptAttrType');
    }
}

Now all that is left to do is register the compiler pass in the bundle.

Further reading: How to Work with Compiler Passes in Bundles

namespace MyBundle;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use MyBundle\DependencyInjection\Compiler\MyCompilerPass;

class MyBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new MyCompilerPass());
    }
}

And this is it. Now all my choice fields are using my custom class which makes sure that the CSS class set in attr is not propagated to my <option> elements.

Chris
  • 6,914
  • 5
  • 54
  • 80
1

There might be a simpler solution, but you might want to take a look at Form Themes. Override the Template for choice_widget_options so that the classes are not applied to the option tags.

{%- block choice_widget_options -%}
    {% for group_label, choice in options %}
        {%- if choice is iterable -%}
            <optgroup label="{{ choice_translation_domain is sameas(false) ? group_label : group_label|trans({}, choice_translation_domain) }}">
                {% set options = choice %}
                {{- block('choice_widget_options') -}}
            </optgroup>
        {%- else -%}
            {% set attr = choice.attr %}
            <option value="{{ choice.value }}" {# DELETE THIS PART: {{ block('attributes') }}#}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is sameas(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}</option>
        {%- endif -%}
    {% endfor %}
{%- endblock choice_widget_options -%}
LorenzSchaef
  • 1,523
  • 10
  • 16