ux icon indicating copy to clipboard operation
ux copied to clipboard

[LiveComponent] Custom form errors not displayed when invoking live action

Open IvoPereira opened this issue 2 years ago • 11 comments
trafficstars

Hi there.

I believe I encountered an issue with the Symfony UX's ComponentWithFormTrait where form errors are not displayed when a live action is invoked from a component template.

To give a brief context: I have some business logic that throws some very specific Domain Errors that I would need to map and display to the user.

I'm using the ComponentWithFormTrait to handle forms in my live component. When invoking a live action, I'm trying to add both global form errors and specific field errors. However, despite adding these errors, they are not displayed in the form.

Reproduction Steps:

  • Create a form using ComponentWithFormTrait.
  • In the live action method, add a global form error and a field-specific error.
  • Invoke the live action from the component template.
  • Observe that the errors are not displayed in the form.

Minimal Reproducible Code: Component:

use ComponentWithFormTrait;

#[LiveAction]
public function save()
{
    // Attempting to add a global form error
    $this->getForm()->addError(new \Symfony\Component\Form\FormError('General error'));
    
    // Attempting to add a specific field error
    $this->getForm()->get('description')->addError(new \Symfony\Component\Form\FormError('Form field error'));
}

Template:

<div {{ attributes }}>
    {{ form_start(form, {
        attr: {
            'data-action': 'live#action',
            'data-action-name': 'prevent|save'
        }
    }) }}
    {{ form_errors(form) }}
    {{ form_row(form.description) }}

    <button type="submit">Save</button>
    {{ form_end(form) }}
</div>

If it makes any difference, my form is using a data-transfer object for the form data. I have noticed that with a regular Symfony action+Twig template this does work.

I suspect that somehow the form instance might not be the same getting rendered?

Is this a known issue, or is there a recommended way to force the form errors to display in this scenario?

IvoPereira avatar Sep 18 '23 07:09 IvoPereira

Hey Ivo!

Real quick, does it help if you submit the form first in your LiveAction?

use ComponentWithFormTrait;

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

    // Attempting to add a global form error
    $this->getForm()->addError(new \Symfony\Component\Form\FormError('General error'));
    
    // Attempting to add a specific field error
    $this->getForm()->get('description')->addError(new \Symfony\Component\Form\FormError('Form field error'));
}

From the code I see above, you were not calling this. When you don't submit a form in your LiveAction, it is submitted for your AFTER the LiveAction. So it's possible that you're adding the error... then it's immediately cleared out when the form submits a moment later.

Cheers!

weaverryan avatar Sep 18 '23 18:09 weaverryan

Hi Ryan!

It doesn't, unfortunately.

I also tried that before, now I had totally forgotten to include it in the reproducible example.

Let me know if there is anything else I can test.

Cheers

IvoPereira avatar Sep 19 '23 05:09 IvoPereira

The problem might be that $this->submitForm() throws an exception if the form is invalid. This is so that, if you write code below it to handle success, it's not called. I think this might be working against you. Try:

try {
    $this->submitForm();
} (catch UnprocessableEntityHttpException $e) {
    // add your custom error here

    throw $e;
}

weaverryan avatar Sep 19 '23 13:09 weaverryan

Thanks for the quick reply.

Let me provide a bit more context (I didn't provide it initially, as I believed the issue was not related).

In my forms, after submitForm() and being sure the form is valid, I usually dispatch a handler/call a service to validate/persist my domain data.

Let's say I am building a signup form. I have in my service a validation that checks for instance whether the given email is unique. If it isn't, it throws an EmailAlreadyInUseException.

Tldr; while the majority of invalid fields can be caught using the form validations, I need to catch service Exceptions and map them to form fields as FormErrors as well.

Simplified example:

use ComponentWithFormTrait;

#[LiveAction]
public function save()
{
    $this->submitForm();
    // form should be valid at this stage

    try {
        $this->userService->create($this->dto); // let's say we initially created the form with a dto
    } catch (EmailAlreadyInUseException) {
        // Attempting to add a global form error
        $this->getForm()->addError(new \Symfony\Component\Form\FormError('General error: Email already in use'));
    
        // Attempting to add a specific field error
        $this->getForm()->get('email')->addError(new \Symfony\Component\Form\FormError('Field error: Email already in use'));
    }
}

IvoPereira avatar Sep 21 '23 10:09 IvoPereira

Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?

carsonbot avatar Apr 25 '24 12:04 carsonbot

Friendly reminder that this issue exists. If I don't hear anything I'll close this.

carsonbot avatar May 11 '24 07:05 carsonbot

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

carsonbot avatar May 25 '24 07:05 carsonbot

This issue is still relevant, I'm facing the same problem

dpinheiro avatar Jun 05 '24 13:06 dpinheiro

Unfortunately I am still facing this as well.

Did you had a chance to look further into the context I have added last time @weaverryan?

IvoPereira avatar Jun 05 '24 13:06 IvoPereira

Did you guys tried to throw a UnprocessableEntityExcetion there ?

smnandre avatar Jun 05 '24 17:06 smnandre

Did you guys tried to throw a UnprocessableEntityExcetion there ?

Yes, I'm throwing an UnprocessableEntityExcetion. I believe I have found the problem and a workaround for it.

When calling some live action method, the first call is to submitFormOnRender, and if the form was not yet submitted form then it's submitted, it gets validated, and then the formView is filled. Later when a custom exception is thrown, we associate the error to the form, and throw an UnprocessableEntityExcetion so then it's handled by the method onKernelException of the class LiveComponentSubscriber. There it will try to create a response using the ComponentRenderer, that will try to obtain the component variables and then render the associated twig template with those variables. The method exposedVariables is obtaining the exposed formView (#[ExposeInTemplate(name: 'form', getter: 'getFormView')]), the problem is that this formView is cached, so it only contains the initial status, and not the new error that was added later.

A workaround that worked for me was to reset the formView to force it to be calculated again.

Taking the example from @IvoPereira

use ComponentWithFormTrait;

#[LiveAction]
public function save()
{
    $this->submitForm();
    // form should be valid at this stage

    try {
        $this->userService->create($this->dto); // let's say we initially created the form with a dto
    } catch (EmailAlreadyInUseException) {        
        // Attempting to add a global form error
        $this->getForm()->addError(new \Symfony\Component\Form\FormError('General error: Email already in use'));
    
        // Attempting to add a specific field error
        $this->getForm()->get('email')->addError(new \Symfony\Component\Form\FormError('Field error: Email already in use'));
        
        $this->formView = null; // this will clean the formView status so it could be rendered with the new errors that were added
    }
}

dpinheiro avatar Jun 06 '24 09:06 dpinheiro

Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?

carsonbot avatar Dec 07 '24 12:12 carsonbot

Just a quick reminder to make a comment on this. If I don't hear anything I'll close this.

carsonbot avatar Dec 21 '24 12:12 carsonbot

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

carsonbot avatar Jan 04 '25 12:01 carsonbot

Still having this issue on Symfony 7.2 / UX 2.23. @dpinheiro's solution resolved the issue, but it's difficult to track down

kentrichards avatar Feb 25 '25 22:02 kentrichards

Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?

carsonbot avatar Aug 26 '25 12:08 carsonbot

Hello? This issue is about to be closed if nobody replies.

carsonbot avatar Sep 09 '25 12:09 carsonbot

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

carsonbot avatar Sep 23 '25 12:09 carsonbot