symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
855 stars 315 forks source link

[InteractsWithLiveComponent] Invoking a LiveAction submitting a Form with a RepeatedType-Password fails upon validation. #2028

Open Mauriceu opened 3 months ago

Mauriceu commented 3 months ago

Hello UX-Team,

first off, thank you for this initiative - developing frontends with symfony is actually fun again.

I am uncertain whether this is an issue relevant for this repo, but here goes:

I want to test form submits of a LiveComponent with the "ComponentWithFormTrait".

The Component:

#[LiveProp(writable: true)]
public ?FormModel $initialFormData = null;

protected function instantiateForm(): FormInterface
{
    $this->initialFormData = $this->initialFormData ?? new FormModel();
    return $this->createForm(FormType::class, $this->initialFormData);
}

#[LiveAction]
public function save(): Response
{
    $this->submitForm();

    return new Response('success');
}

The FormType:


class FormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('password', RepeatedType::class, [
                'label' => 'Password',
                'type' => PasswordType::class,
                'options' => ['always_empty' => false]
            ]);
    }
}

The FormModel:

class FormModel
{
    #[NotBlank]
    public ?string $password = null;
}

Now the TestCase is pretty simple as well:

    use InteractsWithLiveComponents;

    public function testFormComponent(): void
    {
        // instantiate model with valid value
        $model = new FormModel();
        $model->password = 'some_password';

        $testComponent = $this->createLiveComponent(FormComponent::class, ['initialFormData' => $model]);
        /** @var FormComponent $component */
        $component = $testComponent->component();
        try {
            $response = $component->save();
        } catch (Throwable $e) {
            self::assertFalse('Did not expect component to fail with message: '.$e->getMessage());
        }
    }

Invoking the "save()" function always yields a validation error, because the Component does not correctly populate the "initialFormData" property. Creating the Component with "createLiveComponent" and retrieving it with "component()" works as expected - the model contains the "password" property and the FormView is set accordingly. But when invoking the "save()" function, the models "password" property is suddenly set to "null". Changing the FormType to use a "TextType" instead of "PasswordType" works, the "password" property is not reset - however the "always_empty" property is set to false in the FormType-builder, so that should not be an issue...

Whether this is more of a Symfony/Form or a PIBKAC issue I dont know...but maybe someone knows whats going on.

I created a small reproducer here It runs within a docker compose environment, for convenience. Just run "bin/phpunit". I added a few dump() statements to emphasize relevant parts.

smnandre commented 3 months ago

Took me some time to realize there was no stimulus-bundle in your composer.json ... 😅

It seems "logical", as your model contains only one field, and the HTML contains two of them (first and second).

I think you should try with a custom hydration method / extension.

Mauriceu commented 3 months ago

oh, yea I did not think it necessary so I did not include stimulus ^^

I understand that on traditional form submits the html contains multiple entries for the repeated type. However, within this TestCase I'd wager that the HTML content and form submits through submit-buttons are irrelevant, because the form model is populated when creating the LiveComponent - also the LiveAction is invoked directly, so no post/pre mount hooks should have been called. I would have thought the Form is populated only according to the "initialFormData" property, because that is the only data source available. The $this->createLiveComponent(FormComponent::class, ['initialFormData' => $model]); and $testComponent->component(); calls did populate the password property correctly - however that did not happen within the LiveAction.