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

Make morphdom faster and more powerful #3692

Merged
merged 2 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 10 additions & 33 deletions morph.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,21 @@

<div id="before">
<!-- Before markup goes here: -->
<div>
<div>
</div>

<button type="button" wire:click="$refresh" dusk="refresh">
Refresh
</button>

<div dusk="child" key="foo">
<input type="text">
Child
</div>

<div>
</div>
</div>
<ul>
<li data-key="1">foo<input></li>
</ul>
</div>

<div id="after" style="display: none;">
<!-- After markup goes here: -->
<div>
<div>
</div>

<button type="button" wire:click="$refresh" dusk="refresh">
Refresh
</button>

<div dusk="child" key="foo">
<input type="text">
Child
</div>

<div>
</div>
<ul>
<li data-key="2">bar<input></li>
<li data-key="3">baz<input></li>
<li data-key="1">foo<input></li>
</ul>
</div>

</div>
<div id="b"></div>

<div style="display: flex;">
<pre id="log-from"></pre>
Expand All @@ -69,7 +46,7 @@
Alpine.morph(
document.querySelector('#before').firstElementChild,
document.querySelector('#after').firstElementChild.outerHTML,
{ debug: true }
{ debug: true, key(el) { return el.dataset.key } }
)
}

Expand Down
5 changes: 3 additions & 2 deletions packages/alpinejs/src/alpine.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMut
import { mergeProxies, closestDataStack, addScopeToNode, scope as $data } from './scope'
import { setEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions } from './evaluator'
import { transition } from './directives/x-transition'
import { clone, skipDuringClone, onlyDuringClone } from './clone'
import { clone, cloneNode, skipDuringClone, onlyDuringClone } from './clone'
import { interceptor } from './interceptor'
import { getBinding as bound, extractProp } from './utils/bind'
import { debounce } from './utils/debounce'
Expand Down Expand Up @@ -68,7 +68,8 @@ let Alpine = {
magic,
store,
start,
clone,
clone, // INTERNAL
cloneNode, // INTERNAL
bound,
$data,
walk,
Expand Down
47 changes: 45 additions & 2 deletions packages/alpinejs/src/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,53 @@ export function onlyDuringClone(callback) {
return (...args) => isCloning && callback(...args)
}

export function interuptCrawl(callback) {
return (...args) => isCloning || callback(...args)
export function cloneNode(from, to)
{
// Transfer over existing runtime Alpine state from
// the existing dom tree over to the new one...
if (from._x_dataStack) {
to._x_dataStack = from._x_dataStack

// Set a flag to signify the new tree is using
// pre-seeded state (used so x-data knows when
// and when not to initialize state)...
to.setAttribute('data-has-alpine-state', true)
}

isCloning = true

// We don't need reactive effects in the new tree.
// Cloning is just used to seed new server HTML with
// Alpine before "morphing" it onto live Alpine...
dontRegisterReactiveSideEffects(() => {
initTree(to, (el, callback) => {
// We're hijacking the "walker" so that we
// only initialize the element we're cloning...
callback(el, () => {})
})
})

isCloning = false
}

let isCloningLegacy = false

/** deprecated */
export function clone(oldEl, newEl) {
if (! newEl._x_dataStack) newEl._x_dataStack = oldEl._x_dataStack

isCloning = true
isCloningLegacy = true

dontRegisterReactiveSideEffects(() => {
cloneTree(newEl)
})

isCloning = false
isCloningLegacy = false
}

/** deprecated */
export function cloneTree(el) {
let hasRunThroughFirstEl = false

Expand Down Expand Up @@ -59,3 +90,15 @@ function dontRegisterReactiveSideEffects(callback) {

overrideEffect(cache)
}

// If we are cloning a tree, we only want to evaluate x-data if another
// x-data context DOESN'T exist on the component.
// The reason a data context WOULD exist is that we graft root x-data state over
// from the live tree before hydrating the clone tree.
export function shouldSkipRegisteringDataDuringClone(el) {
if (! isCloning) return false
if (isCloningLegacy) return true

return el.hasAttribute('data-has-alpine-state')
}

8 changes: 2 additions & 6 deletions packages/alpinejs/src/directives/x-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { directive, prefix } from '../directives'
import { initInterceptors } from '../interceptor'
import { injectDataProviders } from '../datas'
import { addRootSelector } from '../lifecycle'
import { isCloning } from '../clone'
import { shouldSkipRegisteringDataDuringClone } from '../clone'
import { addScopeToNode } from '../scope'
import { injectMagics, magic } from '../magics'
import { reactive } from '../reactivity'
Expand All @@ -11,11 +11,7 @@ import { evaluate } from '../evaluator'
addRootSelector(() => `[${prefix('data')}]`)

directive('data', ((el, { expression }, { cleanup }) => {
// If we are cloning a tree, we only want to evaluate x-data if another
// x-data context DOESN'T exist on the component.
// The reason a data context WOULD exist is that we graft root x-data state over
// from the live tree before hydrating the clone tree.
if (isCloning && el._x_dataStack) return;
if (shouldSkipRegisteringDataDuringClone(el)) return

expression = expression === '' ? '{}' : expression

Expand Down
85 changes: 0 additions & 85 deletions packages/morph/src/dom.js

This file was deleted.

Loading