I have a problem with Symfony 6.4 forms.
I have a FirstType form that has only one field of type CollectionType with an entry type SecondType. SecondType has several fields (NumberType, TextType...)
Finally, I have a Stimulus controller to manage the dynamic addition of children (SecondType) to the parent form (FirstType).
When I add elements and submit the form, if no errors are detected (via Symfony constraints), then everything is fine and my childrens are persisted in my parent entity.
On the other hand, if I have any errors, the form is displayed again to show me these errors (this behavior is desired).
From now on, whether I correct my errors or not, when I resubmit the form, my Request contains only the 1st child (the others are no longer in the Request).
If anyone has already encountered this problem and has an answer/idea, I'd love to hear from you!
FirstType.php (Parent FormType) :
<?php
namespace App\Form;
use App\Entity\FirstEntity;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class FirstType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('childrens', CollectionType::class, [
'entry_type' => SecondType::class,
'entry_options' => [
'label' => false,
'container_model' => $builder->getData(),
],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => true,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
'data_class' => FirstEntity::class,
])
;
}
}
SecondType.php (Child FormType) :
<?php
namespace App\Form;
use App\Entity\SecondEntity;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class SecondType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('rowLetter', ChoiceType::class)
->add('quantity', IntegerType::class)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
'data_class' => SecondEntity::class,
]);
}
}
form/form-collection-table_controller.js (Stimulus Controller) :
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['collectionContainer']
connect () {
this.prototypeValue = this.element.dataset.formCollectionPrototypeValue
this.indexValue = parseInt(this.element.dataset.formCollectionIndexValue) || 0
this.deleteButtonTextValue = this.element.dataset.formCollectionDeleteButtonTextValue
if (document.referrer !== window.location.href) {
this.addCollectionElement()
}
}
addCollectionElement (event) {
const tableRow = document.createElement('tr')
tableRow.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue.toString())
const actionColumn = document.createElement('td')
const deleteButton = document.createElement('button')
deleteButton.type = 'button'
deleteButton.textContent = this.deleteButtonTextValue
deleteButton.className = 'btn btn-danger float-end'
deleteButton.dataset.action = 'click->form--form-collection-table#removeCollectionElement'
actionColumn.appendChild(deleteButton)
tableRow.appendChild(actionColumn)
this.collectionContainerTarget.appendChild(tableRow)
this.indexValue++
}
removeCollectionElement (event) {
const tableRow = event.target.closest('tr')
if (tableRow) {
this.collectionContainerTarget.removeChild(tableRow)
}
}
}
EmbedCollectionController.php (Controller) :
<?php
namespace App\Controller;
use App\Entity\FirstEntity;
use App\Form\FirstType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class EmbedCollectionController extends AbstractController
{
public function indexAction(Request $request, int $id): Response
{
// ...
$form = $this->createForm(FirstType::class, $firstEntity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($firstEntity);
$this->entityManager->flush();
return $this->redirectToRoute('app_index');
}
return $this->render('index.html.twig', [
'firstEntity' => $firstEntity,
'form' => $form->createView(),
]);
}
}
index.html.twig (Twig view) :
{% block content %}
<!-- ... -->
<tbody {{ stimulus_target('form--form-collection-table', 'collectionContainer') }}>
{% for children in form.childrens %}
<tr>
{{ form(children) }}
<td>
<button type="button" class="btn btn-danger float-end" {{ stimulus_action('form--form-collection-table', 'removeCollectionElement') }}>
Remove
</button>
</td>
</tr>
{% endfor %}
</tbody>
<!-- ... -->
{% endblock %}