Skip to content

Commit 3b10447

Browse files
calebporzioclaude
andcommitted
Fix x-ref crash during child-element morph
Wrap x-ref's inline handler with skipDuringClone() to prevent TypeError when morph runs on a child element (not the x-data root). During morph, cloneNode() runs directives on detached elements where closestRoot() returns undefined. Refs re-register on live DOM via MutationObserver. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 24f401a commit 3b10447

File tree

2 files changed

+40
-3
lines changed

2 files changed

+40
-3
lines changed
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { closestRoot } from '../lifecycle'
2+
import { skipDuringClone } from '../clone'
23
import { directive } from '../directives'
34

45
function handler () {}
56

6-
handler.inline = (el, { expression }, { cleanup }) => {
7+
// Skip during clone because morph runs directives on detached elements
8+
// where closestRoot() has no x-data ancestor. Refs re-register on the
9+
// live DOM via the MutationObserver path after morph patches attributes.
10+
handler.inline = skipDuringClone((el, { expression }, { cleanup }) => {
711
let root = closestRoot(el)
812

913
if (! root._x_refs) root._x_refs = {}
1014

1115
root._x_refs[expression] = el
1216

1317
cleanup(() => delete root._x_refs[expression])
14-
}
18+
})
1519

1620
directive('ref', handler)

tests/cypress/integration/plugins/morph.spec.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { haveAttribute, haveLength, haveText, haveValue, haveHtml, html, test } from '../../utils'
1+
import { haveAttribute, haveFocus, haveLength, haveText, haveValue, haveHtml, html, test } from '../../utils'
22

33
test('can morph components and preserve Alpine state',
44
[html`
@@ -1099,3 +1099,36 @@ test('morph properly closes dialog opened with showModal()',
10991099
get('#outside').click()
11001100
},
11011101
)
1102+
1103+
test('can morph child element containing x-ref without crashing',
1104+
[html`
1105+
<div x-data="{ value: 'initial' }">
1106+
<section>
1107+
<input x-ref="myInput" x-model="value">
1108+
<button @click="$refs.myInput.focus()">Focus</button>
1109+
<span x-text="value"></span>
1110+
</section>
1111+
</div>
1112+
`],
1113+
({ get }, reload, window, document) => {
1114+
get('span').should(haveText('initial'))
1115+
1116+
// Morph a CHILD element (section), not the x-data root
1117+
get('section').then(([el]) => {
1118+
window.Alpine.morph(el, `
1119+
<section>
1120+
<input x-ref="myInput" x-model="value">
1121+
<button @click="$refs.myInput.focus()">Focus</button>
1122+
<span x-text="value"></span>
1123+
</section>
1124+
`)
1125+
})
1126+
1127+
// State should be preserved
1128+
get('span').should(haveText('initial'))
1129+
1130+
// Verify $refs still resolves after morph
1131+
get('button').click()
1132+
get('input').should(haveFocus())
1133+
},
1134+
)

0 commit comments

Comments
 (0)