From c6189b447160a1b8f5b43808da9a294ee73a9456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurice=20Niederges=C3=A4=C3=9F?= Date: Tue, 25 Feb 2025 11:51:20 +0100 Subject: [PATCH 1/3] Added forceCreate parameter to ComponentWithFormTrait::getFormView() forcing a recreation of the FormView. Invoked only once with true in ComponentWithFormTrait::submitFormOnRender() to synchronize FormView and FormInstance --- src/LiveComponent/src/ComponentWithFormTrait.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/LiveComponent/src/ComponentWithFormTrait.php b/src/LiveComponent/src/ComponentWithFormTrait.php index a25a2d1dc02..abfbc8b23e0 100644 --- a/src/LiveComponent/src/ComponentWithFormTrait.php +++ b/src/LiveComponent/src/ComponentWithFormTrait.php @@ -111,11 +111,16 @@ public function submitFormOnRender(): void if ($this->shouldAutoSubmitForm) { $this->submitForm($this->isValidated); } + + // Recreate the FormView because it has been created by the submitForm() with a FormInterface whose values may + // have changed. Basically synchronizes FormView and FormInstance to reflect all manual changes made to the + // latter between form submit and the components re-render. + $this->getFormView(true); } - public function getFormView(): FormView + public function getFormView(bool $forceCreate = false): FormView { - if (null === $this->formView) { + if ($forceCreate || null === $this->formView) { $this->formView = $this->getForm()->createView(); $this->useNameAttributesAsModelName(); } From 1297e56d23914ec6b600220c78a0a025bbf4dce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurice=20Niederges=C3=A4=C3=9F?= Date: Tue, 25 Feb 2025 17:12:31 +0100 Subject: [PATCH 2/3] Added TestCase asserting that manually added FormError is rendered in template. --- .../FormWithCollectionTypeComponent.php | 10 +++++++ .../Functional/Form/ComponentWithFormTest.php | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/LiveComponent/tests/Fixtures/Component/FormWithCollectionTypeComponent.php b/src/LiveComponent/tests/Fixtures/Component/FormWithCollectionTypeComponent.php index 1686b9a9987..ca86b74209c 100644 --- a/src/LiveComponent/tests/Fixtures/Component/FormWithCollectionTypeComponent.php +++ b/src/LiveComponent/tests/Fixtures/Component/FormWithCollectionTypeComponent.php @@ -12,7 +12,9 @@ namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveArg; @@ -42,6 +44,14 @@ protected function instantiateForm(): FormInterface return $this->createForm(BlogPostFormType::class, $this->post); } + #[LiveAction] + public function submitAndAddErrorToForm(): void + { + $this->submitForm(); + $this->getForm()->addError(new FormError("manually added form error")); + throw new UnprocessableEntityHttpException(); + } + #[LiveAction] public function addComment() { diff --git a/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php b/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php index bfbe477c948..b504bd17d9c 100644 --- a/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php +++ b/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php @@ -156,6 +156,33 @@ public function testFormRemembersValidationFromInitialForm(): void ; } + public function testFormViewSynchronizesWithFormInstance(): void + { + /** @var FormFactoryInterface $formFactory */ + $formFactory = self::getContainer()->get('form.factory'); + + $form = $formFactory->create(BlogPostFormType::class); + // make sure validation does not fail on content constraint (min 100 characters) + $validContent = implode('a', range(0, 100)); + $form->submit(['title' => 'Title', 'content' => $validContent]); + + $mounted = $this->mountComponent('form_with_collection_type', [ + 'form' => $form->createView(), + ]); + $dehydratedProps = $this->dehydrateComponent($mounted)->getProps(); + + $this->browser() + // post to action, which will manually add a FormError to the FormInstance after submit + ->post('/_components/form_with_collection_type/submitAndAddErrorToForm', [ + 'body' => ['data' => json_encode(['props' => $dehydratedProps])], + ]) + // action always throws 422 + ->assertStatus(422) + // assert manually added error within LiveAction after submit is rendered in template + ->assertContains('manually added form error') + ; + } + public function testHandleCheckboxChanges(): void { $category = CategoryFixtureEntityFactory::createMany(5); From 173f96bb0ebed69e08f40a760fda52854073985a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurice=20Niederges=C3=A4=C3=9F?= Date: Wed, 26 Feb 2025 17:21:55 +0100 Subject: [PATCH 3/3] moved recreation of FormView into else block to avoid unnecessary recreation in case "shouldAutoSubmitForm" is true --- src/LiveComponent/src/ComponentWithFormTrait.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/LiveComponent/src/ComponentWithFormTrait.php b/src/LiveComponent/src/ComponentWithFormTrait.php index abfbc8b23e0..f7a7caddcdb 100644 --- a/src/LiveComponent/src/ComponentWithFormTrait.php +++ b/src/LiveComponent/src/ComponentWithFormTrait.php @@ -110,12 +110,12 @@ public function submitFormOnRender(): void { if ($this->shouldAutoSubmitForm) { $this->submitForm($this->isValidated); + } else { + // Recreate the FormView because it has been created by the submitForm() with a FormInterface whose values may + // have changed. Basically synchronizes FormView and FormInstance to reflect all manual changes made to the + // latter between form submit and the components re-render. + $this->getFormView(true); } - - // Recreate the FormView because it has been created by the submitForm() with a FormInterface whose values may - // have changed. Basically synchronizes FormView and FormInstance to reflect all manual changes made to the - // latter between form submit and the components re-render. - $this->getFormView(true); } public function getFormView(bool $forceCreate = false): FormView