Skip to content

[LiveComponent] Include prop name in the url modifier function #2650

Open
@jannes-io

Description

@jannes-io

Problem

The current (lack of) DX is best visible when defining abstract re-usable live components.

Let's have a minimal example, using something like a simple list with pagination:

// parent
abstract class AbstractList
{
    use DefaultActionTrait;

    #[LiveProp(writable: true, url: true)]
    public int $page = 1;

    abstract public function getItems(): array;
}

// child 1
#[AsLiveComponent(name: 'FirstCoolList')]
class FirstCoolList extends AbstractList
{
    public function getItems(): array
    {
        // get items from database using $this->page to set offset/limit yadda yadda, not the point of this issue.
    }
}

// child 2
#[AsLiveComponent(name: 'SecondCoolList')]
class SecondCoolList extends AbstractList
{
    public function getItems(): array
    {
        // get items from database using $this->page to set offset/limit yadda yadda, not the point of this issue.
    }
}

Now when we render both components, we can see that the page param will be in conflict.. yikes... not good..

{{ component('FirstCoolList') }}
{{ component('SecondCoolList') }}

So.. since the param is declared in the parent, we can't just change the url anymore, we have to use a modifier. We can follow the LiveComponent documentation and it'll allow us to change the URL parameters based a parameter. Ok cool let's add that.

abstract class AbstractList
{
    // ..

    #[LiveProp(writable: true, url: true, modifier: 'modifyPageQuery')]
    public int $page = 1;

    #[LiveProp]
    public string $pageParamAlias = 'page';

    // ..

    public function modifyPageQuery(LiveProp $prop): LiveProp
    {
        return $prop->withUrl(new UrlMapping($this->pageParamAlias));
    }
}

And we use it:

{{ component('FirstCoolList', { pageParamAlias: 'firstListPage' }) }}
{{ component('SecondCoolList', { pageParamAlias: 'secondListPage' }) }}

Now we modify our abstract class, because we don't just need to know the page, we also want to capture some search query the user has entered.

abstract class AbstractList
{
    // ..

    #[LiveProp(writable: true, url: true, modifier: 'modifySearchQuery')]
    public string $query = 1;

    #[LiveProp]
    public string $searchParamAlias = 'q';

    #[LiveProp(writable: true, url: true, modifier: 'modifyPageQuery')]
    public int $page = 1;

    #[LiveProp]
    public string $pageParamAlias = 'page';

    // ..

    public function modifySearchQuery(LiveProp $prop): LiveProp
    {
        return $prop->withUrl(new UrlMapping($this->searchParamAlias));
    }

    public function modifyPageQuery(LiveProp $prop): LiveProp
    {
        return $prop->withUrl(new UrlMapping($this->pageParamAlias));
    }
}

To make sure our lists don't collide, we again need to modify both usages as well:

{{ component('FirstCoolList', { pageParamAlias: 'firstListPage', searchParamAlias: 'firstListSearch' }) }}
{{ component('SecondCoolList', { pageParamAlias: 'secondListPage', searchParamAlias: 'secondListSearch' }) }}

Rinse and repeat for 4 or 5 other params, and you can see how the abstract class is starting to get out of hand, and in terms of maintainability, if the usage of the components is spread out over multiple Symfony bundles, hundreds of templates,... this becomes absolutely impossible to maintain.

Proposal

Pass the name of the property down into the modify function, either in the LiveProp object, or as a second parameter. This way we can configure 1 prefix on the component that can be applied to all props.

abstract class AbstractList
{
    // ..

    #[LiveProp(writable: true, url: true, modifier: 'addPrefix')]
    public int $page = 1;

    #[LiveProp]
    public string $queryPrefix= '';

    // ..

   // PROPOSAL 1:
    public function addPrefix(LiveProp $prop): LiveProp
    {
        $param = $this->queryPrefix . $prop->getName(); // "page"
        return $prop->withUrl(new UrlMapping($param));
    }

    // PROPOSAL 2:
    public function addPrefix(LiveProp $prop, string $propName): LiveProp
    {
        $param = $this->queryPrefix . $propName; // "page"
        return $prop->withUrl(new UrlMapping($param));
    }
}

Now we no longer need to modify usages of the component whenever we add new properties that are controllable by the URL.
Both proposals would be completely backwards compatible, since we're just adding another param or new field on LiveProp, we're not changing the behavior of the modifier.

It's unlikely that multiple lists of the same component are shown on the same page meanwhile it's very possible that multiple lists of different components are shown on 1 page so in my case I would actually set the prefix as a protected field on the implementation of the list, but I didn't want to use this use-case in the example since it's very application dependent.

What are your thoughts?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions