Skip to content

Commit 2533f29

Browse files
committed
[Cookbook][Dynamic Form Modification] Add AJAX sample
1 parent 5d4a3a4 commit 2533f29

File tree

1 file changed

+136
-15
lines changed

1 file changed

+136
-15
lines changed

cookbook/form/dynamic_form_modification.rst

+136-15
Original file line numberDiff line numberDiff line change
@@ -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

508517
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::
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

0 commit comments

Comments
 (0)