Skip to content

Commit b4d20b6

Browse files
committed
Reworking for much simpler solution
1 parent 09ab0c1 commit b4d20b6

File tree

10 files changed

+144
-59
lines changed

10 files changed

+144
-59
lines changed

src/LiveComponent/src/ComponentWithFormTrait.php

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,6 @@ trait ComponentWithFormTrait
5858
#[LiveProp(writable: true)]
5959
public array $validatedFields = [];
6060

61-
/**
62-
* Internal flag to track if the form should be submitted on render.
63-
*/
64-
private bool $didFormSubmit = false;
65-
6661
/**
6762
* Return the full, top-level, Form object that this component uses.
6863
*/
@@ -97,7 +92,7 @@ public function mount(FormView $form = null)
9792
#[BeforeReRender]
9893
public function submitFormOnRender(): void
9994
{
100-
if (!$this->getFormInstance()->isSubmitted() && !$this->didFormSubmit) {
95+
if (!$this->getFormInstance()->isSubmitted()) {
10196
$this->submitForm(false);
10297
}
10398
}
@@ -128,11 +123,13 @@ private function initializeFormValues(): void
128123
$this->formValues = $this->extractFormValues($this->getForm());
129124
}
130125

131-
private function submitForm(bool $validateAll = true, bool $throwOnValidationError = true): void
126+
private function submitForm(bool $validateAll = true): void
132127
{
133128
$form = $this->getFormInstance();
134129
$form->submit($this->formValues);
135-
$this->didFormSubmit = true;
130+
// re-extract the "view" values in case the submitted data
131+
// changed the underlying data or structure of the form
132+
$this->formValues = $this->extractFormValues($this->getForm());
136133

137134
if ($validateAll) {
138135
// mark the entire component as validated
@@ -146,26 +143,11 @@ private function submitForm(bool $validateAll = true, bool $throwOnValidationErr
146143
$this->clearErrorsForNonValidatedFields($form, $this->getFormName());
147144
}
148145

149-
if ($throwOnValidationError && !$form->isValid()) {
146+
if (!$form->isValid()) {
150147
throw new UnprocessableEntityHttpException('Form validation failed in component');
151148
}
152149
}
153150

154-
/**
155-
* Call this to rebuild the form objects.
156-
*
157-
* This is useful in action where you call $this->submitForm(),
158-
* then, after, change some of the form's underlying data. By
159-
* call this function, the form can rebuild using the new data.
160-
*/
161-
private function refreshForm(): void
162-
{
163-
$this->formInstance = null;
164-
$this->formView = null;
165-
// re-initialize the form object and the formValues from it
166-
$this->initializeFormValues();
167-
}
168-
169151
/**
170152
* Returns a hierarchical array of the entire form's values.
171153
*

src/LiveComponent/tests/Fixture/Component/FormThatResetsComponent.php renamed to src/LiveComponent/tests/Fixture/Component/FormWithCollectionTypeComponent.php

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,34 @@
1717
use Symfony\UX\LiveComponent\Attribute\LiveAction;
1818
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
1919
use Symfony\UX\LiveComponent\DefaultActionTrait;
20+
use Symfony\UX\LiveComponent\Tests\Fixture\Dto\BlogPost;
21+
use Symfony\UX\LiveComponent\Tests\Fixture\Dto\Comment;
2022
use Symfony\UX\LiveComponent\Tests\Fixture\Entity\Entity1;
21-
use Symfony\UX\LiveComponent\Tests\Fixture\Form\FormForEntity1Type;
23+
use Symfony\UX\LiveComponent\Tests\Fixture\Form\BlogPostFormType;
2224

23-
#[AsLiveComponent('form_that_resets')]
24-
class FormThatResetsComponent extends AbstractController
25+
#[AsLiveComponent('form_with_collection_type')]
26+
class FormWithCollectionTypeComponent extends AbstractController
2527
{
2628
use ComponentWithFormTrait;
2729
use DefaultActionTrait;
2830

29-
public Entity1 $entity1;
31+
public BlogPost $post;
3032

3133
public function __construct()
3234
{
33-
$this->entity1 = new Entity1();
35+
$this->post = new BlogPost();
36+
// start with 1 comment
37+
$this->post->comments[] = new Comment();
3438
}
3539

3640
protected function instantiateForm(): FormInterface
3741
{
38-
return $this->createForm(FormForEntity1Type::class, $this->entity1);
42+
return $this->createForm(BlogPostFormType::class, $this->post);
3943
}
4044

4145
#[LiveAction]
42-
public function changeTextField()
46+
public function addComment()
4347
{
44-
// submit the values, but no further validation
45-
$this->submitForm(false, false);
46-
47-
$this->entity1->name = 'changing name from action';
48-
49-
$this->refreshForm();
48+
$this->formValues['comments'][] = [];
5049
}
5150
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Tests\Fixture\Dto;
13+
14+
use Symfony\Component\Validator\Constraints\Length;
15+
16+
class BlogPost
17+
{
18+
public $title;
19+
20+
#[Length(min: 100)]
21+
public $content;
22+
23+
public $comments = [];
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Tests\Fixture\Dto;
13+
14+
class Comment
15+
{
16+
public ?string $content;
17+
18+
public ?BlogPost $blogPost;
19+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\UX\LiveComponent\Tests\Fixture\Form;
15+
16+
use Symfony\Component\Form\AbstractType;
17+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
18+
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
19+
use Symfony\Component\Form\Extension\Core\Type\TextType;
20+
use Symfony\Component\Form\FormBuilderInterface;
21+
use Symfony\Component\OptionsResolver\OptionsResolver;
22+
use Symfony\UX\LiveComponent\Tests\Fixture\Dto\BlogPost;
23+
24+
class BlogPostFormType extends AbstractType
25+
{
26+
public function buildForm(FormBuilderInterface $builder, array $options)
27+
{
28+
$builder
29+
->add('title', TextType::class)
30+
->add('content', TextareaType::class)
31+
->add('comments', CollectionType::class, [
32+
'entry_type' => CommentFormType::class,
33+
'allow_add' => true,
34+
'allow_delete' => true,
35+
])
36+
;
37+
}
38+
39+
public function configureOptions(OptionsResolver $resolver)
40+
{
41+
$resolver->setDefaults([
42+
'csrf_protection' => false,
43+
'data_class' => BlogPost::class,
44+
]);
45+
}
46+
}

src/LiveComponent/tests/Fixture/Form/FormForEntity1Type.php renamed to src/LiveComponent/tests/Fixture/Form/CommentFormType.php

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,24 @@
1515

1616
use Symfony\Component\Form\AbstractType;
1717
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
18-
use Symfony\Component\Form\Extension\Core\Type\TextType;
1918
use Symfony\Component\Form\FormBuilderInterface;
2019
use Symfony\Component\OptionsResolver\OptionsResolver;
21-
use Symfony\UX\LiveComponent\Tests\Fixture\Entity\Entity1;
20+
use Symfony\UX\LiveComponent\Tests\Fixture\Dto\Comment;
2221

23-
/**
24-
* @author Jakub Caban <[email protected]>
25-
*/
26-
class FormForEntity1Type extends AbstractType
22+
class CommentFormType extends AbstractType
2723
{
2824
public function buildForm(FormBuilderInterface $builder, array $options)
2925
{
3026
$builder
31-
->add('name', TextType::class)
32-
->add('description', TextareaType::class)
27+
->add('content', TextareaType::class)
3328
;
3429
}
3530

3631
public function configureOptions(OptionsResolver $resolver)
3732
{
3833
$resolver->setDefaults([
3934
'csrf_protection' => false,
40-
'data_class' => Entity1::class,
35+
'data_class' => Comment::class,
4136
]);
4237
}
4338
}

src/LiveComponent/tests/Fixture/Kernel.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
use Symfony\UX\LiveComponent\Tests\Fixture\Component\Component3;
2828
use Symfony\UX\LiveComponent\Tests\Fixture\Component\Component6;
2929
use Symfony\UX\LiveComponent\Tests\Fixture\Component\ComponentWithAttributes;
30-
use Symfony\UX\LiveComponent\Tests\Fixture\Component\FormThatResetsComponent;
30+
use Symfony\UX\LiveComponent\Tests\Fixture\Component\FormWithCollectionTypeComponent;
3131
use Symfony\UX\TwigComponent\TwigComponentBundle;
3232
use Twig\Environment;
3333

@@ -70,6 +70,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
7070
$c->register(Component6::class)->setAutoconfigured(true)->setAutowired(true);
7171
$c->register(ComponentWithAttributes::class)->setAutoconfigured(true)->setAutowired(true);
7272
$c->register(FormThatResetsComponent::class)->setAutoconfigured(true)->setAutowired(true);
73+
$c->register(FormWithCollectionTypeComponent::class)->setAutoconfigured(true)->setAutowired(true);
7374

7475
$c->loadFromExtension('framework', [
7576
'secret' => 'S3CRET',

src/LiveComponent/tests/Fixture/templates/components/form_that_resets.html.twig

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div{{ attributes }}>
2+
{{ form(this.form) }}
3+
</div>

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace Symfony\UX\LiveComponent\Tests\Functional\EventListener;
1515

1616
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
17+
use Symfony\Component\DomCrawler\Crawler;
1718
use Symfony\UX\LiveComponent\LiveComponentHydrator;
1819
use Symfony\UX\TwigComponent\ComponentFactory;
1920
use Zenstruck\Browser\Response\HtmlResponse;
@@ -36,26 +37,46 @@ public function testRefreshForm(): void
3637
$hydrator = self::getContainer()->get('ux.live_component.component_hydrator');
3738
/** @var ComponentFactory $factory */
3839
$factory = self::getContainer()->get('ux.twig_component.component_factory');
39-
$component = $factory->create('form_that_resets');
40+
$component = $factory->create('form_with_collection_type');
4041

4142
$dehydrated = $hydrator->dehydrate($component);
4243
$token = null;
4344

4445
$this->browser()
45-
->throwExceptions()
46-
->get('/_components/form_that_resets?'.http_build_query($dehydrated))
46+
->get('/_components/form_with_collection_type?'.http_build_query($dehydrated))
4747
->use(function (HtmlResponse $response) use (&$dehydrated, &$token) {
4848
// mimic user typing
49-
$dehydrated['form_for_entity1']['description'] = 'changed description by user';
49+
$dehydrated['blog_post_form']['content'] = 'changed description by user';
50+
$dehydrated['validatedFields'] = ['blog_post_form.content'];
5051
$token = $response->crawler()->filter('div')->first()->attr('data-live-csrf-value');
5152
})
5253
// post to action, which will change the "name" field in the form
53-
->post('/_components/form_that_resets/changeTextField?'.http_build_query($dehydrated), [
54+
->post('/_components/form_with_collection_type/addComment?'.http_build_query($dehydrated), [
5455
'headers' => ['X-CSRF-TOKEN' => $token],
5556
])
56-
->assertSuccessful()
57-
->assertContains('value="changing name from action"')
57+
->assertStatus(422)
58+
->dump()
59+
// look for original embedded form
60+
->assertContains('<textarea id="blog_post_form_comments_0_content"')
61+
// look for new embedded form
62+
->assertContains('<textarea id="blog_post_form_comments_1_content"')
63+
// changed text is still present
5864
->assertContains('changed description by user</textarea>')
65+
// check that validation happened and stuck
66+
->assertContains('This value is too short. It should have 100 characters or more.')
67+
->use(function(Crawler $crawler) {
68+
$div = $crawler->filter('[data-controller="live"]');
69+
$liveData = json_decode($div->attr('data-live-data-value'), true);
70+
// make sure the 2nd collection type was initialized, that it didn't
71+
// just "keep" the empty array that we set it to in the component
72+
$this->assertEquals(
73+
[
74+
['content' => ''],
75+
['content' => ''],
76+
],
77+
$liveData['blog_post_form']['comments']
78+
);
79+
})
5980
;
6081
}
6182
}

0 commit comments

Comments
 (0)