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]));
+ });
+ }
+ );
+ }
+ });
+ });
+
+
+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::
+
+ // 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
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) }} {#
- $(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) }} {#
+ $(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]));
+ });
+ }
+ );
+ }
+ });
});
- });
-
+
+
+ .. code-block:: html+php
+
+
+
+ start($form) ?>
+ row($form['sport']) ?>
+ row($form['position']) ?>
+
+ end($form) ?>
+
+
+
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
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) }} {#
-
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