I have three entities: Country, State and City with the following relationships:
When creating a City, I want two selectors, one for the Country and one for the State where the city belongs. These two selectors need to be chained so changing the Country will "filter" the States shown in the other selector.
I found a tutorial showing how to do this using Form Events but their example it's not quite my case. My entity City it's not directly related to the Country entity (they are indirectly related through State) so, when setting the country field in the City form (inside the class CityType), I'm forced to declare that field as 'property_path'=>false
as you can see in the code below:
class CityType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('country', 'entity', array(
'class'=>'TestBundle:Country',
'property'=>'name',
'property_path'=>false //Country is not directly related to City
));
$builder->add('name');
$factory = $builder->getFormFactory();
$refreshStates = function ($form, $country) use ($factory)
{
$form->add($factory->createNamed('entity', 'state', null, array(
'class' => 'Test\TestBundle\Entity\State',
'property' => 'name',
'query_builder' => function (EntityRepository $repository) use ($country)
{
$qb = $repository->createQueryBuilder('state')
->innerJoin('state.country', 'country');
if($country instanceof Country) {
$qb->where('state.country = :country')
->setParameter('country', $country);
} elseif(is_numeric($country)) {
$qb->where('country.id = :country')
->setParameter('country', $country);
} else {
$qb->where('country.name = :country')
->setParameter('country', "Venezuela");;
}
return $qb;
}
)));
};
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (DataEvent $event) use ($refreshStates)
{
$form = $event->getForm();
$data = $event->getData();
if($data == null)
return;
if($data instanceof City){
if($data->getId()) { //An existing City
$refreshStates($form, $data->getState()->getCountry());
}else{ //A new City
$refreshStates($form, null);
}
}
});
$builder->addEventListener(FormEvents::PRE_BIND, function (DataEvent $event) use ($refreshStates)
{
$form = $event->getForm();
$data = $event->getData();
if(array_key_exists('country', $data)) {
$refreshStates($form, $data['country']);
}
});
}
public function getName()
{
return 'city';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Test\TestBundle\Entity\City');
}
}
The problem is that when I try to edit an existing City, the related Country is not selected by default in the form. If I remove the line 'property_path'=>false
I get (not surprisingly) the error message:
Neither property "country" nor method "getCountry()" nor method "isCountry()" exists in class "Test\TestBundle\Entity\City"
Any ideas?
OK, I finally figured out how to do it properly:
namespace Test\TestBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\Event\DataEvent; use Test\TestBundle\Entity\Country; use Test\TestBundle\Entity\State; use Test\TestBundle\Entity\City; class CityType extends AbstractType { public function buildForm(FormBuilder $builder, array $options) { $builder->add('name'); $factory = $builder->getFormFactory(); $refreshStates = function ($form, $country) use ($factory) { $form->add($factory->createNamed('entity','state', null, array( 'class' => 'Test\TestBundle\Entity\State', 'property' => 'name', 'empty_value' => '-- Select a state --', 'query_builder' => function (EntityRepository $repository) use ($country) { $qb = $repository->createQueryBuilder('state') ->innerJoin('state.country', 'country'); if ($country instanceof Country) { $qb->where('state.country = :country') ->setParameter('country', $country); } elseif (is_numeric($country)) { $qb->where('country.id = :country') ->setParameter('country', $country); } else { $qb->where('country.name = :country') ->setParameter('country', null); } return $qb; }) )); }; $setCountry = function ($form, $country) use ($factory) { $form->add($factory->createNamed('entity', 'country', null, array( 'class' => 'TestBundle:Country', 'property' => 'name', 'property_path' => false, 'empty_value' => '-- Select a country --', 'data' => $country, ))); }; $builder->addEventListener(FormEvents::PRE_SET_DATA, function (DataEvent $event) use ($refreshStates, $setCountry) { $form = $event->getForm(); $data = $event->getData(); if ($data == null) { return; } if ($data instanceof City) { $country = ($data->getId()) ? $data->getState()->getCountry() : null ; $refreshStates($form, $country); $setCountry($form, $country); } }); $builder->addEventListener(FormEvents::PRE_BIND, function (DataEvent $event) use ($refreshStates) { $form = $event->getForm(); $data = $event->getData(); if(array_key_exists('country', $data)) { $refreshStates($form, $data['country']); } }); } public function getName() { return 'city'; } public function getDefaultOptions(array $options) { return array('data_class' => 'Test\TestBundle\Entity\City'); } }
The jQuery AJAX selector
$(document).ready(function () { $('#city_country').change(function(){ $('#city_state option:gt(0)').remove(); if($(this).val()){ $.ajax({ type: "GET", data: "country_id=" + $(this).val(), url: Routing.generate('state_list'), success: function(data){ $('#city_state').append(data); } }); } }); });
I hope this will be helpful to somebody else facing the same situation.
Since your link to this approach is down i decided to complement your excelent answer so anyone can use it:
In order to execute the following javascript command:
url: Routing.generate('state_list'),
You need to install FOSJsRoutingBundle that can be found in here.
ATENTION: in the read me section of the bundle there are instalation instructions but there is something missing. If you use the deps with this:
[FOSJsRoutingBundle] git=git://github.com/FriendsOfSymfony/FOSJsRoutingBundle.git target=/bundles/FOS/JsRoutingBundle
You must run the php bin/vendors update
before the next steps.
I'm still trying to find out what route is needed in the routing.yml for this solution to work. As soon as i discover i will edit this answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With