Skip to content

feat: introduce <Teleport> component with zero-reset re-parenting#14

Open
ahmetbilgay wants to merge 5 commits intodashersw:mainfrom
ahmetbilgay:main
Open

feat: introduce <Teleport> component with zero-reset re-parenting#14
ahmetbilgay wants to merge 5 commits intodashersw:mainfrom
ahmetbilgay:main

Conversation

@ahmetbilgay
Copy link
Copy Markdown

@ahmetbilgay ahmetbilgay commented Mar 26, 2026

Add complete teleport functionality to GEA framework enabling content rendering at different DOM locations while preserving component state and event handling.

Key features:

  • JSX component syntax
  • Robust event delegation for teleported elements
  • Conditional teleporting via disabled prop
  • Automatic cleanup on component disposal
  • Zero breaking changes to existing APIs

Technical implementation:

  • Add teleport handling in component render lifecycle
  • Fix event registration timing in ComponentManager
  • Implement dual element marking for reliable event delegation
  • JSX transform integration for clean syntax
  • TypeScript definitions and proper exports

Bundle impact: +0.2kB gzipped
Test coverage: 9/9 teleport tests passing
Demo: Comprehensive showcase with modal/sidebar/notifications

Closes #4

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced Teleport component enabling UI rendering to alternate DOM locations via CSS selectors while preserving component state and event handling.
  • Documentation

    • Added comprehensive API reference and usage examples for Teleport with configuration options.
    • Added new demo application showcasing Teleport functionality.
  • Tests

    • Expanded test coverage for new Teleport behavior and edge cases.

Add complete teleport functionality to GEA framework enabling
content rendering at different DOM locations while preserving
component state and event handling.

Key features:
- JSX <Teleport to-selector="#target"> component syntax
- Robust event delegation for teleported elements
- Conditional teleporting via disabled prop
- Automatic cleanup on component disposal
- Zero breaking changes to existing APIs

Technical implementation:
- Add teleport handling in component render lifecycle
- Fix event registration timing in ComponentManager
- Implement dual element marking for reliable event delegation
- JSX transform integration for clean syntax
- TypeScript definitions and proper exports

Bundle impact: +0.2kB gzipped
Test coverage: 9/9 teleport tests passing
Demo: Comprehensive showcase with modal/sidebar/notifications

Closes dashersw#4
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

This PR implements a Teleport component for the GEA framework, enabling DOM subtrees to be moved to different document locations while preserving component state, event delegation, and reactivity. The implementation includes core rendering logic, JSX transform support, comprehensive test coverage, and documentation with a working example.

Changes

Cohort / File(s) Summary
Documentation
.cursor/skills/gea-framework/reference.md, docs/api-reference.md, packages/gea/README.md
Added Teleport API documentation including props (to-selector, disabled), behavioral guarantees (state preservation, event delegation, reactivity, cleanup), usage examples, and target requirements.
Example Project Setup
examples/teleport-demo/package.json, examples/teleport-demo/tsconfig.json, examples/teleport-demo/vite.config.ts, examples/teleport-demo/index.html
Created new example project with build configuration, HTML entry point with teleport target containers (#modal-root, #sidebar-root, #notification-root), TypeScript/Vite setup, and dev script in root package.json.
Example Implementation
examples/teleport-demo/src/TeleportDemo.tsx, examples/teleport-demo/src/main.ts
Implemented demo component with global state management (modal/sidebar visibility, counter, notifications), event handling for multiple teleport targets, conditional teleporting via disabled flag, and app entrypoint.
Core Teleport Feature
packages/gea/src/lib/types.ts, packages/gea/src/index.ts
Introduced TeleportProps interface with required 'to-selector' and optional disabled prop; exported new type from public API.
Component Lifecycle Integration
packages/gea/src/lib/base/component.tsx
Integrated teleport handling into render lifecycle via __handleTeleportElements(), __cleanupTeleports(), and __cleanupSingleTeleport() methods; added teleported element tracking across rerenders with focus preservation and state persistence.
Component Manager Enhancement
packages/gea/src/lib/base/component-manager.ts
Extended parent detection to recognize teleported relationships via __geaOriginalComponent property and data-gea-teleport-component attributes; deferred document event listener registration to markComponentRendered().
JSX Transform Support
packages/vite-plugin-gea/src/transform-jsx.ts
Added special-case handling for <Teleport> elements in JSX compilation; generates wrapper <div> with data-gea-teleport metadata and conditionally applies data-gea-teleport-disabled based on props.
Test Coverage
packages/gea/tests/teleport.test.ts
Comprehensive test suite covering teleport movement, disabled state, missing targets with warnings, focus preservation, event delegation, rerenders via observables, multiple teleports, nested elements, and cleanup on dispose.
Test Adjustment
packages/gea/tests/component-manager-coverage.test.ts
Updated event listener registration test to explicitly call markComponentRendered() after setComponent() to reflect new listener registration timing.
Release Metadata
.changeset/hip-papayas-rest.md
Marked @geajs/core as major version release introducing Teleport with zero-reset re-parenting and event delegation support.

Sequence Diagram

sequenceDiagram
    participant Component as GEA Component
    participant Handler as Teleport Handler
    participant DOM as DOM Tree
    participant Target as Target Element
    participant Events as Event System

    Component->>Handler: render() with teleport directives
    Handler->>DOM: discover teleport elements (data-gea-teleport)
    Handler->>Handler: resolve target selector (data-gea-teleport-to)
    Handler->>Target: insert origin placeholder
    Handler->>DOM: move child nodes to target
    Handler->>DOM: annotate moved nodes with teleport metadata
    
    rect rgba(200, 150, 100, 0.5)
        Note over Events,Target: State updates trigger rerender
        Component->>Handler: trigger rerender via observable
        Handler->>DOM: preserve teleported nodes temporarily
        Handler->>Handler: cleanup previous teleport state
        Handler->>DOM: restore preserved nodes to target
        Handler->>DOM: restore focus if in teleported subtree
    end
    
    rect rgba(100, 150, 200, 0.5)
        Note over Events,DOM: Event delegation
        Events->>DOM: click on teleported element
        DOM->>Component: event bubbles through handlers
        Component->>Component: execute event listener
    end
    
    Component->>Handler: dispose()
    Handler->>DOM: move nodes back to origin placeholder
    Handler->>DOM: remove origin placeholder & metadata
    Handler->>Target: clean up target element
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Runtime fixes #2: Modifies packages/vite-plugin-gea/src/transform-jsx.ts to add special-case handling for framework components (Link/Outlet/RouterView), similar pattern to Teleport transform additions in this PR.

Poem

🐰 A teleport dance, oh what delight!
DOM nodes leap to new homes, state burning bright,
No resets, no loss, just surgical grace,
Elements find their destined place!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main feature: introducing a Teleport component with zero-reset re-parenting, which is the primary objective of the changeset.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #4: Teleport component with zero-reset re-parenting, true DOM node re-parenting, reactive target support, JSX integration, and proper lifecycle/cleanup handling.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing Teleport functionality: documentation, type definitions, JSX transform, component lifecycle integration, event delegation, tests, and example demo. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
examples/teleport-demo/src/main.ts (1)

1-1: Unused import: Component is imported but never used.

The Component import from @geajs/core is not referenced in this file. Consider removing it to keep the code clean.

♻️ Suggested fix
-import { Component } from '@geajs/core'
 import TeleportDemo from './TeleportDemo.tsx'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/teleport-demo/src/main.ts` at line 1, Remove the unused import by
deleting the import specifier "Component" from the top-level import in main.ts
(the line importing from '@geajs/core'); ensure no other code references the
symbol Component and keep the import statement only if other exported names from
'@geajs/core' are needed.
packages/gea/src/lib/base/component.tsx (1)

427-438: Stored children array includes nodes that weren't moved.

Line 427 captures all childNodes, but line 430 only moves non-comment nodes. The stored __geaTeleportChildren array includes comment nodes that were never moved to the target. In __cleanupSingleTeleport (line 607), the code checks if (child.parentNode === target) which handles this correctly, but storing unmoved nodes adds unnecessary iteration.

♻️ Consider storing only moved children
      // Move all child nodes to the target element
-     const children = Array.from(teleportEl.childNodes)
-
-     children.forEach((child) => {
+     const movedChildren: Node[] = []
+     Array.from(teleportEl.childNodes).forEach((child) => {
        if (child && child.nodeType !== Node.COMMENT_NODE) {
          targetElement.appendChild(child)
+         movedChildren.push(child)
        }
      })

      // Store teleport info for cleanup
      ;(teleportEl as any).__geaTeleportTarget = targetElement
      ;(teleportEl as any).__geaTeleportPlaceholder = placeholder
-     ;(teleportEl as any).__geaTeleportChildren = children
+     ;(teleportEl as any).__geaTeleportChildren = movedChildren
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/gea/src/lib/base/component.tsx` around lines 427 - 438, The stored
__geaTeleportChildren currently captures all teleportEl.childNodes (including
comment nodes) but only non-comment nodes are actually moved; change the capture
to only include moved nodes by filtering out comment nodes when building the
array (use teleportEl.childNodes -> Array.from(...).filter(child =>
child.nodeType !== Node.COMMENT_NODE)) so __geaTeleportChildren only contains
nodes appended to __geaTeleportTarget; this keeps __cleanupSingleTeleport logic
(which checks child.parentNode === target) efficient and consistent with the
moved set.
packages/gea/tests/teleport.test.ts (1)

113-144: Console mock should use try/finally to ensure restoration on test failure.

If an assertion fails before line 143, console.warn remains mocked, potentially affecting subsequent tests.

♻️ Suggested improvement
   it('should handle missing target gracefully', () => {
     document.body.innerHTML = '<div id="app"></div>'

     // Mock console.warn to capture warning
     const originalWarn = console.warn
     let warningMessage = ''
     console.warn = (msg: string) => {
       warningMessage = msg
     }

-    class TestComponent extends Component {
-      template() {
-        return `
-          <div>
-            <div data-gea-teleport="true" data-gea-teleport-to="#missing-target" style="display: none;">
-              <div class="modal">Content</div>
-            </div>
-          </div>
-        `
-      }
-    }
-
-    const component = new TestComponent()
-    const appRoot = document.getElementById('app')!
-    component.render(appRoot)
-
-    // Should emit warning and keep content in place
-    assert(warningMessage.includes('Target element not found: `#missing-target`'))
-
-    // Content should remain in original location
-    const teleportDiv = appRoot.querySelector('[data-gea-teleport="true"]')!
-    assert.strictEqual(teleportDiv.querySelector('.modal')?.textContent, 'Content')
-
-    // Restore console.warn
-    console.warn = originalWarn
+    try {
+      class TestComponent extends Component {
+        template() {
+          return `
+            <div>
+              <div data-gea-teleport="true" data-gea-teleport-to="#missing-target" style="display: none;">
+                <div class="modal">Content</div>
+              </div>
+            </div>
+          `
+        }
+      }
+
+      const component = new TestComponent()
+      const appRoot = document.getElementById('app')!
+      component.render(appRoot)
+
+      // Should emit warning and keep content in place
+      assert(warningMessage.includes('Target element not found: `#missing-target`'))
+
+      // Content should remain in original location
+      const teleportDiv = appRoot.querySelector('[data-gea-teleport="true"]')!
+      assert.strictEqual(teleportDiv.querySelector('.modal')?.textContent, 'Content')
+    } finally {
+      // Restore console.warn
+      console.warn = originalWarn
+    }
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/gea/tests/teleport.test.ts` around lines 113 - 144, Wrap the
console.warn mocking and assertions in a try/finally so originalWarn is always
restored: capture originalWarn, assign the mock to console.warn and run the
TestComponent creation, component.render(...) and assertions inside a try block,
then restore console.warn = originalWarn in the finally block; reference the
existing variables originalWarn, warningMessage, console.warn, TestComponent and
the call to component.render to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/api-reference.md`:
- Line 615: The phrase "Target elements can be anywhere in the document,
including outside the React root" incorrectly references React; update that
sentence in docs/api-reference.md (search for that exact sentence) to use the
correct framework term such as "outside the app root" or "outside the Gea root"
(e.g., "Target elements can be anywhere in the document, including outside the
Gea root"). Ensure any nearby occurrences of "React root" are similarly
replaced.

In `@packages/gea/CHANGELOG.md`:
- Around line 3-6: The changelog shows a "Minor Changes" header but the version
was bumped only as a patch (1.0.8); revert direct changelog/version edits and
create a proper changeset instead: run npx changeset, choose a "minor" bump and
include the new feature (e.g., Teleport) so a .changeset/*.md is created, which
will update package versions (ensure `@geajs/core` and `@geajs/vite-plugin` are
included) and let the release tooling produce 1.1.0; remove or replace the
manual CHANGELOG.md edit so the release is driven by the generated changeset per
.changeset/config.json.

In `@packages/gea/src/lib/base/component.tsx`:
- Around line 422-424: The code creates a comment node named placeholder and
calls teleportEl.parentNode?.insertBefore(placeholder, teleportEl) but doesn't
handle the case where parentNode is null; update the logic around the
placeholder creation (the placeholder variable and the teleportEl insertion
block) to check the result of teleportEl.parentNode (or the return value of
insertBefore) and if it's null/undefined, abort the teleport setup—either return
early or mark the teleport as inactive—so you don't proceed to move children or
store metadata that assumes placeholder exists and will be used later in
cleanup; ensure the later cleanup code (which expects placeholder) is guarded or
only runs when the placeholder insertion succeeded.
- Around line 622-632: Cleanup currently removes the __geaOriginalComponent
property from teleported children but leaves the data-gea-teleport-component
attribute on the DOM nodes; update the cleanup loop in the component's teardown
(the code manipulating __teleportedElements and deleting __geaOriginalComponent)
to also remove the attribute by calling
removeAttribute('data-gea-teleport-component') on each child HTMLElement (use
the same child cast used for __teleportedElements), ensuring the attribute is
cleared whenever the property is deleted and the element is spliced out.

---

Nitpick comments:
In `@examples/teleport-demo/src/main.ts`:
- Line 1: Remove the unused import by deleting the import specifier "Component"
from the top-level import in main.ts (the line importing from '@geajs/core');
ensure no other code references the symbol Component and keep the import
statement only if other exported names from '@geajs/core' are needed.

In `@packages/gea/src/lib/base/component.tsx`:
- Around line 427-438: The stored __geaTeleportChildren currently captures all
teleportEl.childNodes (including comment nodes) but only non-comment nodes are
actually moved; change the capture to only include moved nodes by filtering out
comment nodes when building the array (use teleportEl.childNodes ->
Array.from(...).filter(child => child.nodeType !== Node.COMMENT_NODE)) so
__geaTeleportChildren only contains nodes appended to __geaTeleportTarget; this
keeps __cleanupSingleTeleport logic (which checks child.parentNode === target)
efficient and consistent with the moved set.

In `@packages/gea/tests/teleport.test.ts`:
- Around line 113-144: Wrap the console.warn mocking and assertions in a
try/finally so originalWarn is always restored: capture originalWarn, assign the
mock to console.warn and run the TestComponent creation, component.render(...)
and assertions inside a try block, then restore console.warn = originalWarn in
the finally block; reference the existing variables originalWarn,
warningMessage, console.warn, TestComponent and the call to component.render to
locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d974f89-ea9a-41e2-89e6-fddf1934b49e

📥 Commits

Reviewing files that changed from the base of the PR and between c04cfd9 and bbfe41c.

📒 Files selected for processing (19)
  • .cursor/skills/gea-framework/reference.md
  • docs/api-reference.md
  • examples/teleport-demo/index.html
  • examples/teleport-demo/package.json
  • examples/teleport-demo/src/TeleportDemo.tsx
  • examples/teleport-demo/src/main.ts
  • examples/teleport-demo/tsconfig.json
  • examples/teleport-demo/vite.config.ts
  • package.json
  • packages/gea/CHANGELOG.md
  • packages/gea/README.md
  • packages/gea/src/index.ts
  • packages/gea/src/jsx.ts
  • packages/gea/src/lib/base/component-manager.ts
  • packages/gea/src/lib/base/component.tsx
  • packages/gea/src/lib/types.ts
  • packages/gea/tests/component-manager-coverage.test.ts
  • packages/gea/tests/teleport.test.ts
  • packages/vite-plugin-gea/src/transform-jsx.ts

Comment on lines +3 to +6
## 1.0.8

### Minor Changes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Version bump inconsistency: "Minor Changes" labeled but patch version used.

The changelog labels this as "Minor Changes" (which typically corresponds to a minor semver bump), but the version incremented from 1.0.7 to 1.0.8 (a patch bump). A new feature like <Teleport> typically warrants a minor version bump (e.g., 1.1.0) according to semver conventions.

Based on learnings, version changes should be declared via npx changeset to create a .md file in .changeset/ with the appropriate bump type. This ensures linked packages @geajs/core and @geajs/vite-plugin receive the same minor/major bump level as configured in .changeset/config.json.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/gea/CHANGELOG.md` around lines 3 - 6, The changelog shows a "Minor
Changes" header but the version was bumped only as a patch (1.0.8); revert
direct changelog/version edits and create a proper changeset instead: run npx
changeset, choose a "minor" bump and include the new feature (e.g., Teleport) so
a .changeset/*.md is created, which will update package versions (ensure
`@geajs/core` and `@geajs/vite-plugin` are included) and let the release tooling
produce 1.1.0; remove or replace the manual CHANGELOG.md edit so the release is
driven by the generated changeset per .changeset/config.json.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/gea/tests/teleport.test.ts (1)

178-221: Strengthen the zero-reset regression here.

This only checks updated text, so it still passes if the teleported node is remounted and loses focus/value/media state. Please add an input/video-style assertion that the same DOM node survives rerender or target changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/gea/tests/teleport.test.ts` around lines 178 - 221, Add an assertion
that the teleported DOM node itself is preserved across the re-render rather
than only its text: inside TestComponent.template include a focusable/input or
video element (e.g., <input id="tele-input" value="initial"> or <video
id="tele-video" currentTime="1">) inside the teleported .modal, capture a
reference to that element from modalRoot right after initial render (const
before = modalRoot.querySelector('#tele-input')!), then after updating state and
awaiting the async re-render capture the element again (const after =
modalRoot.querySelector('#tele-input')!) and assert the same node instance is
preserved (before === after or before.isSameNode(after)) and that its mutable
state (value, focused state or currentTime) remains unchanged; keep references
to TestComponent, template, data-gea-teleport and modal-root to locate where to
add these checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/gea/src/lib/base/component.tsx`:
- Around line 164-165: mountCompiledChildComponents_() and __geaSwapChild()
replay the render lifecycle but never invoke __handleTeleportElements(), so
swapped-in and compiled children don't run their teleport setup; update both
methods to call this.__handleTeleportElements() after they reattach or swap
child DOM (and likewise add the same call in the other replay entry point
referenced around the __geaSwapChild() area) so each replay path performs the
teleport handling immediately after the child subtree is mounted.
- Line 306: The call to __cleanupTeleports() during the rerender path causes
teleported DOM (inputs/videos/iframes) to be moved back into the old tree and
then destroyed/recreated by template(), causing remounts; stop doing that by
deferring teleport cleanup until unmount (or after the new tree has been
reconciled) so teleported nodes are reparented rather than recreated.
Concretely: remove or guard the invocation of __cleanupTeleports() in the
rerender codepath that calls template(), and only call __cleanupTeleports() from
the unmount flow (or after the new DOM is inserted and placeholders reconciled);
adjust __cleanupTeleports() semantics if needed to perform true teardown-only
behavior and ensure __geaRequestRender()-triggered rerenders do not invoke it.
- Around line 404-412: The code calls document.querySelector(toSelector) which
throws for malformed selectors; wrap that call in a try/catch around the lookup
(the block that assigns targetElement from document.querySelector(toSelector))
and on catch call console.warn with the broken selector and the thrown error,
then skip teleporting (return) so the content remains in place; keep the
existing warning/return for null targetElement and reference the existing
identifiers teleportEl, toSelector and targetElement when adding the guard.

---

Nitpick comments:
In `@packages/gea/tests/teleport.test.ts`:
- Around line 178-221: Add an assertion that the teleported DOM node itself is
preserved across the re-render rather than only its text: inside
TestComponent.template include a focusable/input or video element (e.g., <input
id="tele-input" value="initial"> or <video id="tele-video" currentTime="1">)
inside the teleported .modal, capture a reference to that element from modalRoot
right after initial render (const before =
modalRoot.querySelector('#tele-input')!), then after updating state and awaiting
the async re-render capture the element again (const after =
modalRoot.querySelector('#tele-input')!) and assert the same node instance is
preserved (before === after or before.isSameNode(after)) and that its mutable
state (value, focused state or currentTime) remains unchanged; keep references
to TestComponent, template, data-gea-teleport and modal-root to locate where to
add these checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bf1ee4dd-410d-47b2-92dc-2a75497b146c

📥 Commits

Reviewing files that changed from the base of the PR and between bbfe41c and 65d97b6.

📒 Files selected for processing (5)
  • .changeset/hip-papayas-rest.md
  • docs/api-reference.md
  • examples/teleport-demo/src/main.ts
  • packages/gea/src/lib/base/component.tsx
  • packages/gea/tests/teleport.test.ts
✅ Files skipped from review due to trivial changes (3)
  • .changeset/hip-papayas-rest.md
  • examples/teleport-demo/src/main.ts
  • docs/api-reference.md

ahmetbilgay and others added 2 commits March 26, 2026 12:41
Fixes teleported elements losing state and focus during component
updates.
Adds proper lifecycle integration and error handling for production use.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/gea/src/lib/base/component-manager.ts`:
- Around line 243-252: The teleport lookup currently returns after finding the
first teleported owner, collapsing parentComps to a single id; change it so that
when you detect (currentNode as any).__geaOriginalComponent and fetch
originalComponent from this.componentRegistry, you push the originalComponent id
onto parentComps and then walk originalComponent.parentComponent repeatedly to
append its ancestor component ids (matching the normal DOM-walk order like
[leaf, parent, ...]) before returning; make the same fix for the duplicate block
at the later occurrence (lines ~256-263) so teleported elements preserve the
full ancestor component chain.

In `@packages/gea/src/lib/base/component.tsx`:
- Around line 506-517: The teleport wrapper currently stays hidden because early
returns on invalid selector, missing target, or disabled teleport never remove
the wrapper's inline style; update the code in the teleport handling (the query
block using toSelector/targetElement and the later disabled check around lines
handling the teleport wrapper) to clear the wrapper's display:none before each
return — e.g., find the teleport wrapper element (the wrapper created for
teleported children) and remove or reset its style.display (or remove the inline
style attribute) just prior to each early return in the try/catch for
document.querySelector(toSelector), the if (!targetElement) branch, and the
disabled branch so the fallback children render in place.
- Around line 328-350: The current preservation scans all descendants for
[data-gea-teleport-component="${this.id}"] and skips nodes without ids, which
detaches nested teleported children and lets __cleanupTeleports() remove
bookkeeping before the restored swap; instead, change the preservation in the
component (where preservedTeleports Map is created) to locate and detach only
the wrapper's top-level teleported children (i.e., direct children of the
component root that carry the data-gea-teleport-component attr tied to this.id),
do not require element.id (assign a stable temporary key if needed), record
their original wrapper bookkeeping so it is not stripped, and postpone or adjust
__cleanupTeleports() so it does not clear teleport metadata for entries present
in preservedTeleports until after restore/replaceChild completes; reference
preservedTeleports, __cleanupTeleports, and the data-gea-teleport-component
attribute on this.id when implementing these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 517c2637-55b4-447e-b076-a611ceb203a4

📥 Commits

Reviewing files that changed from the base of the PR and between 542c2f9 and 95d5070.

📒 Files selected for processing (3)
  • packages/gea/src/index.ts
  • packages/gea/src/lib/base/component-manager.ts
  • packages/gea/src/lib/base/component.tsx
✅ Files skipped from review due to trivial changes (1)
  • packages/gea/src/index.ts

Comment on lines +243 to +252
// Check if this is a teleported element or descendant of teleported element
let currentNode: HTMLElement | null = child
while (currentNode) {
const originalComponentId = (currentNode as any).__geaOriginalComponent
if (originalComponentId) {
const originalComponent = this.componentRegistry[originalComponentId]
if (originalComponent) {
parentComps.push(originalComponent)
child.parentComps = originalComponentId
return parentComps
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Teleport ownership lookup should keep the ancestor component chain.

This returns on the first teleported owner hit, so parentComps collapses to a single component id. The normal DOM walk below collects [leaf, parent, ...], so ancestor component handlers stop seeing events from teleported descendants. Build the cached list from the owning component's parentComponent chain instead of returning only the leaf owner.

Also applies to: 256-263

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/gea/src/lib/base/component-manager.ts` around lines 243 - 252, The
teleport lookup currently returns after finding the first teleported owner,
collapsing parentComps to a single id; change it so that when you detect
(currentNode as any).__geaOriginalComponent and fetch originalComponent from
this.componentRegistry, you push the originalComponent id onto parentComps and
then walk originalComponent.parentComponent repeatedly to append its ancestor
component ids (matching the normal DOM-walk order like [leaf, parent, ...])
before returning; make the same fix for the duplicate block at the later
occurrence (lines ~256-263) so teleported elements preserve the full ancestor
component chain.

Comment on lines +328 to +350
// Preserve teleported elements before rerender by temporarily detaching them
const preservedTeleports = new Map<
string,
{ element: HTMLElement; target: Element; placeholder: Comment; wasFocused: boolean }
>()

// Find and temporarily detach all teleported elements for this component
document.querySelectorAll(`[data-gea-teleport-component="${this.id}"]`).forEach((element: HTMLElement) => {
const target = element.parentElement
if (target && element.id) {
// Check if this element or any of its children has focus
const wasFocused = element === document.activeElement || element.contains(document.activeElement)

// Create a placeholder to mark where this element should be restored
const placeholder = document.createComment(`preserved-${element.id}`)
target.insertBefore(placeholder, element)
target.removeChild(element)
preservedTeleports.set(element.id, { element, target, placeholder, wasFocused })
}
})

// Clean up any remaining teleport state
this.__cleanupTeleports()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The rerender preservation path still loses the live teleported subtree.

This snapshots every [data-gea-teleport-component="${this.id}"] match even though the marker is applied to every descendant, and it skips any node without an id. Then __cleanupTeleports() strips the teleport bookkeeping from those preserved nodes before replaceChild(element, newElement) swaps them back, so the wrapper still tracks the discarded node. Nested descendants can be detached out of their parent, anonymous teleported nodes are still recreated, and later cleanup/dispose no longer follows the live subtree. Preserve the wrapper's top-level teleported children instead of rescanning marker attributes.

Also applies to: 442-449, 551-565

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/gea/src/lib/base/component.tsx` around lines 328 - 350, The current
preservation scans all descendants for
[data-gea-teleport-component="${this.id}"] and skips nodes without ids, which
detaches nested teleported children and lets __cleanupTeleports() remove
bookkeeping before the restored swap; instead, change the preservation in the
component (where preservedTeleports Map is created) to locate and detach only
the wrapper's top-level teleported children (i.e., direct children of the
component root that carry the data-gea-teleport-component attr tied to this.id),
do not require element.id (assign a stable temporary key if needed), record
their original wrapper bookkeeping so it is not stripped, and postpone or adjust
__cleanupTeleports() so it does not clear teleport metadata for entries present
in preservedTeleports until after restore/replaceChild completes; reference
preservedTeleports, __cleanupTeleports, and the data-gea-teleport-component
attribute on this.id when implementing these changes.

Comment on lines +506 to +517
// Safe selector query with error handling
let targetElement: Element | null = null
try {
targetElement = document.querySelector(toSelector)
} catch (error) {
console.warn(`[Teleport] Invalid target selector: ${toSelector}`, error)
return
}

if (!targetElement) {
console.warn(`[Teleport] Target element not found: ${toSelector}`)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Disabled or unresolved teleports currently render nothing.

packages/vite-plugin-gea/src/transform-jsx.ts:1360-1428 always emits the teleport wrapper with style="display: none;". Every early return here (catch, missing target, or disabled) leaves the children inside that hidden wrapper, so the fallback is invisible instead of rendering in place. At minimum, clear that inline style before returning when teleporting is skipped.

Also applies to: 523-525

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/gea/src/lib/base/component.tsx` around lines 506 - 517, The teleport
wrapper currently stays hidden because early returns on invalid selector,
missing target, or disabled teleport never remove the wrapper's inline style;
update the code in the teleport handling (the query block using
toSelector/targetElement and the later disabled check around lines handling the
teleport wrapper) to clear the wrapper's display:none before each return — e.g.,
find the teleport wrapper element (the wrapper created for teleported children)
and remove or reset its style.display (or remove the inline style attribute)
just prior to each early return in the try/catch for
document.querySelector(toSelector), the if (!targetElement) branch, and the
disabled branch so the fallback children render in place.

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.

[Feature Request] Native Re-parenting: A Zero-Reset Teleport Component

1 participant