From 2533f294209c7aa476f68c03216783a9e58e6390 Mon Sep 17 00:00:00 2001
From: Philipp Rieber <p.rieber@gmail.com>
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) }}    {# <select id="meetup_sport" ... #}
+        {{ form_row(form.position) }} {# <select id="meetup_position" ... #}
+        {# ... #}
+    {{ form_end(form) }}
+
+    {# ... Include jQuery and scripts from FOSJsRoutingBundle ... #}
+    <script>
+    $(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]));
+                        });
+                    }
+                );
+            }
+        });
+    });
+    </script>
+
+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 </bundles/SensioFrameworkExtraBundle/annotations/converters>`
+listener to convert the submitted sport ID into a ``Sport`` object::
+
+    // src/Acme/DemoBundle/Controller/MeetupController.php
+    // ...
+    use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
+
+    /**
+     * @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.
 
 .. _cookbook-dynamic-form-modification-suppressing-form-validation:
 
@@ -622,3 +740,6 @@ 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

From f47a7c3c126e3caf9df49b809c3915ee0f270c71 Mon Sep 17 00:00:00 2001
From: Philipp Rieber <p.rieber@gmail.com>
Date: Sat, 4 Jan 2014 05:54:06 +0100
Subject: [PATCH 2/4] Updates & Fixes after public review

---
 cookbook/form/dynamic_form_modification.rst | 147 ++++++++++++++------
 1 file changed, 103 insertions(+), 44 deletions(-)

diff --git a/cookbook/form/dynamic_form_modification.rst b/cookbook/form/dynamic_form_modification.rst
index 38004fb7145..01e31228000 100644
--- a/cookbook/form/dynamic_form_modification.rst
+++ b/cookbook/form/dynamic_form_modification.rst
@@ -474,8 +474,10 @@ The meetup is passed as an entity field to the form. So we can access each
 sport like this::
 
     // src/Acme/DemoBundle/Form/Type/SportMeetupType.php
+
     namespace Acme\DemoBundle\Form\Type;
 
+    use Symfony\Component\Form\AbstractType;
     use Symfony\Component\Form\FormBuilderInterface;
     use Symfony\Component\Form\FormEvent;
     use Symfony\Component\Form\FormEvents;
@@ -487,9 +489,9 @@ sport like this::
         {
             $builder
                 ->add('sport', 'entity', array(
-                    'class' => 'AcmeDemoBundle:Sport',
+                    'class'       => 'AcmeDemoBundle:Sport',
                     'empty_value' => '',
-                ));
+                ))
             ;
 
             $builder->addEventListener(
@@ -501,16 +503,17 @@ sport like this::
                     $data = $event->getData();
 
                     $sport = $data->getSport();
-                    $positions = (null === $sport) ? array() : $sport->getAvailablePositions();
+                    $positions = null === $sport ? array() : $sport->getAvailablePositions();
 
                     $form->add('position', 'entity', array(
-                        'class' => 'AcmeDemoBundle:Position',
+                        'class'       => 'AcmeDemoBundle:Position',
                         'empty_value' => '',
-                        'choices' => $positions,
+                        'choices'     => $positions,
                     ));
                 }
             );
         }
+
         // ...
     }
 
@@ -545,11 +548,12 @@ new field automatically and map it to the submitted client data.
 The type would now look like::
 
     // src/Acme/DemoBundle/Form/Type/SportMeetupType.php
+
     namespace Acme\DemoBundle\Form\Type;
 
     // ...
-    use Acme\DemoBundle\Entity\Sport;
     use Symfony\Component\Form\FormInterface;
+    use Acme\DemoBundle\Entity\Sport;
 
     class SportMeetupType extends AbstractType
     {
@@ -557,18 +561,18 @@ The type would now look like::
         {
             $builder
                 ->add('sport', 'entity', array(
-                    'class' => 'AcmeDemoBundle:Sport',
+                    'class'       => 'AcmeDemoBundle:Sport',
                     'empty_value' => '',
                 ));
             ;
 
             $formModifier = function(FormInterface $form, Sport $sport = null) {
-                $positions = (null === $sport) ? array() : $sport->getAvailablePositions();
+                $positions = null === $sport ? array() : $sport->getAvailablePositions();
 
                 $form->add('position', 'entity', array(
-                    'class' => 'AcmeDemoBundle:Position',
+                    'class'       => 'AcmeDemoBundle:Position',
                     'empty_value' => '',
-                    'choices' => $positions,
+                    'choices'     => $positions,
                 ));
             };
 
@@ -595,6 +599,7 @@ The type would now look like::
                 }
             );
         }
+
         // ...
     }
 
@@ -608,6 +613,15 @@ 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
+
+    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;
     // ...
 
     /**
@@ -620,7 +634,6 @@ your application. Assume that you have a sport meetup creation controller::
          * @Template
          */
         public function createAction(Request $request)
-        
         {
             $meetup = new SportMeetup();
             $form = $this->createForm(new SportMeetupType(), $meetup);
@@ -631,6 +644,7 @@ your application. Assume that you have a sport meetup creation controller::
 
             return array('form' => $form->createView());
         }
+
         // ...
     }
 
@@ -638,40 +652,79 @@ 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) }}    {# <select id="meetup_sport" ... #}
-        {{ form_row(form.position) }} {# <select id="meetup_position" ... #}
-        {# ... #}
-    {{ form_end(form) }}
-
-    {# ... Include jQuery and scripts from FOSJsRoutingBundle ... #}
-    <script>
-    $(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) }}    {# <select id="meetup_sport" ... #}
+            {{ form_row(form.position) }} {# <select id="meetup_position" ... #}
+            {# ... #}
+        {{ form_end(form) }}
+
+        {# ... Include jQuery and scripts from FOSJsRoutingBundle ... #}
+        <script>
+        $(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]));
+                            });
+                        }
+                    );
+                }
+            });
         });
-    });
-    </script>
+        </script>
+
+    .. code-block:: html+php
+
+        <!-- src/Acme/DemoBundle/Resources/views/Meetup/create.html.php -->
+
+        <?php echo $view['form']->start($form) ?>
+            <?php echo $view['form']->row($form['sport']) ?>    <!-- <select id="meetup_sport" ... -->
+            <?php echo $view['form']->row($form['position']) ?> <!-- <select id="meetup_position" ... -->
+            <!-- ... -->
+        <?php echo $view['form']->end($form) ?>
+
+        <!-- ... Include jQuery and scripts from FOSJsRoutingBundle ... -->
+        <script>
+        $(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]));
+                            });
+                        }
+                    );
+                }
+            });
+        });
+        </script>
 
 The last piece is implementing a controller for the
 ``meetup_positions_by_sport`` route returning the positions as JSON according
@@ -680,8 +733,13 @@ of the :doc:`@ParamConverter </bundles/SensioFrameworkExtraBundle/annotations/co
 listener to convert the submitted sport ID into a ``Sport`` object::
 
     // 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")
@@ -689,6 +747,7 @@ listener to convert the submitted sport ID into a ``Sport`` object::
     class MeetupController extends Controller
     {
         // ...
+
         /**
          * @Route("/{id}/positions.json", name="meetup_positions_by_sport", options={"expose"=true})
          */

From ee33dcdd07c7a9c3cac3bb889be725b818228e83 Mon Sep 17 00:00:00 2001
From: Philipp Rieber <p.rieber@gmail.com>
Date: Sat, 4 Jan 2014 06:00:58 +0100
Subject: [PATCH 3/4] Remove some empty lines from code samples

---
 cookbook/form/dynamic_form_modification.rst | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/cookbook/form/dynamic_form_modification.rst b/cookbook/form/dynamic_form_modification.rst
index 01e31228000..8e1fc9cf865 100644
--- a/cookbook/form/dynamic_form_modification.rst
+++ b/cookbook/form/dynamic_form_modification.rst
@@ -474,7 +474,6 @@ The meetup is passed as an entity field to the form. So we can access each
 sport like this::
 
     // src/Acme/DemoBundle/Form/Type/SportMeetupType.php
-
     namespace Acme\DemoBundle\Form\Type;
 
     use Symfony\Component\Form\AbstractType;
@@ -548,7 +547,6 @@ new field automatically and map it to the submitted client data.
 The type would now look like::
 
     // src/Acme/DemoBundle/Form/Type/SportMeetupType.php
-
     namespace Acme\DemoBundle\Form\Type;
 
     // ...
@@ -613,7 +611,6 @@ 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
-
     namespace Acme\DemoBundle\Controller;
 
     use Symfony\Bundle\FrameworkBundle\Controller\Controller;
@@ -657,7 +654,6 @@ 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) }}    {# <select id="meetup_sport" ... #}
             {{ form_row(form.position) }} {# <select id="meetup_position" ... #}
@@ -693,7 +689,6 @@ it makes use of `jQuery`_ library and the `FOSJsRoutingBundle`_:
     .. code-block:: html+php
 
         <!-- src/Acme/DemoBundle/Resources/views/Meetup/create.html.php -->
-
         <?php echo $view['form']->start($form) ?>
             <?php echo $view['form']->row($form['sport']) ?>    <!-- <select id="meetup_sport" ... -->
             <?php echo $view['form']->row($form['position']) ?> <!-- <select id="meetup_position" ... -->
@@ -733,7 +728,6 @@ of the :doc:`@ParamConverter </bundles/SensioFrameworkExtraBundle/annotations/co
 listener to convert the submitted sport ID into a ``Sport`` object::
 
     // src/Acme/DemoBundle/Controller/MeetupController.php
-
     namespace Acme\DemoBundle\Controller;
 
     // ...

From a75ad9c2aea6b0e5a106c843f9f81c4ef16cae07 Mon Sep 17 00:00:00 2001
From: Philipp Rieber <p.rieber@gmail.com>
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 ... #}
-        <script>
-        $(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]));
-                            });
-                        }
-                    );
-                }
-            });
-        });
-        </script>
+        .. 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`_:
             <!-- ... -->
         <?php echo $view['form']->end($form) ?>
 
-        <!-- ... Include jQuery and scripts from FOSJsRoutingBundle ... -->
-        <script>
-        $(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]));
-                            });
-                        }
-                    );
-                }
-            });
-        });
-        </script>
-
-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 </bundles/SensioFrameworkExtraBundle/annotations/converters>`
-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 @@
+<script>
+var $sport = $('#meetup_sport');
+// When sport gets selected ...
+$sport.change(function(){
+  // ... retrieve the corresponding form.
+  var $form = $(this).closest('form');
+  // Simulate form data, but only include the selected sport value.
+  var data = {};
+  data[$sport.attr('name')] = $sport.val();
+  // Submit data via AJAX to the form's action path.
+  $.ajax({
+    url : $form.attr('action'),
+    type: $form.attr('method'),
+    data : data,
+    success: function(html) {
+      // Replace current position field ...
+      $('#meetup_position').replaceWith(
+        // ... with the returned one from the AJAX response.
+        $(html).find('#meetup_position')
+      );
+      // Position field now displays the appropriate positions.
+    }
+  });
+});
+</script>
\ No newline at end of file