symfony / ux

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

[LiveComponent] Using a DTO for a search form #1850

Open YummYume opened 6 months ago

YummYume commented 6 months ago

Hello !

This issue is more like a question that I feel like is not mentioned anywhere in the docs. Traditionally, when making a search form with filters (for example, let's say a global search), I would use a DTO class specially made for that, with each property being an input to the user.

When using a live component, I struggle to find the "right" way to make my component work with a form + a DTO. In the docs, the very first example is a search, but it's using properties directly in the component. Later on, form examples only use entities and show how the submit works with actions, but not how it could work with a simple DTO. The reason I want to use a form in this case is mostly for the ease of use, the already existing form themes, and it would even allow older apps with old searches to be upgraded very easily, by keeping the same DTO.

There are many ways this could be done, for example listen to input events and call an action every time. But I'd like to have it easy, and just have my component automatically re-render when the user types something, or changes a field, and have my DTO update depending on my form. The easiest solution I found was to add 'data-model': 'on(input)|*' to my form, but this causes issues with my DTO in many cases. Here's an example :

class GlobalSearchDTO
{
    public ?string $query = null;
}
#[AsLiveComponent]
class GlobalSearch extends AbstractController
{
    use ComponentWithFormTrait;
    use DefaultActionTrait;

    #[LiveProp]
    public GlobalSearchDTO $globalSearchDTO;

    public function __construct( ) {
        // This is the only way I found to make this "work".
        // Not instantiating the $globalSearchDTO causes an error, obviously
        // Setting it to "null" by default causes the value to never change
        $this->globalSearchDTO = new GlobalSearchDTO();
    }

    public function getResults(): array
    {
        $query = $this->globalSearchDTO->query;

       // Get the results...

        return $results;
    }

    protected function instantiateForm(): FormInterface
    {
        return $this->createForm(GlobalSearchType::class, $this->globalSearchDTO);
    }
}
<div {{ attributes }}>
    <div class="flex gap-2.5 items-center">
        {{ form_start(form, {attr: {
            class: 'grow',
            role: 'search',
            'data-model': 'on(input)|*'
        }}) }}
            {# The form... #}
        {{ form_end(form) }}
    </div>

    {# ... #}
</div>

The form itself is a simple form with a simple SearchType for the query.

I'm fairly certain that what I'm doing is a hacky way to get things done, which I don't like. I would love to hear opinions on this and settle on the right approach to get a simple search working with just a DTO and a form.

WebMamba commented 6 months ago

Yes this is the right way do it. What in your code feel hacky to you?

smnandre commented 6 months ago

The form itself is a simple form with a simple SearchType for the query.

In this case you should do without a form.

This is the simplest and the best demo to showcase LiveComponent: a live search, no form, only one class: https://ux.symfony.com/live-component

YummYume commented 6 months ago

@WebMamba What feels pretty hacky to me is the fact that I'm mixing a form with a DTO, but without actually ever submitting the form, just using the data-model to refresh the component on any input.

@smnandre That was more of an example for a simple case, but I do have much more complicated DTOs with many filters, many of which would be a pain to make work without a form (themes, data transformers, options and so on...)

YummYume commented 6 months ago

Another issue I found with this approach is validations. I'm not sure how to know if my form is valid or not in my getResults method. I can't submit the form there because it's not a live action.

I could check if it's valid manually by calling the validator service, but another issue would be that the results would always be empty then if the form is invalid, instead of keeping the latest valid results. A solution would be to store the results as a LiveProp too ? It would be great if I could just get the errors from the form.

carsonbot commented 4 days ago

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

Nayte91 commented 4 days ago

Hello @YummYume , Just wanted you to know that I opened a VERY similar issue here (https://github.com/symfony/ux/issues/2142) where I discussed with team, so you may have more hints about which direction to take together!