0
votes

I've a little problem with FormEvents, I want do 3 fields populated dynamically. I explain, I've 3 fields: Project > Box > Cell, the user choose a Project, the Box list is updated, he choose a Box, the cell list is updated.

To do it, I use FormEvent like the documentation say (http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#cookbook-form-events-submitted-data)

But I've a problem, for just one field dynamically updated, it's work, but no for 2 fields... Actually a user can choose a project, and when he does it, the box field is updated. But, when he choose a box, the cell field wasn't updated...

But, I've find something, who permit it to work, just change something in a ->add() and inversed to ->add(). But I don't want it.

My code is:

$builder
    ->add('project', EntityType::class, array(
        'class' => 'AppBundle\Entity\Project',
        'choice_label' => 'name',
        'placeholder' => '-- select a project --',
        'mapped' => false,
    ))
    ->add('box', EntityType::class, array(
        'class' => 'AppBundle\Entity\Box',
        'choice_label' => 'name',
        'placeholder' => '-- select a box --',
        'choices' => [],
    ))
    ->add('cell', ChoiceType::class, array(
        'placeholder' => '-- select a cell --',
    ))
;

And when I change it to:

    builder
    ->add('box', EntityType::class, array(
        'class' => 'AppBundle\Entity\Box',
        'choice_label' => 'name',
        'placeholder' => '-- select a box --',
        //    'choices' => [],
    ))
    ->add('project', EntityType::class, array(
        'class' => 'AppBundle\Entity\Project',
        'choice_label' => 'name',
        'placeholder' => '-- select a project --',
        'mapped' => false,
    ))

    ->add('cell', ChoiceType::class, array(
        'placeholder' => '-- select a cell --',
    ))
;

It's work... But I want an empty list for box at the start, and I want project before box...

A little precision, this form is embded in an other form as a CollectionType.

All the code of this Type:

    <?php

namespace AppBundle\Form;

use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class TubeType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('project', EntityType::class, array(
                'class' => 'AppBundle\Entity\Project',
                'choice_label' => 'name',
                'placeholder' => '-- select a project --',
                'mapped' => false,
            ))
            ->add('box', EntityType::class, array(
                'class' => 'AppBundle\Entity\Box',
                'choice_label' => 'name',
                'placeholder' => '-- select a box --',
                'choices' => [],
            ))
            ->add('cell', ChoiceType::class, array(
                'placeholder' => '-- select a cell --',
            ))
        ;

        // MODIFIER
        $boxModifier = function (FormInterface $form, $project) {
            $boxes = (null === $project) ? [] : $project->getBoxes();

            $form->add('box', EntityType::class, array(
                'class' => 'AppBundle\Entity\Box',
                'choice_label' => 'name',
                'placeholder' => '-- select a box --',
                'choices' => $boxes,
            ));
        };

        $cellModifier = function(FormInterface $form, $box) {
            $cells = (null === $box) ? [] : $box->getEmptyCells();

            $form->add('cell', ChoiceType::class, array(
                'placeholder' => '-- select a cell --',
                'choices' => $cells,
            ));
        };

        // FORM EVENT LISTENER
        $builder->get('project')->addEventListener(
            FormEvents::POST_SUBMIT,
            function(FormEvent $event) use ($boxModifier) {
                $project = $event->getForm()->getData();

                $boxModifier($event->getForm()->getParent(), $project);
            }
        );

        $builder->get('box')->addEventListener(
            FormEvents::POST_SUBMIT,
            function(FormEvent $event) use ($cellModifier) {
                $box = $event->getForm()->getData();

                $cellModifier($event->getForm()->getParent(), $box);
            }
        );
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Tube'
        ));
    }
}

Thanks a lot to your help :)

1

1 Answers

0
votes

You should use $builder->addEventListener. For multiple fields all you need to do is to have dynamic fields inside FormEvents::PRE_SET_DATA event handler. Also, use parent field data, as explained in the doc to fetch child field choices.

I have used this approach, for generating Country, State and City Entities in hierarchical fields. Let me know if it helps or you need more information.

EDIT : For bigger logic, you can use eventSubscriber which will keep your code clean and you also get to re-use dynamic part of the form for somewhere else.

For multiple dependent hierarchical fields, just add them through conditions in the eventSubscriber class.

Update with code snippet :

Here is a walk through on code snippet that worked for me in Symfony 2.7

Note : I don't replace the dynamic html field as described in the document, instead via jQuery I simply collect child options as per selected parent option and fill in those. When submitted, The form recognises correct child options as per eventSubscriber context. So here is how you might do it :

In your parent Form type (where you have all 3 fields) call a eventSubscriber instead of defining those 3 fields :

$builder->add(); // all other fields..
$builder->addEventSubscriber(new DynamicFieldsSubscriber());

Create an eventSubscriber as defined in the doc, here the file name is DynamicFieldsSubscriber

<?php
namespace YourBundle\Form\EventListener;

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormInterface;

class DynamicFieldsSubscriber implements EventSubscriberInterface
{

    /**
     * Define the events we need to subscribe
     * @return type
     */
    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData', // check preSetData method below
            FormEvents::PRE_SUBMIT => 'preSubmitData', // check preSubmitData method below
        );
    }

    /**
     * Handling form fields before form renders.
     * @param FormEvent $event
     */
    public function preSetData(FormEvent $event)
    {
        $location = $event->getData();
        // Location is the main entity which is obviously form's (data_class)
        $form = $event->getForm();

        $country = "";
        $state = "";
        $district = "";

        if ($location) {
            // collect preliminary values for 3 fields.
            $country = $location->getCountry();
            $state = $location->getState();
            $district = $location->getDistrict();
        }
        // Add country field as its static.
        $form->add('country', 'entity', array(
            'class' => 'YourBundle:Country',
            'label' => 'Select Country',
            'empty_value' => ' -- Select Country -- ',
            'required' => true,
            'query_builder' => function (EntityRepository $er) {
                return $er->createQueryBuilder('c')
                        ->where('c.status = ?1')
                        ->setParameter(1, 1);
            }
        ));
        // Now add all child fields.
        $this->addStateField($form, $country);
        $this->addDistrictField($form, $state);
    }

    /**
     * Handling Form fields before form submits.
     * @param FormEvent $event
     */
    public function preSubmitData(FormEvent $event)
    {
        $form = $event->getForm();
        $data = $event->getData();

        // Here $data will be in array format.

        // Add property field if parent entity data is available.
        $country = isset($data['country']) ? $data['country'] : $data['country'];
        $state = isset($data['state']) ? $data['state'] : $data['state'];
        $district = isset($data['district']) ? $data['district'] : $data['district'];

        // Call methods to add child fields.
        $this->addStateField($form, $country);
        $this->addDistrictField($form, $state);
    }

    /**
     * Method to Add State Field. (first dynamic field.)
     * @param FormInterface $form
     * @param type $country
     */
    private function addStateField(FormInterface $form, $country = null)
    {
        $countryCode = (is_object($country)) ? $country->getCountryCode() : $country;
        // $countryCode is dynamic here, collected from the event based data flow.
        $form->add('state', 'entity', array(
            'class' => 'YourBundle:State',
            'label' => 'Select State',
            'empty_value' => ' -- Select State -- ',
            'required' => true,
            'attr' => array('class' => 'state'),
            'query_builder' => function (EntityRepository $er) use($countryCode) {
                return $er->createQueryBuilder('u')
                        ->where('u.countryCode = :countrycode')
                        ->setParameter('countrycode', $countryCode);
            }
        ));
    }

    /**
     * Method to Add District Field, (second dynamic field)
     * @param FormInterface $form
     * @param type $state
     */
    private function addDistrictField(FormInterface $form, $state = null)
    {
        $stateCode = (is_object($state)) ? $state->getStatecode() : $state;
        // $stateCode is dynamic in here collected from event based data flow.
        $form->add('district', 'entity', array(
            'class' => 'YourBundle:District',
            'label' => 'Select District',
            'empty_value' => ' -- Select District -- ',
            'required' => true,
            'attr' => array('class' => 'district'),
            'query_builder' => function (EntityRepository $er) use($stateCode) {
                return $er->createQueryBuilder('s')
                        ->where('s.stateCode = :statecode')
                        ->setParameter('statecode', $stateCode);
            }
        ));
    }
}

After this, you need to write jQuery events which should update child options on change of parent option explicitly, You shouldn't face any error on submission of the form.

Note : The code above is extracted and changed for publishing here. Take care of namespace and variables where ever required.