Skip to content

Fix x-model.parent when inside of x-teleport#4676

Merged
calebporzio merged 5 commits intoalpinejs:mainfrom
willrowe:hotfix/x-model-parent-in-teleport
Feb 9, 2026
Merged

Fix x-model.parent when inside of x-teleport#4676
calebporzio merged 5 commits intoalpinejs:mainfrom
willrowe:hotfix/x-model-parent-in-teleport

Conversation

@willrowe
Copy link
Contributor

This does two things:

  1. Fixes findClosest to include the teleport element (usually a template element) instead of skipping over it to its parent.
  2. Updates x-model.parent to use findClosest to determine the parent. This allows the current logic of traversing upwards through teleports to work correctly as well as automatically working with any future changes to traversal.

@willrowe willrowe changed the title Fix x-model.parent when insided of x-teleport Fix x-model.parent when inside of x-teleport Sep 15, 2025
calebporzio and others added 2 commits February 8, 2026 21:11
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fixed </h1> -> </h2> in test HTML (was opening h2, closing h1)
- Added missing newline at end of file

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@calebporzio
Copy link
Collaborator

PR Review: #4676 — Fix x-model.parent when inside of x-teleport

Type: Bug fix
Verdict: Merge

What's happening (plain English)

  1. When you use x-teleport, Alpine clones the template's content and moves it to a different place in the DOM (e.g. <body>). The original <template> element stays where it was, and the cloned element gets a _x_teleportBack pointer back to the template.
  2. x-model.parent resolves its scope by looking at el.parentNode — the DOM parent. This normally works fine.
  3. But when the element is teleported, el.parentNode is <body> (or wherever it was teleported to), not the logical parent component. So x-model.parent="value" throws "value is not defined" because <body> has no x-data scope.
  4. The fix changes x-model.parent to use findClosest(el, element => element !== el) instead of el.parentNode. findClosest already knows how to traverse teleport boundaries via _x_teleportBack — it walks up through the logical component tree, not the DOM tree.

Other approaches considered

  1. Special-case teleport in x-model.parent — could check el._x_teleportBack directly in x-model.js. Worse because it duplicates teleport traversal logic that findClosest already handles.
  2. Leave el.parentNode but patch it for teleported elements — e.g., override parentNode on teleported elements. Worse because monkey-patching DOM properties is fragile.
  3. Use findClosest (this PR's approach) — best approach. Reuses existing infrastructure, automatically handles future changes to traversal logic, and is a one-line change.

Changes Made

  • Resolved merge conflict in lifecycle.js: main added ShadowRoot support to findClosest after this PR was opened. I preserved both the PR's teleport fix and main's ShadowRoot handling.
  • Fixed mismatched HTML tag in test: <h2 x-text="value"></h1><h2 x-text="value"></h2>
  • Added missing newline at end of test file.

Test Results

Suite Tests Pass Fail
x-modelable.spec.js 6 6 0
x-model.spec.js 33 33 0
x-teleport.spec.js 11 11 0
CI - PASS -

Regression verified: stashed the fix, rebuilt, and confirmed tests 5 and 6 fail with "value is not defined" on main. Tests pass with the fix applied.

Code Review

lifecycle.js:69 — The key findClosest fix: when _x_teleportBack exists, recurse to the teleport source and return early. The old code did el = el._x_teleportBack then fell through to el.parentElement, which skipped the teleport template element itself for callback evaluation. The early return ensures the template element gets checked against the callback. Clean fix.

x-model.js:14findClosest(el, (element) => element !== el) is a clever way to say "find my logical parent." It starts at el, callback returns false (same element), then walks up through teleport boundaries or DOM, and returns the first different element. This is the right abstraction — it delegates all traversal knowledge to findClosest.

Test quality: Three good test cases covering (1) basic x-modelable inside teleport, (2) x-model.parent inside teleport, and (3) x-data directly on the template element. Tests 2 and 3 specifically target the bug. Test 1 already passed on main (since x-model without .parent uses closestRoot which already calls findClosest), but it's good documentation.

Style: No semicolons, uses let. Matches project conventions.

Security

No security concerns identified.

Verdict

Clean, surgical bug fix. The root cause is correctly identified (x-model.parent uses DOM parentNode which breaks for teleported elements), the fix is minimal (one import, one line change in x-model.js, one early-return in lifecycle.js), and it reuses existing infrastructure rather than introducing new concepts. Tests are solid and verified against regression. No regressions in related test suites. Merge it.


Reviewed by Claude

@calebporzio calebporzio merged commit 6dcca3c into alpinejs:main Feb 9, 2026
1 check passed
@willrowe willrowe deleted the hotfix/x-model-parent-in-teleport branch February 9, 2026 03:20
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.

2 participants