@@ -486,7 +486,10 @@ sport like this::
486486 public function buildForm(FormBuilderInterface $builder, array $options)
487487 {
488488 $builder
489- ->add('sport', 'entity', array(...))
489+ ->add('sport', 'entity', array(
490+ 'class' => 'AcmeDemoBundle:Sport',
491+ 'empty_value' => '',
492+ ));
490493 ;
491494
492495 $builder->addEventListener(
@@ -497,12 +500,18 @@ sport like this::
497500 // this would be your entity, i.e. SportMeetup
498501 $data = $event->getData();
499502
500- $positions = $data->getSport()->getAvailablePositions();
503+ $sport = $data->getSport();
504+ $positions = (null === $sport) ? array() : $sport->getAvailablePositions();
501505
502- $form->add('position', 'entity', array('choices' => $positions));
506+ $form->add('position', 'entity', array(
507+ 'class' => 'AcmeDemoBundle:Position',
508+ 'empty_value' => '',
509+ 'choices' => $positions,
510+ ));
503511 }
504512 );
505513 }
514+ // ...
506515 }
507516
508517When you're building this form to display to the user for the first time,
@@ -547,13 +556,20 @@ The type would now look like::
547556 public function buildForm(FormBuilderInterface $builder, array $options)
548557 {
549558 $builder
550- ->add('sport', 'entity', array(...))
559+ ->add('sport', 'entity', array(
560+ 'class' => 'AcmeDemoBundle:Sport',
561+ 'empty_value' => '',
562+ ));
551563 ;
552564
553- $formModifier = function(FormInterface $form, Sport $sport) {
554- $positions = $sport->getAvailablePositions();
565+ $formModifier = function(FormInterface $form, Sport $sport = null ) {
566+ $positions =(null === $sport) ? array() : $sport->getAvailablePositions();
555567
556- $form->add('position', 'entity', array('choices' => $positions));
568+ $form->add('position', 'entity', array(
569+ 'class' => 'AcmeDemoBundle:Position',
570+ 'empty_value' => '',
571+ 'choices' => $positions,
572+ ));
557573 };
558574
559575 $builder->addEventListener(
@@ -579,17 +595,119 @@ The type would now look like::
579595 }
580596 );
581597 }
598+ // ...
582599 }
583600
584- You can see that you need to listen on these two events and have different callbacks
585- only because in two different scenarios, the data that you can use is available in different events.
586- Other than that, the listeners always perform exactly the same things on a given form.
601+ You can see that you need to listen on these two events and have different
602+ callbacks only because in two different scenarios, the data that you can use is
603+ available in different events. Other than that, the listeners always perform
604+ exactly the same things on a given form.
605+
606+ One piece that is still missing is the client-side updating of your form after
607+ the sport is selected. This should be handled by making an AJAX call back to
608+ your application. Assume that you have a sport meetup creation controller::
609+
610+ // src/Acme/DemoBundle/Controller/MeetupController.php
611+ // ...
612+
613+ /**
614+ * @Route("/meetup")
615+ */
616+ class MeetupController extends Controller
617+ {
618+ /**
619+ * @Route("/create", name="meetup_create")
620+ * @Template
621+ */
622+ public function createAction(Request $request)
623+
624+ {
625+ $meetup = new SportMeetup();
626+ $form = $this->createForm(new SportMeetupType(), $meetup);
627+ $form->handleRequest($request);
628+ if ($form->isValid()) {
629+ // ... save the meetup, redirect etc.
630+ }
631+
632+ return array('form' => $form->createView());
633+ }
634+ // ...
635+ }
587636
588- One piece that may still be missing is the client-side updating of your form
589- after the sport is selected. This should be handled by making an AJAX call
590- back to your application. In that controller, you can submit your form, but
591- instead of processing it, simply use the submitted form to render the updated
592- fields. The response from the AJAX call can then be used to update the view.
637+ The associated template uses some JavaScript to update the ``position `` form
638+ field according to the current selection in the ``sport `` field. To ease things
639+ it makes use of `jQuery `_ library and the `FOSJsRoutingBundle `_:
640+
641+ ..code-block ::html+jinja
642+
643+ {# src/Acme/DemoBundle/Resources/views/Meetup/create.html.twig #}
644+ {{ form_start(form) }}
645+ {{ form_row(form.sport) }} {# <select id="meetup_sport" ... #}
646+ {{ form_row(form.position) }} {# <select id="meetup_position" ... #}
647+ {# ... #}
648+ {{ form_end(form) }}
649+
650+ {# ... Include jQuery and scripts from FOSJsRoutingBundle ... #}
651+ <script>
652+ $(function(){
653+ // When sport gets selected ...
654+ $('#meetup_sport').change(function(){
655+ var $position = $('#meetup_position');
656+ // Remove current position options except first "empty_value" option
657+ $position.find('option:not(:first)').remove();
658+ var sportId = $(this).val();
659+ if (sportId) {
660+ // Issue AJAX call fetching positions for selected sport as JSON
661+ $.getJSON(
662+ // FOSJsRoutingBundle generates route including selected sport ID
663+ Routing.generate('meetup_positions_by_sport', {id: sportId}),
664+ function(positions) {
665+ // Append fetched positions associated with selected sport
666+ $.each(positions, function(key, position){
667+ $position.append(new Option(position[1], position[0]));
668+ });
669+ }
670+ );
671+ }
672+ });
673+ });
674+ </script>
675+
676+ The last piece is implementing a controller for the
677+ ``meetup_positions_by_sport `` route returning the positions as JSON according
678+ to the currently selected sport. To ease things again the controller makes use
679+ of the:doc: `@ParamConverter </bundles/SensioFrameworkExtraBundle/annotations/converters >`
680+ listener to convert the submitted sport ID into a ``Sport `` object::
681+
682+ // src/Acme/DemoBundle/Controller/MeetupController.php
683+ // ...
684+ use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
685+
686+ /**
687+ * @Route("/meetup")
688+ */
689+ class MeetupController extends Controller
690+ {
691+ // ...
692+ /**
693+ * @Route("/{id}/positions.json", name="meetup_positions_by_sport", options={"expose"=true})
694+ */
695+ public function positionsBySportAction(Sport $sport)
696+ {
697+ $result = array();
698+ foreach ($sport->getAvailablePositions() as $position) {
699+ $result[] = array($position->getId(), $position->getName());
700+ }
701+
702+ return new JsonResponse($result);
703+ }
704+ }
705+
706+ ..note ::
707+
708+ The returned JSON should not be created from an associative array
709+ (``$result[$position->getId()] = $position->getName()) ``) as the iterating
710+ order in JavaScript is undefined and may vary in different browsers.
593711
594712.. _cookbook-dynamic-form-modification-suppressing-form-validation :
595713
@@ -622,3 +740,6 @@ all of this, use a listener::
622740
623741 By doing this, you may accidentally disable something more than just form
624742 validation, since the ``POST_SUBMIT `` event may have other listeners.
743+
744+ .. _`jQuery` :http://jquery.com
745+ .. _`FOSJsRoutingBundle` :https://github.com/FriendsOfSymfony/FOSJsRoutingBundle