7

I would like to use a conditional statement when creating a form in Symfony.

I am using a choice widget in general case. If the user selects the option "Other", I would like to display an additional text box widget. I suppose this can be done in javascript, but how can I still persist the data from 2 widgets into the same property in my entity?

I have this so far:

  $builder->add('menu', 'choice', array(
        'choices'   => array('Option 1' => 'Option 1', 'Other' => 'Other'),
        'required'  => false,
    ));
  //How to add text box if choice == Other ????

I was planing to use a DataTransfomer, but on 2 widgets??

Mick
  • 30,759
  • 16
  • 111
  • 130

1 Answers1

33

I recommend to build a custom type for that, for example ChoiceOrTextType. To this type you add both the choice (named "choice") and the text field (named "text").

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class ChoiceOrTextType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('choice', 'choice', array(
                'choices' => $options['choices'] + array('Other' => 'Other'),
                'required' => false,
            ))
            ->add('text', 'text', array(
                'required' => false,
            ))
            ->addModelTransformer(new ValueToChoiceOrTextTransformer($options['choices']))
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setRequired(array('choices'));
        $resolver->setAllowedTypes(array('choices' => 'array'));
    }
}

As you already guessed, you also need a data transformer, which can be quite simple:

use Symfony\Component\Form\DataTransformerInterface;

class ValueToChoiceOrTextTransformer implements DataTransformerInterface
{
    private $choices;

    public function __construct(array $choices)
    {
        $this->choices = $choices;
    }

    public function transform($data)
    {
        if (in_array($data, $this->choices, true)) {
            return array('choice' => $data, 'text' => null);
        }

        return array('choice' => 'Other', 'text' => $data);
    }

    public function reverseTransform($data)
    {
        if ('Other' === $data['choice']) {
            return $data['text'];
        }

        return $data['choice'];
    }
}

Now only make the "menu" field a field of that type.

$builder->add('menu', new ChoiceOrTextType(), array(
    'choices'  => array('Option 1' => 'Option 1', 'Option 2' => 'Option 2'),
    'required' => false,
));
Bernhard Schussek
  • 4,823
  • 26
  • 33
  • 2
    Thanks bernhard. I am so glad you took the time to help me with this. This is an awesome and very detailed answer, I wish I could put +100. Everything makes sense now. Many thanks for the work you have accomplished on the Form component, this is an AMAZING and very powerful tool. Thanks. – Mick Jul 25 '12 at 18:35
  • As you probably already found out, there was an `'Other' => 'Other'` too much in the last code sample. I removed that now. – Bernhard Schussek Jul 25 '12 at 19:30
  • I think that in `transform()`, if data is null, then the array should not be returned, otherwise Other will be selected by default – ncatnow Mar 29 '14 at 11:54