Hello I am using Symfony 4.
I have managed to link up to two select box with form events, but I need to have three dynamic select box.
This is the relation between my entities:
Country -> Province -> City.
These are linked to a Person entity like this
When I add a new person I should be able to select a Country and have the Province dropdown updated in accordance to Country selection; same thing for the City dropdown after I have selected a Province. I have made things working for Country and Province following the official Symfony guide here https://symfony.com/doc/current/form/dynamic_form_modification.html#dynamic-generation-for-submitted-forms How should I manage adding the third dropdown?
This is my Country entity:
<?php
namespace App\Entity\Geo;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\Geo\CountryRepository")
*/
class Country
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank()
*/
private $name;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Geo\Province", mappedBy="country")
* @ORM\JoinColumn(nullable=false)
*/
private $provinces;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Geo\City", mappedBy="country")
* @ORM\JoinColumn(nullable=false)
*/
private $cities;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Geo\Person", mappedBy="country")
* @ORM\JoinColumn(nullable=false)
*/
private $persons;
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @return mixed
*/
public function getName()
{
return $this->name;
}
/**
* @param mixed $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* @return mixed
*/
public function getProvinces()
{
return $this->provinces;
}
/**
* @param mixed $provinces
*/
public function setProvinces($provinces): void
{
$this->provinces = $provinces;
}
/**
* @return mixed
*/
public function getCities()
{
return $this->cities;
}
/**
* @param mixed $cities
*/
public function setCities($cities): void
{
$this->cities = $cities;
}
/**
* @return mixed
*/
public function getPersons()
{
return $this->persons;
}
/**
* @param mixed $persons
*/
public function setPersons($persons): void
{
$this->persons = $persons;
}
}
This is my Province entity:
<?php
namespace App\Entity\Geo;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\Geo\ProvinceRepository")
*/
class Province
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank()
*/
private $name;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Geo\Country", inversedBy="provinces")
* @ORM\JoinColumn(nullable=false)
*/
private $country;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Geo\City", mappedBy="province")
* @ORM\JoinColumn(nullable=false)
*/
private $cities;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Geo\Person", mappedBy="province")
* @ORM\JoinColumn(nullable=false)
*/
private $persons;
public function __toString() {
return $this->name;
}
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @return mixed
*/
public function getName()
{
return $this->name;
}
/**
* @param mixed $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* @return mixed
*/
public function getCountry()
{
return $this->country;
}
/**
* @param mixed $country
*/
public function setCountry($country): void
{
$this->country = $country;
}
/**
* @return mixed
*/
public function getCities()
{
return $this->cities;
}
/**
* @param mixed $cities
*/
public function setCities($cities): void
{
$this->cities = $cities;
}
/**
* @return mixed
*/
public function getPersons()
{
return $this->persons;
}
/**
* @param mixed $persons
*/
public function setPersons($persons): void
{
$this->persons = $persons;
}
}
This is my City entity:
<?php
namespace App\Entity\Geo;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\Geo\CityRepository")
*/
class City
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank()
*/
private $name;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Geo\Province", inversedBy="cities")
* @ORM\JoinColumn(nullable=false)
*/
private $province;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Geo\Country", inversedBy="cities")
* @ORM\JoinColumn(nullable=false)
*/
private $country;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Geo\Person", mappedBy="city")
* @ORM\JoinColumn(nullable=false)
*/
private $persons;
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @return mixed
*/
public function getName()
{
return $this->name;
}
/**
* @param mixed $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* @return mixed
*/
public function getProvince()
{
return $this->province;
}
/**
* @param mixed $province
*/
public function setProvince($province): void
{
$this->province = $province;
}
/**
* @return mixed
*/
public function getCountry()
{
return $this->country;
}
/**
* @param mixed $country
*/
public function setCountry($country): void
{
$this->country = $country;
}
/**
* @return mixed
*/
public function getPersons()
{
return $this->persons;
}
/**
* @param mixed $persons
*/
public function setPersons($persons): void
{
$this->persons = $persons;
}
}
This is my form to add a Person (PersonType.php)
<?php
namespace App\Form\Geo;
use App\Entity\Geo\Person;
use App\Entity\Geo\Country;
use App\Entity\Geo\Province;
use App\Entity\Geo\City;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, ['label' => "Name"])
->add('country', EntityType::class, [
'class' => Country::class,
'choice_label' => function(Country $country) {
return $country->getName();
},
'placeholder' => 'Choose a Country'
])
;
$formModifier = function (FormInterface $form, Country $country = null) {
$provinces = null === $country ? [] : $country->getProvinces();
$form->add('province', EntityType::class, [
'class' => Province::class,
'placeholder' => 'Choose a Province',
'choices' => $provinces,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getCountry());
}
);
$builder->get('country')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$country = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $country);
}
);
$builder->add( 'save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' =>Person::class
]);
}
}
This is my twig template (person-add.html.twig)
{% extends 'base.html.twig' %}
{% block title %}Add Person{% endblock %}
{% block body %}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.country) }}
{{ form_row(form.province) }}
{{ form_end(form) }}
<script>
$(document).ready(function() {
var $country = $('#person_country');
// When sport gets selected ...
$country.change(function () {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected sport value.
var data = {};
data[$country.attr('name')] = $country.val();
// Submit data via AJAX to the form's action path.
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: data,
success: function (html) {
// Replace current position field ...
$('#person_province').replaceWith(
// ... with the returned one from the AJAX response.
$(html).find('#person_province')
);
// Position field now displays the appropriate positions.
}
});
})
});
</script>
{% endblock %}
Thanks to this post I have managed to change my PersonType.php form file like this:
<?php
namespace App\Form\Geo;
use App\Entity\Geo\Person;
use App\Entity\Geo\Country;
use App\Entity\Geo\Province;
use App\Entity\Geo\City;
use App\Repository\Geo\CityRepository;
use App\Repository\Geo\ProvinceRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
//
->add('country', EntityType::class, [
'class' => Country::class,
'label' => 'Country',
'required' => true,
'choice_label' => function(Country $country) {
return $country->getName();
},
'invalid_message' => 'You must select a Country',
'placeholder' => 'Select Country',
]);
//**************** Start Province Form
$addProvinceForm = function (FormInterface $form, $country_id) {
// it would be easier to use a Park entity here,
// but it's not trivial to get it in the PRE_SUBMIT events
$form->add('province', EntityType::class, [
'class' => Province::class,
'label' => 'Province',
'required' => true,
'invalid_message' => 'Choose a Province',
'placeholder' => null === $country_id ? 'Choose a Country first' : 'Select Province',
'query_builder' => function (ProvinceRepository $repository) use ($country_id) {
return $repository->createQueryBuilder('p')
->innerJoin('p.country', 'c')
->where('c.id = :country')
->setParameter('country', $country_id)
;
}
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($addProvinceForm) {
$country = $event->getData()->getCountry();
$country_id = $country ? $country->getId() : null;
$addProvinceForm($event->getForm(), $country_id);
}
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($addProvinceForm) {
$data = $event->getData();
$country_id = array_key_exists('country', $data) ? $data['country'] : null;
$addProvinceForm($event->getForm(), $country_id);
}
);
//**************** End Province Form
//**************** Start City Form
$addCityForm = function (FormInterface $form, $province_id) {
$form->add('city', EntityType::class, [
'class' => City::class,
'label' => 'City',
'required' => true,
'invalid_message' => 'You must choose a City',
'placeholder' => null === $province_id ? 'Choose a Province first' : 'Choose a City',
'query_builder' => function (CityRepository $repository) use ($province_id) {
return $repository->createQueryBuilder('ci')
->innerJoin('ci.province', 'pr')
->where('pr.id = :province')
->setParameter('province', $province_id)
;
}
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($addCityForm) {
$province = $event->getData()->getProvince();
$province_id = $province ? $province->getId() : null;
$addCityForm($event->getForm(), $province_id);
}
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($addCityForm) {
$data = $event->getData();
$province_id = array_key_exists('province', $data) ? $data['province'] : null;
$addCityForm($event->getForm(), $province_id);
}
);
//**************** End City Form
$builder->add( 'save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' =>Person::class
]);
}
}
The Province dropdown works as expected when you first select a Country. The problem is the City dropdown: nothing changes after you select a Province. If everything is ok with the query executed inside the PersonType.php file, I think I am doing something wrong with the javascript. Here's my code:
<script>
$(document).ready(function() {
var $country = $('#person_country');
var $province = $('#person_province');
// When country gets selected ...
$country.change(function () {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected country value.
var data = {};
data[$country.attr('name')] = $country.val();
// Submit data via AJAX to the form's action path.
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: data,
success: function (html) {
// Replace current province field ...
$('#person_province').replaceWith(
// ... with the returned one from the AJAX response.
$(html).find('#person_province')
);
}
});
});
// When province gets selected ...
$province.change( function () {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected province value.
var data = {};
data[$province.attr('name')] = $province.val();
// Submit data via AJAX to the form's action path.
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: data,
success: function (html) {
// Replace current city field ...
$('#person_city').replaceWith(
// ... with the returned one from the AJAX response.
$(html).find('#person_city')
);
}
});
});
});
</script>