How can I avoid applying attr to all options of my choice field?
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.
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 -%}