symfony / ux

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

[LiveComponent] [Autocomplete] Autocomplete with custom data endpoint not working in a live component #1879

Open momocode-de opened 4 months ago

momocode-de commented 4 months ago

I have a problem when I use an autocomplete field, which uses a custom endpoint, together with a live component. I have simplified my case and added the files below. The problem is that when I select something in the autocomplete field, the message “The selected choice is invalid.” appears:

image

If I then click on “Save”, a message appears that I should select an element from the list, even though I have selected one:

image

Versions:

This is my form type:

<?php

namespace App\Form;

use App\Struct\TestFormStruct;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class TestFormType extends AbstractType
{
    public function __construct(
        private readonly UrlGeneratorInterface $router,
    ) {
    }

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('testField', ChoiceType::class, [
            'placeholder' => 'Please select...',
            'autocomplete' => true,
            'autocomplete_url' => $this->router->generate('api_test'),
            'options_as_html' => true,
            'no_more_results_text' => '',
        ]);

        $builder->add('save', SubmitType::class);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => TestFormStruct::class,
        ]);
    }
}

This is my TestFormStruct:

<?php

namespace App\Struct;

class TestFormStruct
{
    private ?string $testField = null;

    public function getTestField(): ?string
    {
        return $this->testField;
    }

    public function setTestField(?string $testField): void
    {
        $this->testField = $testField;
    }
}

This is the autocomplete_url action:

#[Route('/test', name: 'api_test', methods: ['GET'])]
public function test(): JsonResponse
{
    return $this->json([
        'results' => [
            'options' => [
                [
                    'value' => '1',
                    'text' => 'Pizza',
                    'group_by' => 'food',
                ],
                [
                    'value' => '2',
                    'text' => 'Banana',
                    'group_by' => 'food',
                ],
            ],
            'optgroups' => [
                [
                    'value' => 'food',
                    'label' => 'food',
                ]
            ],
        ],
    ]);
}

This is my live component:

<?php

declare(strict_types=1);

namespace App\Twig\Components;

use App\Form\TestFormType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class TestForm extends AbstractController
{
    use ComponentWithFormTrait;
    use DefaultActionTrait;

    protected function instantiateForm(): FormInterface
    {
        return $this->createForm(TestFormType::class);
    }
}

And this is the template of my live component:

<div {{ attributes }}>
    {{ form_start(form) }}

    {{ form_row(form.testField) }}

    {{ form_end(form) }}
</div>

The component is added to a page with this: <twig:TestForm />

Does anyone have any idea what the problem is?

smnandre commented 4 months ago

You should start by setting a LiveProp with the data you want to use in your form (a TestFormStruct instance i guess): https://symfony.com/bundles/ux-live-component/current/index.html#forms

Then, some first questions to maybe give you some ideas / leads #1844

Can you look in the DOM & your browser inspector what is happening ? When is the error thrown ? What is the initial state ? Can you submit without changing the field ? Is your ajax endpoint called ? Does it work if you use a standard form and not a live one ? If so, does it work with a Autocomplete with hard-coded choices ?

momocode-de commented 4 months ago

You should start by setting a LiveProp with the data you want to use in your form (a TestFormStruct instance i guess): https://symfony.com/bundles/ux-live-component/current/index.html#forms

Ok, I thought that was optional. I've added it, but unfortunately it hasn't changed anything yet.

When is the error thrown ? What is the initial state ?

When I click in the selection field, a request is sent to the endpoint to load the options. Then the options are displayed and when I select an option, the live component is re-rendered. A request is then sent to the live component and this then fails with a 422 and returns the HTML, which then contains the message “The selected choice is invalid.”. This is the payload that is sent to the endpoint to re-render the live component after I select an option:

{"props":{"initialFormData":{"testField":null},"formName":"test_form","test_form":{"testField":"","save":null,"_token":"..."},"isValidated":false,"validatedFields":[],"@attributes":{"id":"live-3754781061-0"},"@checksum":"..."},"updated":{"test_form.testField":"1","validatedFields":["test_form.testField"]}}

Can you submit without changing the field ?

No, the browser stops me with the message from my second screenshot, because the field is required.

Is your ajax endpoint called ?

Yes, as soon as I click in the field

Does it work if you use a standard form and not a live one ?

Yes, this works, but I need a live form as I need dependent fields in my real case, like in this example.

If so, does it work with a Autocomplete with hard-coded choices ?

Yes, the live form with hardcoded choices works


I found out that I can work around the problem by setting 'required' => false in my field, adding the following line and then validating the field manually when submitting.

$builder->get('testField')->resetViewTransformers();

But I don't think that's a nice solution.


I think the problem is that the options loaded via Ajax are missing in the ChoiceList of the form field. A special “choice_loader” was used for the Entity Autocomplete, which I think must also be done for the Ajax Autocomplete. But I haven't come up with a solution yet.

smnandre commented 4 months ago

Maybe try this ? https://github.com/symfony/ux/issues/391#issuecomment-2139592202`

momocode-de commented 4 months ago

Maybe try this ? https://github.com/symfony/ux/issues/391#issuecomment-2139592202

No, unfortunately that doesn't help. In my case, it's not about an entity autocomplete. My custom Ajax endpoint, which loads the options, sends a request to an external API. So it would also be bad to have to call the external API again in a custom “choice_loader” (because of rate limits).

Or should I still do it with the 'required' => false and the resetViewTransformers to skip the standard validation and then do it myself when submitting the form? But it would still be nice if there was a clean standard solution for this case that was also documented. It's actually not an unusual case in my opinion.