javascriptphphtmlformssymfony

Symfony Forms - dynamically add/remove fields depending on choices


I'm trying to create a form that maps to an entity of the type "Participant". A participant is in a one-to-one relationship with a 'person'. Adding a participant, I want to first give the option to choose a person already in the database and if the right one doesn't exist, create that person with the participant form.

This works if I do it with two pages/forms. The first one trying to choose an existing person, otherwise open a new page with the different form. First page:

$form->add('person', AjaxEntityType, [ // EntityType but with select2 ajax
    'class' => Person::class,
    'remote_route' => 'person_ajax_list'
]);

Second page:

$participant->setPerson(new Person());
$form->add('person', PersonType::class);
// adds PersonType fields to the Participant form

Well, that works, but it's terribly slow and unecessary. What I'd rather want is having BOTH of those shown, where the PersonType form fields (first name, last name, title, company, address, etc.) are automatically populated with the persons data, if one is selected. Otherwise, if no Person is selected and the form is submitted with data entered, a new Person should be created and persisted in the database.

It's sadly not possible to render the 'person' twice, once as a dropdown and once as a PersonType form. So how would I go about achieving what I want, without surreal amounts of JavaScript?

My current solution would be to manually create all the required fields with JavaScript and populate them with the person data I'd get with another Ajax request on a onchange event on the person dropdown, then in the PRE_SUBMIT event of the form, remove the 'person' field and add it again as a PersonType field, check if the entered data corresponds to an existing person or a new one and then act accordingly. There has to be a better solution, right?

Form events have sadly otherwise proven majorly pointless, as it's not possible to attach an event listener to a 'change' event on one of the fields.

Thanks.


Solution

  • Ended up solving it with an unmapped person choice field and javascript to automatically update the data (using ajax).

    participant/add.twig:

    {% block javascripts %}
    
        <script type="text/javascript">
    
            $(document).ready(function () {
                function onTrainerChange() {
                    let trainerId = $('#participant_person_choose').val();
                    $.get(Routing.generate('person_data_ajax', { id: trainerId }), function (data) {
                        $('#participant_person_gender').val(data.gender);
                        $('#participant_person_title').val(data.title);
                        $('#participant_person_firstName').val(data.firstName);
                        $('#participant_person_lastName').val(data.lastName);
                        $('#participant_person_email').val(data.email);
                        $('#participant_person_telephone').val(data.telephone);
                        if (data.company) {
                            let company = $('#participant_person_company');
                            company.empty();
                            company.append(new Option(data.company.text, data.company.id));
                            company.val(data.company.id);
                            company.trigger('change');
                            // manipulate dom directly because of .select('data') bug with select2 >=4.0
                        }
                    });
                };
                
                let trainer = $('#participant_person_choose');
                trainer.change(onTrainerChange);
            });
    
        </script>
    
    {% endblock %}
    

    ParticipantController add:

        $participant = new Participant($seminar);
        $person = $participant->getPerson() ?? new Person();
        $participant->setPerson($person);
        $form = $this->createParticipantForm($participant)
            ->add('person_choose', AjaxEntityType::class, [
                'mapped' => false,
                'class' => Person::class,
                'remote_route' => 'person_select_ajax',
                'placeholder' => 'form.personCreate',
                'label' => 'form.person'
            ])
            ->add('person', PersonType::class);
    
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
    
            if ($form->get('reservation')->getData()) {
                $participant->setInterested();
            }
    
            $personEntered = $form->get('person')->getData();
            $personChosen = $form->get('person_choose')->getData();
            if ($personChosen) {
                $person = $personChosen;
                $person->setGender($personEntered->getGender());
                $person->setTitle($personEntered->getTitle());
                $person->setFirstName($personEntered->getFirstName());
                $person->setFirstName($personEntered->getLastName());
                $person->setCompany($personEntered->getCompany());
                $person->setEmail($personEntered->getEmail());
                $person->setTelephone($personEntered->getTelephone());
                $participant->setPerson($person);
            }
    
            $this->getDoctrine()->getManager()->persist($person);
    
            $this->getDoctrine()->getManager()->persist($participant);
        }
    

    PersonController Ajax:

        /**
         * @Route("/{id}/data", name="person_data_ajax", methods={"GET"}, options={"expose": true})
         */
        public function dataAjax(Person $person, PhoneNumberHelper $phonenumberHelper)
        {
            $arr = [
                'id' => $person->id,
                'gender' => $person->getGender(),
                'title' => $person->getTitle(),
                'firstName' => $person->getFirstName(),
                'lastName' => $person->getLastName(),
                'email' => $person->getEMail(),
                'telephone' => $person->getTelephone() ? $phonenumberHelper->format($person->getTelephone(), PhoneNumberFormat::NATIONAL) : null,
                'company' => $person->getCompany() ? [
                    'id' => $person->getCompany()->id,
                    'text' => $person->getCompany()->__toString()
                ] : null
            ];
    
            return new JsonResponse($arr);
        }
    

    Hope this can help someone else. Really disappointed with how limited Symfonys Forms are.

    Edit: Five years later, thanks to the Symfony UX initiative, there are better ways to solve this using Live Components.