Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LiveComponent] - Make DeterministicTwigIdCalculator Extendable/Overridable #2573

Open
Pechynho opened this issue Feb 15, 2025 · 4 comments
Open

Comments

@Pechynho
Copy link

Pechynho commented Feb 15, 2025

Hi,

Could you please make DeterministicTwigIdCalculator extendable or overridable from the application?

I recently encountered an issue that took me a while to diagnose. I implemented a scrolling feature based on this tutorial.

Everything worked fine in an older version, but after an update, it stopped working. Instead of appending new items to the component, each new "page" replaced the previous content.

After investigating, I discovered that the issue was caused by Morphdom’s implementation and the deterministic ID calculator. It seems that Morphdom builds an element ID map based on all IDs in the DOM tree. Since I didn't provide a custom ID for one of my deeply nested Live Components, the deterministic calculator assigned it the same ID every time (every "page" has contained same set of ids). This caused Morphdom to behave incorrectly. After explicitly providing an ID for that Live Component, everything started working again.

This is roughly how my pagination code looked like.

{% for item in items %}
    <div id="my_item_{{ item.id }}" data-live-ignore>
        ... very complicated dom and nested Twig / Live Components
        <twig:AnotherLiveComponent />
        ... very complicated dom and nested Twig / Live Components
    </div>
{% endfor %}

My proposal is to make the service responsible for providing default IDs to components overridable (e.g., by implementing an interface). This would allow me to do something like this:

class IdProvider implements ComponentIdProvider
{
    public function getId(): string
    {
        throw new \RuntimeException('Provide id to Live Component manually!');
    }
}

This approach would enforce the manual definition of element IDs instead of relying on the deterministic calculator, which can cause issues with the "infinite scroll hack" solution.

@smnandre
Copy link
Member

Everything worked fine in an older version, but after an update, it stopped working.

I'd be curious to know which one (the older and the new one)

Since I didn't provide a custom ID for one of my deeply nested Live Components, the deterministic calculator assigned it the same ID every time (every "page" has contained same set of ids).

"one" nested ? Because ..

every "page" has contained same set of ids

Looks you have a lot of ids here, right ?

This caused Morphdom to behave incorrectly.

Very logical indeed

After, you may have a problem somewhere because if you pass an "id" to a child component, it will be used as id. The determenistic value is not used in your component HTML then. So with no impact on the Morph.

@Pechynho
Copy link
Author

I will try to provide some more context.

I have implemented infinite scrolling like described here https://ux.symfony.com/demos/live-component/infinite-scroll

LiveListComponent

{% for item in items %}
    <twig:LiveComponentA
        id="live_component_a_{{ item.id }}"
        data-live-ignore
    />
{% endfor %}

LiveComponentA - Twig part

<div {{ attributes }}>
    {# a lot of html #}
    {# actually in my case this component was deeply nested in other Twig components #}
    <twig:LiveComponentB />
    {# a lot of html #}
</div>

Let's assume that first page of items have ids [1, 2, 3] and second one have ids [4, 5, 6]

So for first page of List component I have these element ids on page

  • live_component_a_1 -> live-4564564-0 (id of LiveComponentB)
  • live_component_a_2 -> live-4564564-1 (id of LiveComponentB)
  • live_component_a_3 -> live-4564564-3 (id of LiveComponentB)

So for second page of List component I have these element ids on page

  • live_component_a_4 -> live-4564564-0 (id of LiveComponentB)
  • live_component_a_5 -> live-4564564-1 (id of LiveComponentB)
  • live_component_a_6 -> live-4564564-3 (id of LiveComponentB)

As you can see, the deterministic calculator is providing same ids for first and second page. It cannot be solved with key prop in LiveListComponent.

  1. solution is to pass some page (and filter) hash from LiveListComponent to LiveComponentA and from here I would need it to pass to every Live Component in node tree and use it generate id. That was not an option for me, because I would have to remember to do this every time I update LiveComponentA, LiveComponentB etc.
  2. solution was to enforce me to provide id to every LiveComponent so I don't run to a problem with pagination and Deterministic Id Calculator. I am using this solution for now.
readonly class LiveComponentIdEventListener
{
    #[AsEventListener(priority: 100)]
    public function onPostMountEvent(PostMountEvent $event): void
    {
        $metadata = $event->getMetadata();
        if ($metadata === null || $metadata->get('live', false) !== true) {
            return;
        }
        if (Strings::isNullOrWhiteSpace($data['id'] ?? null)) {
            throw new RuntimeException(
                sprintf(
                    'The Live Component "%s" (%s) must have and "id" attribute',
                    $metadata->getName(),
                    get_class($event->getComponent()),
                ),
            );
        }
    }
}
  1. solution which I've tried to implement but currently I do not have enough Twig knowledge to pull this. This should work that it would force me to setup my own Live Component ids but only on Components which were created in body of this tag - (in this example LiveComponentA and LiveComponentB
{% for item in items %}
    {% apply enforce_live_component_ids %}
        <twig:LiveComponentA
            id="live_component_a_{{ item.id }}"
            data-live-ignore
        />
    {% endapply %}
{% endfor %}
  1. solution would be that Live Component library would somehow pass key prop down the component tree so it's used in every Live Component child.
    LiveListComponent
{% for item in items %}
    <twig:LiveComponentA
        key="{{ item.id }}"
        data-live-ignore
    />
{% endfor %}

key prop from LiveComponentA would be used in Deterministic Id Calculator to calculate id for LiveComponentB here.

LiveComponentA - Twig part

<div {{ attributes }}>
    {# a lot of html #}
    <twig:LiveComponentB />
    {# a lot of html #}
</div>

But this solution cannot be done by hooks and it would need to be part of Live Component library (if it's possible to implement).

To answer your questions:

I'd be curious to know which one (the older and the new one)

I don't know. Maybe it was not caused by update but I've changed something in my Twig templates which caused Morphdom to act differently then before.

So my questions are:

  1. Can solution 3. or 4. be implemented and be part of the library? Is it even possible with current implementation of Twig / Live components?
  2. Why do we need to have LiveComponent to have id in first place? Is it really necessary? Could it be solved by using some other attribute (e.g. data-live-unique-id) - maybe this attr name could be fully configurable by config with default to "id"?
  3. Maybe you could write a small note to infinite scroll guide about this protentional problem. For me it was really hard to find why Morphdom is not appending elements to list but it is replacing whole list. It could save some time for others.

@smnandre
Copy link
Member

The DeterministicId is not made for this case, but to differenciate multiple component rendered at the same time, and clearly cannot be made external, as yourself would not have the content of the component when it rerenders

Half of the https://ux.symfony.com/demos/live-component/infinite-scroll-2 is about Morphing trick and the importance of choosing your ID manually.

If you do use manual ID, they are used instead of the generated live-id. So any automation you would need/want to have, you can, using functions, filters, tags, or any implementation.

I don't get: is there something preventing you to use the page as a prefix ?

@Pechynho
Copy link
Author

Pechynho commented Feb 22, 2025

I don't get: is there something preventing you to use the page as a prefix ?

As I said before. Nothing is preventing me from using page / id / whatever from as element id prefix. But when you are using morphing trick to paginate multiple-nested live components, YOU HAVE TO pass this prefix (or set your own id) to EVERY Live Component which will occur in paginated item HTML. And that is just not an option for me to do this, because you have to remember that this, this and this Live Component is used in this morphing trick and when you update them (and maybe adding new Live Component to one them) you can't forget to set its own id because than morphing trick can stop working. It's just so much easily error-prone.

So I am trying to come up with a solution which will alert me, if I add new Live Component to morphing pagination trick and forgot to set its unique id.

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

No branches or pull requests

3 participants