alpinejs / alpine

A rugged, minimal framework for composing JavaScript behavior in your markup.
https://alpinejs.dev
MIT License
27.92k stars 1.22k forks source link

Fix morphing root level state #4169

Closed calebporzio closed 4 months ago

calebporzio commented 4 months ago

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

patchChildren(from, to) operation

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:

Cons:

Solution B: Re-run patch() on targets

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

Pros:

Cons:

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

Cons

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:

Cons:


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.

MrPunyapal commented 4 months ago

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 commented 4 months ago

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

MrPunyapal commented 4 months ago

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

SimoTod commented 4 months ago

@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 commented 4 months ago

@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).

https://github.com/livewire/livewire/pull/8282#issuecomment-2080951339

P.s. Your last link is private

thanks updated

SimoTod commented 4 months ago

@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 commented 4 months ago

Solved it by tweaking some code at all the teleports

joshhanley commented 3 months ago

@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 https://github.com/livewire/livewire/discussions/8439

ekwoka commented 3 months ago

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

bernig commented 3 months ago

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 commented 3 months ago

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.