Skip to content

[LiveComponent] How do I manually add a FormError to the Form when my submit-function fails? #1981

@Mauriceu

Description

@Mauriceu

Let me preface by saying "yea I know this aint probably the right place to ask", but I am fairly certain this problem is unwanted behaviour...but I'd be glad to be proven wrong.

The LiveComponent would be pretty simple:

#[AsLiveComponent(template: 'some_template.html.twig')]
class SomeLiveComponent extends AbstractController
{
     use DefaultActionTrait, ComponentWithFormTrait;
    
     protected function instantiateForm(): FormInterface
     {
          return $this-createForm(SomeFormType::class);
     }

     #[LiveAction]
     public function save(): ?Response
     {
          try {
               /// ... do some persistence 
          } catch (\Throwable $e) {
               $error = new FormError('An error occurred');
               $this->form->addError($error); // <--- How would I display this error in my template?
          }
     }

The template, as simple as can be:

{# some_template.html.twig #}

{{ form_start(form, 
     {
          'attr': {
               'data-action': 'live#action:prevent',
               'data-live-action-param': 'save'
          }
     })
}}

{{ form_errors(form) }} <!-- The Error should be displayed here -->
{{ form_row(form.someField) }}

<button type="submit" data-loading="action(save)|addAttribute(disabled)">Submit</button>

{{ form_end(form) }}

A somewhat ugly workaround would be adding a $formErrors array property to the component and populating it with whatever error-messages occur. You could then simply display all those messages in the template, though due to how detached that is from the normal symfony form workflow I would rather avoid this.

I understand that throwing a UnprocessableEntityHttpException() makes turbo re-render the HTML it receives in the response, so the "save()" function should throw that whenever it fails. But the errors are not added to the response HTML anyway - either because the HTML has already been rendered or the FormErrors are removed manually...though I have no idea

I tried adding a writable LiveProp array property to my component which would be populated with FormErrors throughout the submit process - similar to the hacky $formErrors array property mentioned above. But the "instantiateForm()" function would add these errors manually to the form - but that does not work either.

Any pointers would be helpful.

Activity

WebMamba

WebMamba commented on Jul 13, 2024

@WebMamba
Contributor

Sorry I don't get what you are trying to do here... What error are you trying to catch ? Form errors happen when the value submitted by the user are not valid but no errors are throw the errors are just added to the form. If the error came from outside the form it's a good idea to have an another props for the error outside the form.

Mauriceu

Mauriceu commented on Jul 13, 2024

@Mauriceu
Author

The Idea is that apart from FormErrors being added by Constraints, FormErrors would also be added manually by the Component.

For example, a User submits some data and this data - if valid from a constraint perspective - is then sent to a third party which validates it against some internal constraints and saves it. If this third party rejects the data, or an error occurs in transit, or while saving it, I would like the Form to reflect that by manually adding a FormError.

To expand on the example above:

     #[LiveAction]
     public function save(): ?Response
     {
          /** @var SomeModel $model */
          $model = $this->getForm()->getData();
          try {
               $this->someService->save($model);
          } catch (\Throwable $e) {

               // Possible error cases would be:
               // An error occurred during transit
               // Third party server is not available

               // manually add an error to the form which would then be rendered by the 
               // "form_errors(form)" twig function
               $error = new FormError('An error occurred while saving your data');
               $this->form->addError($error);
          }
     }

If this is currently not possible is it worth thinking about opening a feature request? Otherwise, the FormInterface::addError() function would be obsolete within LiveComponents.

WebMamba

WebMamba commented on Jul 14, 2024

@WebMamba
Contributor

Ok now I got it now, thanks! You can't add Errors after your form have been submitted and validated. This not about LiveComponent, but more about how Symfony form works. So I think you have two options:

  • You can create a custom constraint that do the check with your third party, and then add this custom constraint to your form (https://symfony.com/doc/current/validation/custom_constraint.html)
  • You choose to validate your data after the form is submitted, but then you need to store the error message on a new propertie of your component and not on the form
Mauriceu

Mauriceu commented on Jul 15, 2024

@Mauriceu
Author

Yea I thought about these too, however they would still feel like a workaround, because even when adding the FormError to the form within the "instantiateForm()" function it is not rendered - and at this point the form is not submitted.

smnandre

smnandre commented on Jul 17, 2024

@smnandre
Member

Only errors related to validated field / models are kept (see: https://github.com/symfony/ux-live-component/blob/2.x/src/ComponentWithFormTrait.php), and the live props are the form values, not the form itself.

Not sure at all, but maybe you can call resetForm and then add some error ?

palgalik

palgalik commented on Dec 11, 2024

@palgalik

I’m struggling with the same issue.

@smnandre
If you were to call the resetForm function, it would clear the already entered input data, which isn’t an option in my case.

@WebMamba
Perhaps you’re not entirely right about this, let me show it.
Here’s a very simple example that shows how to add errors to an input field even after Form validation, beyond the constraints already defined on the field.

TestController.php

<?php

declare(strict_types=1);

namespace App\Security\Infrastructure\Ui\Http\Registration;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route(
    path: '/test',
    methods: [
        Request::METHOD_GET,
        Request::METHOD_POST,
    ]
)]
class TestController extends AbstractController
{
    public function __invoke(Request $request): Response
    {
        $form = $this->createFormBuilder()
            ->add('name', TextType::class)
            ->add('submit', SubmitType::class)
            ->getForm();

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $form->get('name')->addError(new FormError('some custom error'));
        }

        return $this->render(
            view: 'test.html.twig',
            parameters: [
                'form' => $form->createView(),
            ],
        );
    }
}

test.html.twig

{% block content %}
    {{ form_start(form) }}
    {{ form_end(form) }}
{% endblock %}
Screenshot 2024-12-11 at 22 07 09

It’s clear that the name field has no validation applied; instead, a FormError is manually added to the field, and the error message appears below the input field.

smnandre

smnandre commented on Dec 16, 2024

@smnandre
Member
Mauriceu

Mauriceu commented on Feb 24, 2025

@Mauriceu
Author

@smnandre The ValidatableComponentTrait looks like it can only be used for validating constraints on demand which is nice to have, however the errors I would like to display are somewhat independent of the underlying model and its constraints. While that may sound like a case of "well then display the errors independently of the form", they still kind of belong within the form context and being able to utilize the form_errors(form) twig function would prevent implementation of redundant code.

Maybe this example showcases the problem a bit better:

     #[LiveAction]
     public function save(): ?Response
     {
          try {
               /// ... send data to third party server that may raise its own validation error
          } catch (ThirdPartyServerValidationError $e) {
               $error = new FormError('An external validation error occurred');
               $this->getForm()->addError($error);
               throw UnprocessableEntityHttpException();
          }
     }

Is there a reason why only FormErrors corresponding to a validatedField entry are displayed? Is it possible to make the validatedFields be co-dependent on existing FormErrors on Form instantiation/rendering?

smnandre

smnandre commented on Feb 24, 2025

@smnandre
Member

This is why i suggested some diving deep in the Trait if you want to make it work as you wish ... and maybe why not open a PR afterward ?

Mauriceu

Mauriceu commented on Feb 25, 2025

@Mauriceu
Author

This is why i suggested some diving deep in the Trait if you want to make it work as you wish ... and maybe why not open a PR afterward ?

Behold! My first pull request ...

#2602

dubbs

dubbs commented on May 1, 2025

@dubbs

I managed to achieve the same effect by nulling out the formView before throwing an exception:

$this->submitForm();

$this->getForm()->addError(
    new FormError('The error.')
);

$this->formView = null;
        
throw new UnprocessableEntityHttpException();

I don't know if this would have any unintended side effects though.

I tested on a basic form, form state is maintained and the errors show up.

Mauriceu

Mauriceu commented on May 9, 2025

@Mauriceu
Author

@dubbs looks like a valid workaround. Weirdly enough, $this->submitForm() must be called beforehand, otherwise the Error will not be rendered.

Thanks man, that really helps me out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @dubbs@smnandre@carsonbot@palgalik@WebMamba

      Issue actions

        [LiveComponent] How do I manually add a FormError to the Form when my submit-function fails? · Issue #1981 · symfony/ux