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

Fix morphing root level state #4169

Merged
merged 4 commits into from
Apr 22, 2024
Merged

Conversation

calebporzio
Copy link
Collaborator

The Problem

The following HTML, when morphed to itself (like in the case of a Livewire component refreshing), will throw an error:

<div>
    <template x-teleport="body">
        <div x-data="{ foo: false }">
            <div x-text="'foo value: ' + foo"></div>
        </div>
    </template>
</div>

The error: Uncaught ReferenceError: foo is not defined

Interestingly, the problem does NOT occur when wrapping the x-data element in a plain <div> like so:

<div>
    <template x-teleport="body">
        <div>
            <div x-data="{ foo: false }">
                <div x-text="'foo value: ' + foo"></div>
            </div>
        </div>
    </template>
</div>

The explanation

Clearly during the morph/clone phase, the live tree's x-data data is not being brought over to the reference <div> during clone.

Alpine's morph algorithm will walk both the live and reference tree, element by element. As it walks, it will "clone" (initialize) the reference element based on information from the live tree (such as existing x-data state), then compare the two, and make any "mutations" to the live tree it needs.

So, in a normal, non-teleport scenario, when <div x-data> is encountered in the tree walk, Alpine will recognize live "state" in the live tree and graft it onto the reference tree before initializing the reference element (cloning).

However, for some reason Alpine isn't carrying over the "live" state from the live tree to the reference tree when x-data is the first child of x-teleport.

Here's the reason this is happening:

As Alpine walks both trees during morph, when it encounters an element using x-teleport, it will internally do it's own "teleport" in the DOM from the <template> element to the live, teleported element.

To understand the problem, you have to understand two key processes in "morph": patch and patchChildren

patch(from, to) operation

  • Compare nodes for "swapping" (early return if so)
  • Clone "to" (reference) node based on "from" (live) node
  • Run patchChildren(from, to) (recursively patch each child)

patchChildren(from, to) operation

  • Check the "from" and "to" for "teleports" and gather an array of the teleported element children, instead of the <template> tag's children.
  • Go through the two arrays of children, compare and patch(), remove, and add elements

As you can see from above, the combination of patch() and patchChildren, is how morph crawls a both DOM trees in parallel.

The problem is that the teleported elements are substituted until AFTER the <template> elements are cloned. This means that the <div x-data> elements never have an opportunity to be "cloned", and the state is never grafted over, resulting in state-based scope errors downstream.

The solution

Solution A: Re-clone template targets

We could call "clone" once more inside patchChilren() when the "from" and "to" are about to be substituted with their actual teleported elements.

Pros:

  • Simple
  • Non-invasive

Cons:

  • Not "pure" or "deep"
    • Like if the root element changes it won't be "swapped" and the lifecycle hook won't run

Solution B: Re-run patch() on targets

From inside patchChildren(), we could pass the template target elements back through patch() and return early.

Pros:

  • Comprehensive, deep

Cons:

  • Potentially too invasive?
  • Might cause other unintended issues

Solution C: Swap teleport targets earlier

We could also move the conditional for swapping teleport targets earlier in the process (out of patch() and into patchChildren).

Pros

  • Comprehensive, deep

Cons

  • We then need to decide which operations apply to the <template> element and which apply to the <div> elements (swapping, cloning, lifecycle hooks, etc...)
  • We also need both <template> AND <div>to be cloned so there would need to be duplication somewhere

Solution D: Just graft state

Instead of full on "patching" the teleport targets, we could just move state if it exists over to the "to" tree.

Pros:

  • Super simple

Cons:

  • "diffing" wouldn't work on the root element of a template teleport element

Based on the above, I think solution "B" is worth trying, as it's the most comprehensive. It is also the scariest to me (risk of causing other complicated bugs), but we should at least try it.

@calebporzio calebporzio marked this pull request as ready for review April 22, 2024 20:55
@calebporzio calebporzio merged commit 54c7eb7 into main Apr 22, 2024
2 checks passed
@joshhanley joshhanley deleted the fix-morphing-root-level-state branch April 24, 2024 10:04
@MrPunyapal
Copy link

is it the reason for this bug?
see the bug

3.4.10: https://wirebox.app/b/xeo72

3.4.11: https://wirebox.app/b/430dl

open the modal and click on Add random
both codes are exactly the same!

@ekwoka
Copy link
Contributor

ekwoka commented May 7, 2024

Well, the second one there is using a much older version of Alpine.

@MrPunyapal
Copy link

okay, not working in 3.4.12 too
https://wirebox.app/b/4qpw2

@SimoTod
Copy link
Collaborator

SimoTod commented May 7, 2024

@MrPunyapal if it's broken in 3.4.11 (which uses Alpine 3.13.8), is not caused by this Pull request as it was only released in Alpine 3.4.10. Perhaps you can start a different discussion (maybe try the Livewire repo first).

P.s. Your last link is private

@MrPunyapal
Copy link

MrPunyapal commented May 7, 2024

@MrPunyapal if it's broken in 3.4.11 (which uses Alpine 3.13.8), is not caused by this Pull request as it was only released in Alpine 3.4.10. Perhaps you can start a different discussion (maybe try the Livewire repo first).

livewire/livewire#8282 (comment)

P.s. Your last link is private

thanks updated

@SimoTod
Copy link
Collaborator

SimoTod commented May 7, 2024

@MrPunyapal if it's broken in 3.4.11 (which uses Alpine 3.13.8), is not caused by this Pull request as it was only released in Alpine 3.4.10. Perhaps you can start a different discussion (maybe try the Livewire repo first).

livewire/livewire#8282 (comment)

P.s. Your last link is private

thanks updated

Yeah, but the example you posted above uses 3.13.8 so that comment is not accurate either. Either ways, this MR is not the root cause because it was merged in a later release so it's better to open a different discussion. it will be something released between 3.13.0 working and 3.13.8 (broken).

@MrPunyapal
Copy link

Solved it by tweaking some code at all the teleports

@joshhanley
Copy link
Collaborator

joshhanley commented May 17, 2024

@ekwoka and @SimoTod FYI he is talking about Livewire version numbers, not Alpine version numbers.

There is a similar problem reported on Livewire that morphing is now copying across x-cloak livewire/livewire#8439

@ekwoka
Copy link
Contributor

ekwoka commented May 17, 2024

Yes, I did gather that. I looked at the Alpine versions used in the presented sandboxes.

@bernig
Copy link

bernig commented May 28, 2024

is it the reason for this bug? see the bug

3.4.10: https://wirebox.app/b/xeo72

3.4.11: https://wirebox.app/b/430dl

open the modal and click on Add random both codes are exactly the same!

I encountered the exact same problem (on all versions of Livewire after 3.4.10). Adding a wrapper <div> to the content being teleported, as suggested, fixes the issue for me.

@MrPunyapal
Copy link

I encountered the exact same problem (on all versions of Livewire after 3.4.10). Adding a wrapper <div> to the content being teleported, as suggested, fixes the issue for me.

Thanks 👍

I solved it by adding wire.ignore.self on parent div.

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

Successfully merging this pull request may close these issues.

6 participants