Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "regor",
"version": "1.4.0",
"version": "1.4.1",
"description": "A modern UI framework for web and desktop applications, inspired by Vue's concepts and powered by simplicity and flexibility.",
"author": "Ahmed Yasin Koculu",
"license": "MIT",
Expand Down
91 changes: 62 additions & 29 deletions src/bind/Binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import { getSwitch, hasSwitch, rswitch } from './switch'
* @internal
*/
export class Binder {
__bindDepth = 0
__pendingTeleports = new Map<HTMLElement, string>()

__parser: Parser
__ifBinder: IfBinder
__forBinder: ForBinder
Expand Down Expand Up @@ -70,17 +73,63 @@ export class Binder {
}

__bindDefault(element: Element): void {
if (
element.nodeType !== Node.ELEMENT_NODE ||
element.hasAttribute(this.__pre)
)
return
if (this.__ifBinder.__bindAll(element)) return
if (this.__forBinder.__bindAll(element)) return
if (this.__dynamicBinder.__bindAll(element)) return
this.__componentBinder.__bindAll(element)
this.__unwrapTemplates(element)
this.__bindAttributes(element, true)
++this.__bindDepth
try {
if (
element.nodeType !== Node.ELEMENT_NODE ||
element.hasAttribute(this.__pre)
)
return
if (this.__ifBinder.__bindAll(element)) return
if (this.__forBinder.__bindAll(element)) return
if (this.__dynamicBinder.__bindAll(element)) return
this.__componentBinder.__bindAll(element)
this.__unwrapTemplates(element)
this.__bindAttributes(element, true)
} finally {
--this.__bindDepth
if (this.__bindDepth === 0) this.__flushPendingTeleports()
}
}

__resolveTeleportTarget(el: HTMLElement, selector: string): Element | null {
let queryRoot = document as ParentNode
if (!queryRoot) {
let root = el.parentNode
while (root?.parentNode) root = root.parentNode
if (!root) return null
queryRoot = root
}
return queryRoot.querySelector(selector)
}

__performTeleport(el: HTMLElement, selector: string): boolean {
const teleportTo = this.__resolveTeleportTarget(el, selector)
if (!teleportTo) return false
const parent = el.parentElement
if (!parent) return false
const placeholder = new Comment(`teleported => '${selector}'`)
parent.insertBefore(placeholder, el)
;(el as HTMLElement & { teleportedFrom: Node }).teleportedFrom = placeholder
;(placeholder as Comment & { teleportedTo: HTMLElement }).teleportedTo = el
addUnbinder(placeholder, () => {
removeNode(el)
})
teleportTo.appendChild(el)
return true
}

__queueTeleport(el: HTMLElement, selector: string): void {
this.__pendingTeleports.set(el, selector)
}

__flushPendingTeleports(): void {
const pending = this.__pendingTeleports
if (pending.size === 0) return
this.__pendingTeleports = new Map<HTMLElement, string>()
for (const [el, selector] of pending.entries()) {
this.__performTeleport(el, selector)
}
}

__bindAttributes(element: Element, isRecursive: boolean): void {
Expand Down Expand Up @@ -141,25 +190,9 @@ export class Binder {
): boolean {
if (config !== teleportDirective) return false
if (isNullOrWhitespace(valueExpression)) return true
let queryRoot = document as ParentNode
if (!queryRoot) {
let root = el.parentNode
while (root?.parentNode) root = root.parentNode
if (root) queryRoot = root
else return true
if (!this.__performTeleport(el, valueExpression)) {
this.__queueTeleport(el, valueExpression)
}
const teleportTo = queryRoot.querySelector(valueExpression)
if (!teleportTo) return true
const parent = el.parentElement
if (!parent) return true
const placeholder = new Comment(`teleported => '${valueExpression}'`)
parent.insertBefore(placeholder, el)
;(el as HTMLElement & { teleportedFrom: Node }).teleportedFrom = placeholder
;(placeholder as Comment & { teleportedTo: HTMLElement }).teleportedTo = el
addUnbinder(placeholder, () => {
removeNode(el)
})
teleportTo.appendChild(el)
return true
}

Expand Down
66 changes: 66 additions & 0 deletions tests/app/createComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,72 @@ test('nested component tree teleports when parent binds :r-teleport on ChildComp
}
})

test('r-teleport fallthrough can target markup rendered by another nested component tree lazily', () => {
const cleanup = createDom('<html><body><div id="app"></div></body></html>')
try {
const root = document.querySelector('#app') as HTMLElement

const firstChild = defineComponent(
html`<article class="first-child-root">
<span class="first-payload">from first</span>
</article>`,
)
const firstParent = defineComponent(
html`<section class="first-parent-shell">
<FirstChild
:r-teleport="target"
class="first-fallthrough"
data-flow="from-parent-1"
></FirstChild>
</section>`,
{
context: () => ({
target: '#second-child-target',
}),
},
)

const secondChild = defineComponent(
html`<div id="second-child-target" class="second-child-target">
<p class="second-static">target host</p>
</div>`,
)
const secondParent = defineComponent(
html`<section class="second-parent-shell">
<SecondChild></SecondChild>
</section>`,
)

createApp(
{
components: { firstParent, firstChild, secondParent, secondChild },
},
{
element: root,
// render second tree first so teleport target exists before first child bind
template: html`<main>
<FirstParent></FirstParent>
<SecondParent></SecondParent>
</main>`,
},
)
const target = root.querySelector('#second-child-target') as HTMLElement
const moved = target.querySelector(
'.first-child-root',
) as HTMLElement | null
expect(moved).toBeTruthy()
expect(moved?.classList.contains('first-fallthrough')).toBe(true)
expect(moved?.getAttribute('data-flow')).toBe('from-parent-1')
expect(target.querySelector('.first-payload')?.textContent?.trim()).toBe(
'from first',
)
expect(root.querySelector('.first-parent-shell')).toBeTruthy()
expect(root.innerHTML).toContain("teleported => '#second-child-target'")
} finally {
cleanup()
}
})

test('component named slot fallback renders when slot content is not provided', () => {
const root = document.createElement('div')
const slotComp = defineComponent(
Expand Down