Reputation: 1555
I'm building a Symfony app and using form events with some jquery/ajax to do the whole "state/locality" thing. I have a slight issue though, I am using the format Province -> City -> Suburb. Now as far as I can tell my code is fine, but when the execution hits the section where I add a listener to the "City" select, it throws an error saying the following:
The child with the name "physicalCity" does not exist.
This obviously happens when I try and add an event listener to the newly created field, thus adding an event listener to an element created by an event listener?
A section of the code is below... What am I doing wrong? Any help would be very much appreciated!
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('schoolName')
->add('physicalProvince', 'entity', array(
'mapped' => false,
'class' => 'MY\MainBundle\Entity\Province',
'empty_value' => 'Select a province',
'attr' => array(
'class' => 'province',
'data-show' => 'physical-city',
)
));
/*
* For the physical cities
*/
$physicalCityModifier = function(FormInterface $form, Province $province = null) {
if (null !== $province)
$cities = $province->getCities();
else
$cities = array();
$form->add('physicalCity', 'entity', array(
'mapped' => false,
'class' => 'MY\MainBundle\Entity\City',
'empty_value' => 'Select a province first',
'choices' => $cities,
'attr' => array(
'class' => 'city physical-city',
'data-show' => 'physical-suburb'
)
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function(FormEvent $event) use ($physicalCityModifier) {
$data = $event->getData();
if (is_object($data->getPhysicalSuburb()))
$province = $data->getPhysicalSuburb()->getCity()->getProvince();
else
$province = null;
$physicalCityModifier($event->getForm(), $province);
}
);
$builder->get('physicalProvince')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($physicalCityModifier) {
$province = $event->getForm()->getData();
$physicalCityModifier($event->getForm()->getParent(), $province);
}
);
/*
* For the physical suburbs
*/
$physicalSuburbModifier = function(FormInterface $form, City $city = null) {
if (null !== $city)
$suburbs = $city->getSuburbs();
else
$suburbs = array();
$form->add('physicalSuburb', null, array(
'choices' => $suburbs,
'empty_value' => 'Select a city first',
'attr' => array(
'class' => 'physical-suburb'
),
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function(FormEvent $event) use ($physicalSuburbModifier) {
$data = $event->getData();
if (is_object($data->getCity()))
$city = $data->getCity();
else
$city = null;
$physicalSuburbModifier($event->getForm(), $city);
}
);
$builder->get('physicalCity')->addEventListener(
FormEvents::POST_SUBMIT,
function(FormEvent $event) use ($physicalSuburbModifier) {
$city = $event->getForm()->getData();
$physicalSuburbModifier($event->getForm()->getParent(), $city);
}
);
}
Upvotes: 11
Views: 9812
Reputation: 1
For those who prefer simplicity, you can relate whatever field you want to another by putting the logic into the controller.
So you can use ajax to check any change for a specific select and send the result to a method in your controller which will return the result fetched from your database.
Your dependent select box will then contains the specific options related to parent select.
Upvotes: 0
Reputation: 1555
If anybody else has a similar issue, I eventually got it right with event subscribers for each field, with help from this site (translate it for those non-spanish speaking folk among us).
Bascially, what I did was create a new Subscriber class for each field, including province, and then just created a query builder inside each of them to populate their values with those from the preceding fields. The code is shown below.
AddProvinceFieldSubscriber.php
class AddProvinceFieldSubscriber implements EventSubscriberInterface {
private $factory;
private $fieldName;
private $type;
public function __construct(FormFactoryInterface $factory, $fieldName) {
$this->factory = $factory;
$this->fieldName = $fieldName . 'Province';
$this->type = $fieldName;
}
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit',
);
}
private function addProvinceForm(FormInterface $form, $province) {
$form->add($this->factory->createNamed($this->fieldName, 'entity', $province, array(
'class' => 'MyThing\MainBundle\Entity\Province',
'mapped' => false,
'empty_value' => 'Select a province',
'query_builder' => function (EntityRepository $repository) {
$qb = $repository->createQueryBuilder('p');
return $qb;
},
'auto_initialize' => false,
'attr' => array(
'class' => 'province ' . $this->type .'-province',
'data-show' => $this->type . '-city',
)
)));
}
public function preSetData(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if (null === $data)
return;
$fieldName = 'get' . ucwords($this->type) . 'Suburb';
$province = ($data->$fieldName()) ? $data->$fieldName()->getCity()->getProvince() : null;
$this->addProvinceForm($form, $province);
}
public function preSubmit(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if (null === $data)
return;
$province = array_key_exists($this->fieldName, $data) ? $data[$this->fieldName] : null;
$this->addProvinceForm($form, $province);
}
}
AddCityFieldSubscriber.php
class AddCityFieldSubscriber implements EventSubscriberInterface {
private $factory;
private $fieldName;
private $provinceName;
private $suburbName;
private $type;
public function __construct(FormFactoryInterface $factory, $fieldName) {
$this->factory = $factory;
$this->fieldName = $fieldName . 'City';
$this->provinceName = $fieldName . 'Province';
$this->suburbName = $fieldName . 'Suburb';
$this->type = $fieldName;
}
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit',
);
}
private function addCityForm(FormInterface $form, $city, $province) {
$form->add($this->factory->createNamed($this->fieldName, 'entity', $city, array(
'class' => 'MyThing\MainBundle\Entity\City',
'empty_value' => 'Select a city',
'mapped' => false,
'query_builder' => function (EntityRepository $repository) use ($province) {
$qb = $repository->createQueryBuilder('c')
->innerJoin('c.province', 'province');
if ($province instanceof Province) {
$qb->where('c.province = :province')
->setParameter('province', $province);
} elseif (is_numeric($province)) {
$qb->where('province.id = :province')
->setParameter('province', $province);
} else {
$qb->where('province.provinceName = :province')
->setParameter('province', null);
}
return $qb;
},
'auto_initialize' => false,
'attr' => array(
'class' => 'city ' . $this->type . '-city',
'data-show' => $this->type . '-suburb',
)
)));
}
public function preSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$fieldName = 'get' . ucwords($this->suburbName);
$city = ($data->$fieldName()) ? $data->$fieldName()->getCity() : null;
$province = ($city) ? $city->getProvince() : null;
$this->addCityForm($form, $city, $province);
}
public function preSubmit(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data)
return;
$city = array_key_exists($this->fieldName, $data) ? $data[$this->fieldName] : null;
$province = array_key_exists($this->provinceName, $data) ? $data[$this->provinceName] : null;
$this->addCityForm($form, $city, $province);
}
}
And finally AddSuburbFieldSubscriber.php
class AddSuburbFieldSubscriber implements EventSubscriberInterface {
private $factory;
private $fieldName;
private $type;
public function __construct(FormFactoryInterface $factory, $fieldName) {
$this->factory = $factory;
$this->fieldName = $fieldName . 'Suburb';
$this->type = $fieldName;
}
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit',
);
}
private function addSuburbForm(FormInterface $form, $city) {
$form->add($this->factory->createNamed($this->fieldName, 'entity', null, array(
'class' => 'MyThing\MainBundle\Entity\Suburb',
'empty_value' => 'Select a suburb',
'query_builder' => function (EntityRepository $repository) use ($city) {
$qb = $repository->createQueryBuilder('s')
->innerJoin('s.city', 'city');
if ($city instanceof City) {
$qb->where('s.city = :city')
->setParameter('city', $city);
} elseif (is_numeric($city)) {
$qb->where('city.id = :city')
->setParameter('city', $city);
} else {
$qb->where('city.cityName = :city')
->setParameter('city', null);
}
$sql = $qb->getQuery()->getSQL();
return $qb;
},
'auto_initialize' => false,
'attr' => array(
'class' => 'suburb ' . $this->type . '-suburb',
),
)));
}
public function preSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data)
return;
$fieldName = 'get' . ucwords($this->fieldName);
$city = ($data->$fieldName()) ? $data->$fieldName()->getCity() : null;
$this->addSuburbForm($form, $city);
}
public function preSubmit(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data)
return;
$city = array_key_exists($this->type . 'City', $data) ? $data[$this->type . 'City'] : null;
$this->addSuburbForm($form, $city);
}
}
I had to add some extra stuff in there, but you get the gist of it.
In my form type I simply added the following:
$builder
->addEventSubscriber(new AddProvinceFieldSubscriber($factory, 'postal'))
->addEventSubscriber(new AddCityFieldSubscriber($factory, 'postal'))
->addEventSubscriber(new AddSuburbFieldSubscriber($factory, 'postal'))
//...
And happy days! Hope this helps somebody.
Also, I added the data-show
attributes to simplify my AJAX process, just in case somebody was wondering.
Upvotes: 18