From 2533f294209c7aa476f68c03216783a9e58e6390 Mon Sep 17 00:00:00 2001 From: Philipp Rieber Date: Fri, 3 Jan 2014 12:21:50 +0100 Subject: [PATCH 1/4] [Cookbook][Dynamic Form Modification] Add AJAX sample --- cookbook/form/dynamic_form_modification.rst | 151 ++++++++++++++++++-- 1 file changed, 136 insertions(+), 15 deletions(-) diff --git a/cookbook/form/dynamic_form_modification.rst b/cookbook/form/dynamic_form_modification.rst index 14ad9f02756..38004fb7145 100644 --- a/cookbook/form/dynamic_form_modification.rst +++ b/cookbook/form/dynamic_form_modification.rst @@ -486,7 +486,10 @@ sport like this:: public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('sport', 'entity', array(...)) + ->add('sport', 'entity', array( + 'class' => 'AcmeDemoBundle:Sport', + 'empty_value' => '', + )); ; $builder->addEventListener( @@ -497,12 +500,18 @@ sport like this:: // this would be your entity, i.e. SportMeetup $data = $event->getData(); - $positions = $data->getSport()->getAvailablePositions(); + $sport = $data->getSport(); + $positions = (null === $sport) ? array() : $sport->getAvailablePositions(); - $form->add('position', 'entity', array('choices' => $positions)); + $form->add('position', 'entity', array( + 'class' => 'AcmeDemoBundle:Position', + 'empty_value' => '', + 'choices' => $positions, + )); } ); } + // ... } When you're building this form to display to the user for the first time, @@ -547,13 +556,20 @@ The type would now look like:: public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('sport', 'entity', array(...)) + ->add('sport', 'entity', array( + 'class' => 'AcmeDemoBundle:Sport', + 'empty_value' => '', + )); ; - $formModifier = function(FormInterface $form, Sport $sport) { - $positions = $sport->getAvailablePositions(); + $formModifier = function(FormInterface $form, Sport $sport = null) { + $positions = (null === $sport) ? array() : $sport->getAvailablePositions(); - $form->add('position', 'entity', array('choices' => $positions)); + $form->add('position', 'entity', array( + 'class' => 'AcmeDemoBundle:Position', + 'empty_value' => '', + 'choices' => $positions, + )); }; $builder->addEventListener( @@ -579,17 +595,119 @@ The type would now look like:: } ); } + // ... } -You can see that you need to listen on these two events and have different callbacks -only because in two different scenarios, the data that you can use is available in different events. -Other than that, the listeners always perform exactly the same things on a given form. +You can see that you need to listen on these two events and have different +callbacks only because in two different scenarios, the data that you can use is +available in different events. Other than that, the listeners always perform +exactly the same things on a given form. + +One piece that is still missing is the client-side updating of your form after +the sport is selected. This should be handled by making an AJAX call back to +your application. Assume that you have a sport meetup creation controller:: + + // src/Acme/DemoBundle/Controller/MeetupController.php + // ... + + /** + * @Route("/meetup") + */ + class MeetupController extends Controller + { + /** + * @Route("/create", name="meetup_create") + * @Template + */ + public function createAction(Request $request) + + { + $meetup = new SportMeetup(); + $form = $this->createForm(new SportMeetupType(), $meetup); + $form->handleRequest($request); + if ($form->isValid()) { + // ... save the meetup, redirect etc. + } + + return array('form' => $form->createView()); + } + // ... + } -One piece that may still be missing is the client-side updating of your form -after the sport is selected. This should be handled by making an AJAX call -back to your application. In that controller, you can submit your form, but -instead of processing it, simply use the submitted form to render the updated -fields. The response from the AJAX call can then be used to update the view. +The associated template uses some JavaScript to update the ``position`` form +field according to the current selection in the ``sport`` field. To ease things +it makes use of `jQuery`_ library and the `FOSJsRoutingBundle`_: + +.. code-block:: html+jinja + + {# src/Acme/DemoBundle/Resources/views/Meetup/create.html.twig #} + {{ form_start(form) }} + {{ form_row(form.sport) }} {# - $(function(){ - // When sport gets selected ... - $('#meetup_sport').change(function(){ - var $position = $('#meetup_position'); - // Remove current position options except first "empty_value" option - $position.find('option:not(:first)').remove(); - var sportId = $(this).val(); - if (sportId) { - // Issue AJAX call fetching positions for selected sport as JSON - $.getJSON( - // FOSJsRoutingBundle generates route including selected sport ID - Routing.generate('meetup_positions_by_sport', {id: sportId}), - function(positions) { - // Append fetched positions associated with selected sport - $.each(positions, function(key, position){ - $position.append(new Option(position[1], position[0])); - }); - } - ); - } +.. configuration-block:: + + .. code-block:: html+jinja + + {# src/Acme/DemoBundle/Resources/views/Meetup/create.html.twig #} + + {{ form_start(form) }} + {{ form_row(form.sport) }} {# - start($form) ?> row($form['sport']) ?> row($form['position']) ?> @@ -733,7 +728,6 @@ of the :doc:`@ParamConverter Date: Thu, 23 Jan 2014 06:43:05 +0100 Subject: [PATCH 4/4] Shorten AJAX example --- cookbook/form/dynamic_form_modification.rst | 115 ++---------------- .../dynamic_form_modification_ajax_js.rst.inc | 25 ++++ 2 files changed, 35 insertions(+), 105 deletions(-) create mode 100644 cookbook/form/dynamic_form_modification_ajax_js.rst.inc diff --git a/cookbook/form/dynamic_form_modification.rst b/cookbook/form/dynamic_form_modification.rst index 8e1fc9cf865..b963bab1e5d 100644 --- a/cookbook/form/dynamic_form_modification.rst +++ b/cookbook/form/dynamic_form_modification.rst @@ -614,22 +614,13 @@ your application. Assume that you have a sport meetup creation controller:: namespace Acme\DemoBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Symfony\Component\HttpFoundation\Request; use Acme\DemoBundle\Entity\SportMeetup; use Acme\DemoBundle\Form\Type\SportMeetupType; // ... - /** - * @Route("/meetup") - */ class MeetupController extends Controller { - /** - * @Route("/create", name="meetup_create") - * @Template - */ public function createAction(Request $request) { $meetup = new SportMeetup(); @@ -639,15 +630,17 @@ your application. Assume that you have a sport meetup creation controller:: // ... save the meetup, redirect etc. } - return array('form' => $form->createView()); + return $this->render( + 'AcmeDemoBundle:Meetup:create.html.twig', + array('form' => $form->createView()) + ); } // ... } The associated template uses some JavaScript to update the ``position`` form -field according to the current selection in the ``sport`` field. To ease things -it makes use of `jQuery`_ library and the `FOSJsRoutingBundle`_: +field according to the current selection in the ``sport`` field: .. configuration-block:: @@ -660,31 +653,7 @@ it makes use of `jQuery`_ library and the `FOSJsRoutingBundle`_: {# ... #} {{ form_end(form) }} - {# ... Include jQuery and scripts from FOSJsRoutingBundle ... #} - + .. include:: /cookbook/form/dynamic_form_modification_ajax_js.rst.inc .. code-block:: html+php @@ -695,72 +664,11 @@ it makes use of `jQuery`_ library and the `FOSJsRoutingBundle`_: end($form) ?> - - - -The last piece is implementing a controller for the -``meetup_positions_by_sport`` route returning the positions as JSON according -to the currently selected sport. To ease things again the controller makes use -of the :doc:`@ParamConverter ` -listener to convert the submitted sport ID into a ``Sport`` object:: + .. include:: /cookbook/form/dynamic_form_modification_ajax_js.rst.inc - // src/Acme/DemoBundle/Controller/MeetupController.php - namespace Acme\DemoBundle\Controller; - - // ... - use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; - use Symfony\Component\HttpFoundation\JsonResponse; - use Acme\DemoBundle\Entity\Sport; - - /** - * @Route("/meetup") - */ - class MeetupController extends Controller - { - // ... - - /** - * @Route("/{id}/positions.json", name="meetup_positions_by_sport", options={"expose"=true}) - */ - public function positionsBySportAction(Sport $sport) - { - $result = array(); - foreach ($sport->getAvailablePositions() as $position) { - $result[] = array($position->getId(), $position->getName()); - } - - return new JsonResponse($result); - } - } - -.. note:: - - The returned JSON should not be created from an associative array - (``$result[$position->getId()] = $position->getName())``) as the iterating - order in JavaScript is undefined and may vary in different browsers. +The major benefit of submitting the whole form to just extract the updated +``position`` field is that no additional server-side code is needed; all the +code from above to generate the submitted form can be reused. .. _cookbook-dynamic-form-modification-suppressing-form-validation: @@ -793,6 +701,3 @@ all of this, use a listener:: By doing this, you may accidentally disable something more than just form validation, since the ``POST_SUBMIT`` event may have other listeners. - -.. _`jQuery`: http://jquery.com -.. _`FOSJsRoutingBundle`: https://github.com/FriendsOfSymfony/FOSJsRoutingBundle diff --git a/cookbook/form/dynamic_form_modification_ajax_js.rst.inc b/cookbook/form/dynamic_form_modification_ajax_js.rst.inc new file mode 100644 index 00000000000..09e255808db --- /dev/null +++ b/cookbook/form/dynamic_form_modification_ajax_js.rst.inc @@ -0,0 +1,25 @@ + \ No newline at end of file