symfony / ux

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

[LiveComponent] [Turbo] "Error: Form responses must redirect to another location" when using a search form inside a Live Component #2051

Closed matthieumastadenis closed 1 month ago

matthieumastadenis commented 1 month ago

Hello again,

Following my previous issue #2048, I have another one that's directly related to the same code.

I work with a live component containing two things : a table of users and a search form allowing to filter the users displayed in the table.

When I simply type into the form fields, my component is correctly refreshed, and after I solved my previous issue everything is fine (meaning the table correctly re-renders with a new list of users corresponding to my search).

But when I try to use the submit button, I get a JS error : Error: Form responses must redirect to another location.

I actually already encountered this error previously, when I tried the same things but with a regular symfony controller instead of a live component. Since my form is a search form meant to refine results on the same page, performing a redirect is not the correct solution here. So I ended up returning a 202 HTTP status code instead of 200 when my form was submitted. Despite this being an ugly fix rather than an elegant solution, it worked.

But now I'm not in a regular controller. I'm trying to implement the same thing in a Live Component instead. And so I don't know how to solve my JS error because:

As a last resort I know I could just remove the submit button, because everything works fine by just typing into the form fields, without clicking on that button. But I still wonder how to make this work properly with the button? Did I do something wrong?

Thanks,


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

    #[LiveProp]
    public array $initialFormData = [];

    #[LiveProp]
    public array $users = [];

    public function mount(): void
    {
        $this->users = $this->fetchUsers($this->initialFormData);
    }

    public function __invoke(): void
    {
        $this->search();
    }

    #[LiveAction]
    public function search(): void
    {
        $this->submitForm();
        $this->users = $this->fetchUsers($this->getForm()->getData());
    }

    protected function fetchUsers(array $searchData = []): array
    {
        // fetching users from database using $searchData to filter results...
        return $users;
    }

    protected function instantiateForm(): FormInterface
    {
        return $this->createForm(
            type : AdminUsersSearchType::class,
            data : $this->initialFormData,
        );
    }
}
<div has-datatable {{ attributes }}>
    <twig:UI:Section :full="false">
        <twig:UI:Title:Section icon="fa6-solid:magnifying-glass">Rechercher</twig:UI:Title:Section>

        {{ form_start(form, {
            method: 'POST',
            attr: {
                'data-model': 'debounce(300)|*',
                'data-action': 'live#action',
                'data-live-action-param': 'search',
            },
        }) }}
        <div class="form_grid form_grid-2_columns">
            {% for child in form %}
                {% if child.vars.name != '_token' %}
                    <div class="form_label">
                        {{ form_label(child) }}
                    </div>
                    <div class="form_field">
                        {{ form_widget(child) }}
                    </div>
                {% endif %}
            {% endfor %}
            <div class="form_row form_row-buttons padding-top_s">
                {% block buttons %}
                    <input type="submit" value="Rechercher">
                {% endblock %}
            </div>
        </div>
        {{ form_rest(form) }}
        {{ form_end(form) }}
    </twig:UI:Section>

    <twig:UI:Section data-live-id="{{ microtime() }}">
        {# The title below is correctly updated each time the Live Component re-renders 
             (so I see the correct number of results according to what I typed in the search form above) #}
        <twig:UI:Title:Section>
            {{ users|length~(users|length > 1 ? ' Résultats' : ' Résultat') }}
        </twig:UI:Title:Section>

        {# The markup of the stimulus controller below is correctly updated each time the Live Component re-renders, 
             but the controller is actually never re-rendered (and so the generated table never changes) #}
        <div id="users_table"
            data-controller="UI--datatable"
            data-UI--datatable-data-value='{{ users|json_encode }}'
            data-UI--datatable-columns-value='{{ [
                    {
                        title: 'E-mail',
                        name: 'email',
                        width: 250,
                    },
                    {
                        title: 'Identifiant',
                        name: 'username',
                        width: 250,
                    },
                ]|json_encode }}'
            data-live-id="{{ microtime() }}"
            class="datatable"
        >
            <div data-UI--datatable-target="table" class="datatable_table"></div>
        </div>
    </twig:UI:Section>
</div>
smnandre commented 1 month ago

Turbo does require a redirect for form responses.. you can add the :prevent modifier to your action to let Stimulus/LiveComponent handle the submission (instead of Turbo)

https://stimulus.hotwired.dev/reference/actions

matthieumastadenis commented 1 month ago

Turbo does require a redirect for form responses.. you can add the :prevent modifier to your action to let Stimulus/LiveComponent handle the submission (instead of Turbo)

https://stimulus.hotwired.dev/reference/actions

Thanks, it works :+1: