Skip to content

Commit f261c7a

Browse files
committed
Fixing bug where validation was lost on update with a passed-in-form
1 parent b783f27 commit f261c7a

File tree

5 files changed

+79
-20
lines changed

5 files changed

+79
-20
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
- Added `data-live-ignore` attribute. If included in an element, that element
66
will not be updated on re-render.
77

8-
- `ComponentWithFormTrait`: call `$this->initializeFormValues()` from
9-
`mount()` if your component has a custom `mount()`.
10-
11-
- `ComponentWithFormTrait` no longer has a `setForm()` method, just call
12-
`$this->formView = $form` from `mount()` if you have a custom mount.
13-
Hopefully that will not be needed soon.
8+
- `ComponentWithFormTrait` no longer has a `setForm()` method. But there
9+
is also no need to call it anymore. To pass an already-built form to
10+
your component, pass it as a `form` var to `component()`. If you have
11+
a custom `mount()`, you no longer need to call `setForm()` or anything else.
1412

1513
- The Live Component AJAX endpoints now return HTML in all situations
1614
instead of JSON.

src/LiveComponent/src/ComponentWithFormTrait.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
1818
use Symfony\UX\LiveComponent\Attribute\BeforeReRender;
1919
use Symfony\UX\LiveComponent\Attribute\LiveProp;
20-
use Symfony\UX\LiveComponent\Util\ArrayUtil;
20+
use Symfony\UX\LiveComponent\Util\LiveFormUtility;
2121
use Symfony\UX\TwigComponent\Attribute\PostMount;
2222

2323
/**
@@ -69,9 +69,19 @@ abstract protected function instantiateForm(): FormInterface;
6969
public function postMount(array $data): array
7070
{
7171
// allow the FormView object to be passed into the component() as "form"
72-
if (array_key_exists('form', $data)) {
72+
if (\array_key_exists('form', $data)) {
7373
$this->formView = $data['form'];
7474
unset($data['form']);
75+
76+
if ($this->formView) {
77+
// if a FormView is passed in and it contains any errors, then
78+
// we mark that this entire component has been validated so that
79+
// all validation errors continue showing on re-render
80+
if (LiveFormUtility::doesFormContainAnyErrors($this->formView)) {
81+
$this->isValidated = true;
82+
$this->validatedFields = [];
83+
}
84+
}
7585
}
7686

7787
// set the formValues from the initial form view's data
@@ -91,7 +101,7 @@ public function postMount(array $data): array
91101
public function submitFormOnRender(): void
92102
{
93103
if (!$this->getFormInstance()->isSubmitted()) {
94-
$this->submitForm(false);
104+
$this->submitForm($this->isValidated);
95105
}
96106
}
97107

@@ -146,7 +156,7 @@ private function submitForm(bool $validateAll = true): void
146156
// changed the underlying data or structure of the form
147157
$this->formValues = $this->extractFormValues($this->getForm());
148158
// remove any validatedFields that do not exist in data anymore
149-
$this->validatedFields = ArrayUtil::removePathsNotInData(
159+
$this->validatedFields = LiveFormUtility::removePathsNotInData(
150160
$this->validatedFields ?? [],
151161
[$form->getName() => $this->formValues],
152162
);

src/LiveComponent/src/Util/ArrayUtil.php renamed to src/LiveComponent/src/Util/LiveFormUtility.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\UX\LiveComponent\Util;
1313

14-
final class ArrayUtil
14+
use Symfony\Component\Form\FormView;
15+
16+
final class LiveFormUtility
1517
{
1618
/**
1719
* Removes the "paths" not present in the $data array.
@@ -22,11 +24,11 @@ final class ArrayUtil
2224
*/
2325
public static function removePathsNotInData(array $paths, array $data): array
2426
{
25-
return array_values(array_filter($paths, static function($path) use ($data) {
27+
return array_values(array_filter($paths, static function ($path) use ($data) {
2628
$parts = explode('.', $path);
27-
while (count($parts) > 0) {
29+
while (\count($parts) > 0) {
2830
$part = $parts[0];
29-
if (!array_key_exists($part, $data)) {
31+
if (!\array_key_exists($part, $data)) {
3032
return false;
3133
}
3234

@@ -40,4 +42,19 @@ public static function removePathsNotInData(array $paths, array $data): array
4042
return true;
4143
}));
4244
}
45+
46+
public static function doesFormContainAnyErrors(FormView $formView): bool
47+
{
48+
if ($formView->vars['errors'] ?? null && \count($formView->vars['errors']) > 0) {
49+
return true;
50+
}
51+
52+
foreach ($formView->children as $childView) {
53+
if (self::doesFormContainAnyErrors($childView)) {
54+
return true;
55+
}
56+
}
57+
58+
return false;
59+
}
4360
}

src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
1717
use Symfony\Component\DomCrawler\Crawler;
18+
use Symfony\Component\Form\FormFactoryInterface;
1819
use Symfony\UX\LiveComponent\LiveComponentHydrator;
20+
use Symfony\UX\LiveComponent\Tests\Fixture\Component\FormWithCollectionTypeComponent;
21+
use Symfony\UX\LiveComponent\Tests\Fixture\Form\BlogPostFormType;
1922
use Symfony\UX\TwigComponent\ComponentFactory;
2023
use Zenstruck\Browser\Response\HtmlResponse;
2124
use Zenstruck\Browser\Test\HasBrowser;
@@ -93,7 +96,6 @@ public function testFormValuesRebuildAfterFormChanges(): void
9396
'headers' => ['X-CSRF-TOKEN' => $token],
9497
])
9598
->assertStatus(422)
96-
->dump()
9799
// the original embedded form should be gone
98100
->assertNotContains('<textarea id="blog_post_form_comments_0_content"')
99101
// the added one should still be present
@@ -109,4 +111,36 @@ public function testFormValuesRebuildAfterFormChanges(): void
109111
})
110112
;
111113
}
114+
115+
public function testFormRemembersValidationFromInitialForm(): void
116+
{
117+
/** @var LiveComponentHydrator $hydrator */
118+
$hydrator = self::getContainer()->get('ux.live_component.component_hydrator');
119+
/** @var ComponentFactory $factory */
120+
$factory = self::getContainer()->get('ux.twig_component.component_factory');
121+
/** @var FormFactoryInterface $formFactory */
122+
$formFactory = self::getContainer()->get('form.factory');
123+
124+
$form = $formFactory->create(BlogPostFormType::class);
125+
$form->submit(['title' => '', 'content' => '']);
126+
/** @var FormWithCollectionTypeComponent $component */
127+
$component = $factory->create('form_with_collection_type', [
128+
'form' => $form->createView(),
129+
]);
130+
131+
// component should recognize that it is already submitted
132+
$this->assertTrue($component->isValidated);
133+
134+
$dehydrated = $hydrator->dehydrate($component);
135+
$dehydrated['blog_post_form']['content'] = 'changed description';
136+
$dehydrated['validatedFields'][] = 'blog_post_form.content';
137+
138+
$this->browser()
139+
->get('/_components/form_with_collection_type?'.http_build_query($dehydrated))
140+
// normal validation happened
141+
->assertContains('The content field is too short')
142+
// title is STILL validated as all fields should be validated
143+
->assertContains('The title field should not be blank')
144+
;
145+
}
112146
}

src/LiveComponent/tests/Unit/Util/ArrayUtilTest.php renamed to src/LiveComponent/tests/Unit/Util/LiveFormUtilityTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@
1212
namespace Symfony\UX\LiveComponent\Tests\Unit\Util;
1313

1414
use PHPUnit\Framework\TestCase;
15-
use Symfony\UX\LiveComponent\Util\ArrayUtil;
15+
use Symfony\UX\LiveComponent\Util\LiveFormUtility;
1616

17-
final class ArrayUtilTest extends TestCase
17+
final class LiveFormUtilityTest extends TestCase
1818
{
1919
/**
2020
* @dataProvider getPathsTests
2121
*/
2222
public function testRemovePathsNotInData(array $inputPaths, array $inputData, array $expectedPaths)
2323
{
24-
$this->assertEquals($expectedPaths, ArrayUtil::removePathsNotInData($inputPaths, $inputData));
24+
$this->assertEquals($expectedPaths, LiveFormUtility::removePathsNotInData($inputPaths, $inputData));
2525
}
2626

2727
public function getPathsTests(): iterable
@@ -49,8 +49,8 @@ public function getPathsTests(): iterable
4949
[
5050
'name' => 'Ryan',
5151
'posts' => [
52-
1 => ['title' => '1 index post']
53-
]
52+
1 => ['title' => '1 index post'],
53+
],
5454
],
5555
['name', 'posts.1.title'],
5656
];

0 commit comments

Comments
 (0)