'Symfony reusable AJAX select / ChoiceType
I want to create a reusable AJAX-based select (select2) using Symfony form types and I've spent quite some time on it but can't get it to work like I want.
As far as I know you cannot override options of form fields after they have been added, so you have to re-add them with the new config. The Symfony docs also provide some examples on how to dynamically add or modify forms using events. https://symfony.com/doc/current/form/dynamic_form_modification.html
I've managed to create my AJAX based elements in the form and it's working but not completely reusable yet:
- Form field extends Doctrine EntityType to have full support of data mappers etc
- Form field is initialized with
'choices' => [],so Doctrine does not load any entities from db - Existing choices on edit is added during
FormEvents::PRE_SET_DATA - Posted choices are added during
FormEvents::PRE_SUBMIT
The current setup works but only in the parent form. I want to have my AjaxNodeType completely reusable so the parent form does not need to care about data handling and events.
Following the 2 examples, parent and child form. Here with event listeners in both, but of course they should only be in one.
Is it simply not possible in single element types to replace "yourself" in the parent form or am I doing something wrong? Is there any other way to dynamically change the choices?
Parent form This works!
class MyResultElementType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'fixedNode',
AjaxNodeType::class,
[
'label' => 'Fixed node',
'required' => false,
]
);
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (PreSetDataEvent $event) {
if ($event->getData()) {
$fixedNode = $event->getData()->getFixedNode();
//this works here but not in child???
if ($fixedNode) {
$name = 'fixedNode';
$parentForm = $event->getForm();
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$fixedNode];
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
},
1000
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (PreSubmitEvent $event) {
$data = $event->getData()['fixedNode'];
if ($data) {
$name = 'fixedNode';
$parentForm = $event->getForm();
// we have to add the POST-ed data/node here to the choice list
// otherwise the submitted value is not valid
$node = $this->entityManager->find(Node::class, $data);
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$node];
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => MyResultElement::class,
'method' => 'POST',
]
);
}
}
Child form / single select This does NOT work. On POST the fixedNode field is not set to the form data-entity.
class AjaxNodeType extends AbstractType
{
/** @var EntityManager */
private $entityManager;
public function __construct(
EntityManager $entityManager
) {
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
//this does not work here but in parent???
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (PreSetDataEvent $event) {
if ($event->getData()) {
$fixedNode = $event->getData();
$name = $event->getForm()->getName();
$parentForm = $event->getForm()->getParent();
$options = $parentForm->get($name)->getConfig()->getOptions();
$newChoices = [$fixedNode];
// check if the choices already match, otherwise we'll end up in an endless loop ???
if ($options['choices'] !== $newChoices) {
$options['choices'] = $newChoices;
$parentForm->add($name, AjaxNodeType::class, $options);
}
}
},
1000
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (PreSubmitEvent $event) {
if ($event->getData()) {
$name = $event->getForm()->getName();
$data = $event->getData();
$parentForm = $event->getForm()->getParent();
// we have to add the POST-ed data/node here to the choice list
// otherwise the submitted value is not valid
$node = $this->entityManager->find(Node::class, $data);
$options = $parentForm->get($name)->getConfig()->getOptions();
$options['choices'] = [$node];
$parentForm->add($name, self::class, $options);
}
},
1000
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'class' => Node::class,
// prevent doctrine from loading ALL nodes
'choices' => [],
]
);
}
public function getParent(): string
{
return EntityType::class;
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
