0

I want to dynamically fill in my form (see picture)

countries states and cities

The states select (html tag) is populated with a json file by the javascript function :

function onChangeCountries(countries, states) {
    $(countries).on("change", function(ev) {
        var countryId = $(this).val();

        if (countryId != '') {

            states.empty();
            states.append('<option selected="true" disabled>Choose state</option>');
            states.prop('selectedIndex', 0);

            $.getJSON(statesUrl, function (data) {
                $.each(data['states'], function (key, entry) {
                    if (entry.country_id == countryId) {
                        states.append($('<option></option>').attr('value', entry.id).text(entry.name));
                    }
                })
            });
        }
    });
}

countries and states parameters correspond to the two firsts select. This function populate the states select corresponding to the country selected. The same mechanism operates with the cities select.

Now I want to use Symfony 4, here is my form :

<?php

// src/Form/LocationType.php
namespace App\Form;

use App\Entity\Location;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class LocationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $countries = json_decode(file_get_contents("js/countries.json"), TRUE)['countries'];
        $countriesChoices = array();

        foreach($countries as $country) {
            $countriesChoices[$country['name']] = $country['id'];
        }

        $states = json_decode(file_get_contents("js/states.json"), TRUE)['states'];
        $statesChoices = array();

        foreach($states as $state) {
            $statesChoices[$state['name']] = $state['id'];
        }

        $cities = json_decode(file_get_contents("js/cities.json"), TRUE)['cities'];
        $citiesChoices = array();

        foreach($cities as $city) {
            $citiesChoices[$city['name']] = $city['id'];
        }

        $builder
            ->add('country', ChoiceType::class, array(
                'choices' => $countriesChoices
            ))
            ->add('area', ChoiceType::class, array(
                'choices'  => $statesChoices
            ))
            ->add('city', ChoiceType::class, array(
                'choices'  => array(
                    'Choose city' => $citiesChoices
            ))
            ->add('zip_code', TextType::class)
            ->add('street', TextType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Location::class,
        ));
    }
}

The problem here is that I can not fill in the choices depending on user inputs. Also I'm receiving an OutOfMemoryException (there is more than 40 000 cities in cities.json).

Edit

After dlondero answer, I haven't resolve my problem, here is my new LocationType class :

<?php

// src/Form/LocationType.php
namespace App\Form;

use App\Entity\Location;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

class LocationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $countries = json_decode(file_get_contents("js/countries.json"), TRUE)['countries'];
        $countriesChoices = array();

        foreach($countries as $country) {
            $countriesChoices[$country['name']] = $country['id'];
        }

        $builder
            ->add('zip_code', TextType::class)
            ->add('street', TextType::class)
            ->add('country', ChoiceType::class, array(
                'choices'  => $countriesChoices
            ))
            ->add('area', ChoiceType::class, array(
                'choices'  => array()
            ))
            ->add('city', ChoiceType::class, array(
                'choices'  => array()
            ))
        ;

        $formModifierStates = function (FormInterface $form, $countryId = null) {
            $statesChoices = array();

            if (null !== $countryId) {
                $states = json_decode(file_get_contents("js/states.json"), TRUE)['states'];

                foreach($states as $state) {
                    if ($countryId == $state['country_id']) {
                        $statesChoices[$state['name']] = $state['id'];
                    }
                }
            }

            $form->add('area', ChoiceType::class, array(
                'choices'  => $statesChoices
            ));
        };

        $formModifierCities = function (FormInterface $form, $stateId = null) {
            $citiesChoices = array();

            if (null !== $stateId) {
                $cities = json_decode(file_get_contents("js/cities.json"), TRUE)['cities'];

                foreach($cities as $city) {
                    if ($stateId == $city['state_id']) {
                        $citiesChoices[$city['name']] = $city['id'];
                    }
                }
            }

            $form->add('city', ChoiceType::class, array(
                'choices'  => $citiesChoices
            ));
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifierStates) {
                $data = $event->getData(); // country id

                $formModifierStates($event->getForm(), $data);
            }
        );

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifierCities) {
                $data = $event->getData(); // state id

                $formModifierCities($event->getForm(), $data);
            }
        );

        $builder->get('country')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formModifierStates) {
                // It's important here to fetch $event->getForm()->getData(), as
                // $event->getData() will get you the client data (that is, the ID)
                $country = $event->getForm()->getData();

                // since we've added the listener to the child, we'll have to pass on
                // the parent to the callback functions!
                $formModifierStates($event->getForm()->getParent(), $country);
            }
        );

        $builder->get('area')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formModifierCities) {
                // It's important here to fetch $event->getForm()->getData(), as
                // $event->getData() will get you the client data (that is, the ID)
                $state = $event->getForm()->getData();

                // since we've added the listener to the child, we'll have to pass on
                // the parent to the callback functions!
                $formModifierCities($event->getForm()->getParent(), $state);
            }
        );
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Location::class,
        ));
    }
}

After filling in the form, I've got the message : "The choice "..." does not exist or is not unique". The choice is refering to the id (value of option) of the city selected in the select.

The problem is that I can't figure out how to populate the city ChoiceType?

1 Answer 1

1

You'd need to load them dynamically based on the choice made for the previous select. Approach is a lot similar to your with JS.

You can see an example in the official documentation: Dynamic Generation for Submitted Forms.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks dlondero I will look this.
Did you add the AJAX calls? Check the very last code part from the link above. There's some JS you need to add binding the onchange event in order to fetch data and populate subsequent selects.
Yes I added the Ajax Calls, and it populates the city select when a state is chosen but I got the error mentioned due to, I think, the buildForm of LocationType.
Dependent fields should be added only within the event listener and not in the builder initially.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.