Skip to content

Commit a1050eb

Browse files
committed
feature symfony#3411 [Cookbook][Dynamic Form Modification] Add AJAX sample (bicpi)
This PR was merged into the 2.3 branch. Discussion ---------- [Cookbook][Dynamic Form Modification] Add AJAX sample | Q | A | ------------- | --- | Doc fix? | no | New docs? | yes | Applies to | 2.3+ | Fixed tickets | symfony#2464 ("AJAX experience") Do we need a PHP version of the `create.html.twig` which would mainly contain redundant JavaScript? Commits ------- a75ad9c Shorten AJAX example ee33dcd Remove some empty lines from code samples f47a7c3 Updates & Fixes after public review 2533f29 [Cookbook][Dynamic Form Modification] Add AJAX sample
2 parents 9dd8d96 + a75ad9c commit a1050eb

File tree

2 files changed

+120
-16
lines changed

2 files changed

+120
-16
lines changed

cookbook/form/dynamic_form_modification.rst

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ sport like this::
476476
// src/Acme/DemoBundle/Form/Type/SportMeetupType.php
477477
namespace Acme\DemoBundle\Form\Type;
478478

479+
use Symfony\Component\Form\AbstractType;
479480
use Symfony\Component\Form\FormBuilderInterface;
480481
use Symfony\Component\Form\FormEvent;
481482
use Symfony\Component\Form\FormEvents;
@@ -486,7 +487,10 @@ sport like this::
486487
public function buildForm(FormBuilderInterface $builder, array $options)
487488
{
488489
$builder
489-
->add('sport', 'entity', array(...))
490+
->add('sport', 'entity', array(
491+
'class' => 'AcmeDemoBundle:Sport',
492+
'empty_value' => '',
493+
))
490494
;
491495

492496
$builder->addEventListener(
@@ -497,12 +501,19 @@ sport like this::
497501
// this would be your entity, i.e. SportMeetup
498502
$data = $event->getData();
499503

500-
$positions = $data->getSport()->getAvailablePositions();
504+
$sport = $data->getSport();
505+
$positions = null === $sport ? array() : $sport->getAvailablePositions();
501506

502-
$form->add('position', 'entity', array('choices' => $positions));
507+
$form->add('position', 'entity', array(
508+
'class' => 'AcmeDemoBundle:Position',
509+
'empty_value' => '',
510+
'choices' => $positions,
511+
));
503512
}
504513
);
505514
}
515+
516+
// ...
506517
}
507518

508519
When you're building this form to display to the user for the first time,
@@ -539,21 +550,28 @@ The type would now look like::
539550
namespace Acme\DemoBundle\Form\Type;
540551

541552
// ...
542-
use Acme\DemoBundle\Entity\Sport;
543553
use Symfony\Component\Form\FormInterface;
554+
use Acme\DemoBundle\Entity\Sport;
544555

545556
class SportMeetupType extends AbstractType
546557
{
547558
public function buildForm(FormBuilderInterface $builder, array $options)
548559
{
549560
$builder
550-
->add('sport', 'entity', array(...))
561+
->add('sport', 'entity', array(
562+
'class' => 'AcmeDemoBundle:Sport',
563+
'empty_value' => '',
564+
));
551565
;
552566

553-
$formModifier = function(FormInterface $form, Sport $sport) {
554-
$positions = $sport->getAvailablePositions();
567+
$formModifier = function(FormInterface $form, Sport $sport = null) {
568+
$positions = null === $sport ? array() : $sport->getAvailablePositions();
555569

556-
$form->add('position', 'entity', array('choices' => $positions));
570+
$form->add('position', 'entity', array(
571+
'class' => 'AcmeDemoBundle:Position',
572+
'empty_value' => '',
573+
'choices' => $positions,
574+
));
557575
};
558576

559577
$builder->addEventListener(
@@ -579,17 +597,78 @@ The type would now look like::
579597
}
580598
);
581599
}
600+
601+
// ...
602+
}
603+
604+
You can see that you need to listen on these two events and have different
605+
callbacks only because in two different scenarios, the data that you can use is
606+
available in different events. Other than that, the listeners always perform
607+
exactly the same things on a given form.
608+
609+
One piece that is still missing is the client-side updating of your form after
610+
the sport is selected. This should be handled by making an AJAX call back to
611+
your application. Assume that you have a sport meetup creation controller::
612+
613+
// src/Acme/DemoBundle/Controller/MeetupController.php
614+
namespace Acme\DemoBundle\Controller;
615+
616+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
617+
use Symfony\Component\HttpFoundation\Request;
618+
use Acme\DemoBundle\Entity\SportMeetup;
619+
use Acme\DemoBundle\Form\Type\SportMeetupType;
620+
// ...
621+
622+
class MeetupController extends Controller
623+
{
624+
public function createAction(Request $request)
625+
{
626+
$meetup = new SportMeetup();
627+
$form = $this->createForm(new SportMeetupType(), $meetup);
628+
$form->handleRequest($request);
629+
if ($form->isValid()) {
630+
// ... save the meetup, redirect etc.
631+
}
632+
633+
return $this->render(
634+
'AcmeDemoBundle:Meetup:create.html.twig',
635+
array('form' => $form->createView())
636+
);
637+
}
638+
639+
// ...
582640
}
583641

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.
642+
The associated template uses some JavaScript to update the ``position`` form
643+
field according to the current selection in the ``sport`` field:
644+
645+
.. configuration-block::
646+
647+
.. code-block:: html+jinja
648+
649+
{# src/Acme/DemoBundle/Resources/views/Meetup/create.html.twig #}
650+
{{ form_start(form) }}
651+
{{ form_row(form.sport) }} {# <select id="meetup_sport" ... #}
652+
{{ form_row(form.position) }} {# <select id="meetup_position" ... #}
653+
{# ... #}
654+
{{ form_end(form) }}
655+
656+
.. include:: /cookbook/form/dynamic_form_modification_ajax_js.rst.inc
657+
658+
.. code-block:: html+php
659+
660+
<!-- src/Acme/DemoBundle/Resources/views/Meetup/create.html.php -->
661+
<?php echo $view['form']->start($form) ?>
662+
<?php echo $view['form']->row($form['sport']) ?> <!-- <select id="meetup_sport" ... -->
663+
<?php echo $view['form']->row($form['position']) ?> <!-- <select id="meetup_position" ... -->
664+
<!-- ... -->
665+
<?php echo $view['form']->end($form) ?>
666+
667+
.. include:: /cookbook/form/dynamic_form_modification_ajax_js.rst.inc
587668

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.
669+
The major benefit of submitting the whole form to just extract the updated
670+
``position`` field is that no additional server-side code is needed; all the
671+
code from above to generate the submitted form can be reused.
593672

594673
.. _cookbook-dynamic-form-modification-suppressing-form-validation:
595674

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script>
2+
var $sport = $('#meetup_sport');
3+
// When sport gets selected ...
4+
$sport.change(function(){
5+
// ... retrieve the corresponding form.
6+
var $form = $(this).closest('form');
7+
// Simulate form data, but only include the selected sport value.
8+
var data = {};
9+
data[$sport.attr('name')] = $sport.val();
10+
// Submit data via AJAX to the form's action path.
11+
$.ajax({
12+
url : $form.attr('action'),
13+
type: $form.attr('method'),
14+
data : data,
15+
success: function(html) {
16+
// Replace current position field ...
17+
$('#meetup_position').replaceWith(
18+
// ... with the returned one from the AJAX response.
19+
$(html).find('#meetup_position')
20+
);
21+
// Position field now displays the appropriate positions.
22+
}
23+
});
24+
});
25+
</script>

0 commit comments

Comments
 (0)