Symfony2 AJAX Form: Republish

Update:

The original blog post’s blog is dead. I found a related article here.

This article was published here. I am reposting here because it is an interesting solution.

The concept is simple. We have two HTML SELECT fields, one for the city and one for the state (US). When the state is changed, we would like to populate the city select with cities from the selected state. We’ll use my ContactInfoType as an example. The form type would look like this:

public function buildForm(FormBuilder $builder, array $options) {
    $builder->add('city', 'entity', array('query_builder' => function (EntityRepository $er) { ... }));
    $builder->add('state', 'entity', array('query_builder' => function (EntityRepository $er) { ... }));
}

Here is the relevant snippet from the Controller:

$em = $this->get('doctrine.orm.entity_manager');
 
// In beta2 the Entity Manager is retrieved by $this->get('doctrine')->getEntityManager();
 
$contact = $em->getRepository('Entity:ContactInfo')
 ->find(1);   $form = $this->get('form.factory')
 ->create(new ContactInfoType(), $contact)
 ->getForm();
 
if($this->get('request')->getMethod() == 'POST') {
    $form->bindRequest($this->get('request');
 
    $em->persist($contact);
    $em->flush();
}

Everything looks straightforward enough, but the solution is flawed. Let’s say out $contact’s city is Columbia, MO. When we instantiate our form in the Controller, the “city” field is
populated with cities in Missouri. Now we want to set the city to Boulder, CO. We issue out our AJAX request and retrieve a list of cities in Colorado and populate the “city” SELECT accordingly, select Boulder and submit the form. Now, back in our Controller, we reload our $contact form the database and instantiate our form. Once again, our “city” field is populated with cities in Missouri. Since this is now a POST request, we want to bind the request to the form. When the Form Component tries to setCity, the city it is trying to set is not on the list, and that’s a problem. I posted my issue on the Symfony Users mailing list, and Berhard Schussek was kind enough to offer this solution. It’s not the shortest solution in the world, but it works. To work around this we need to use the Event System, utilizing the “preSetData” and “preBind” events, and create a Closure to use as a callback to create and populate the “city” field. Before the data is set, we populate our “city” field with cities in Missouri and then, before we bind data, we re-create our “city” field, populating it with cities from Colorado.

Here is our Closure:

$refreshCity = function ($form, $state) use ($factory) {
    $form->add($factory->createNamed('entity', 'city', null, array(
        'class'         => 'Entity:Cities',
        'property'      => 'city_name',
        'label'         => 'City',
        'query_builder' => function (EntityRepository $repository) use ($state) {
                               $qb = $repository->createQueryBuilder('cities')
                                                ->select(array('cities', 'zip_codes'))
                                                ->innerJoin('cities.states', 'states')
                                                ->innerJoin('cities.zip_codes', 'zip_codes');
 
                               if($state instanceof States) {
                                   $qb = $qb->where('cities.states = :state')
                                            ->setParameter('state', $state);
                               } elseif(is_numeric($state)) {
                                   $qb = $qb->where('states.state_id = :state_id')
                                            ->setParameter('state_id', $state);
                               } else {
                                   $qb = $qb->where('states.state_id = 1');
                               }
 
                               return $qb;
                           }
         )));
};

And now, let’s put $refreshCity to use:

$builder->addEventListener(Events::preSetData, function (DataEvent $event) use ($refreshCity) {
 $form = $event->getForm();
 $data = $event->getData();
 
 if($data == null)
    return;  //As of beta2, when a form is created setData(null) is called first
 
 if($data instanceof ContactInfo) {
 $refreshCity($form, $data->getCity()->getState());
 }
 });
 
$builder->addEventListener(Events::preBind, function (DataEvent $event) use ($refreshCity) {
 $form = $event->getForm();
 $data = $event->getData();
 
 if(array_key_exists('state', $data)) {
 $refreshCity($form, $data['state']);
 }
});

Now we can use the ContactInfoType in our Controller and we get the result we expect. This may be a somewhat lengthy solution, but Symfony provides lots of opportunities for code re-use. I use this form type in a *lot* of places and copy and paste elsewhere.

11 thoughts on “Symfony2 AJAX Form: Republish

    • i think the validation is implicit because if it is different option that persists then it would fire up an exception hmm of course different exeception.

      This is actually a very good question, have you yourself found a solution? please share it back url?

      • I think validation in terms of regular validation should work normal, however if you want to validate further like for sets of ids that means your code needs to include a validator in the form type i guess.

  1. A query. I followed suit, but I can’t handle the event changed the field ‘state’.
    Yes, once the object is to create the form, but it does once the form has already been created. I mean?
    That is, once the object already exists, and return to change the “state”, not refreshed the ‘cities’.
    I hope I was clear. Thank you very much for the reply.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>