Skip to content

Commit 20fedac

Browse files
authored
Merge pull request #77 from koculu/lazy-r-teleport-binding
Teleport can bind to the target lazily.
2 parents 6a2b293 + 503af63 commit 20fedac

File tree

3 files changed

+129
-30
lines changed

3 files changed

+129
-30
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "regor",
3-
"version": "1.4.0",
3+
"version": "1.4.1",
44
"description": "A modern UI framework for web and desktop applications, inspired by Vue's concepts and powered by simplicity and flexibility.",
55
"author": "Ahmed Yasin Koculu",
66
"license": "MIT",

src/bind/Binder.ts

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import { getSwitch, hasSwitch, rswitch } from './switch'
2828
* @internal
2929
*/
3030
export class Binder {
31+
__bindDepth = 0
32+
__pendingTeleports = new Map<HTMLElement, string>()
33+
3134
__parser: Parser
3235
__ifBinder: IfBinder
3336
__forBinder: ForBinder
@@ -70,17 +73,63 @@ export class Binder {
7073
}
7174

7275
__bindDefault(element: Element): void {
73-
if (
74-
element.nodeType !== Node.ELEMENT_NODE ||
75-
element.hasAttribute(this.__pre)
76-
)
77-
return
78-
if (this.__ifBinder.__bindAll(element)) return
79-
if (this.__forBinder.__bindAll(element)) return
80-
if (this.__dynamicBinder.__bindAll(element)) return
81-
this.__componentBinder.__bindAll(element)
82-
this.__unwrapTemplates(element)
83-
this.__bindAttributes(element, true)
76+
++this.__bindDepth
77+
try {
78+
if (
79+
element.nodeType !== Node.ELEMENT_NODE ||
80+
element.hasAttribute(this.__pre)
81+
)
82+
return
83+
if (this.__ifBinder.__bindAll(element)) return
84+
if (this.__forBinder.__bindAll(element)) return
85+
if (this.__dynamicBinder.__bindAll(element)) return
86+
this.__componentBinder.__bindAll(element)
87+
this.__unwrapTemplates(element)
88+
this.__bindAttributes(element, true)
89+
} finally {
90+
--this.__bindDepth
91+
if (this.__bindDepth === 0) this.__flushPendingTeleports()
92+
}
93+
}
94+
95+
__resolveTeleportTarget(el: HTMLElement, selector: string): Element | null {
96+
let queryRoot = document as ParentNode
97+
if (!queryRoot) {
98+
let root = el.parentNode
99+
while (root?.parentNode) root = root.parentNode
100+
if (!root) return null
101+
queryRoot = root
102+
}
103+
return queryRoot.querySelector(selector)
104+
}
105+
106+
__performTeleport(el: HTMLElement, selector: string): boolean {
107+
const teleportTo = this.__resolveTeleportTarget(el, selector)
108+
if (!teleportTo) return false
109+
const parent = el.parentElement
110+
if (!parent) return false
111+
const placeholder = new Comment(`teleported => '${selector}'`)
112+
parent.insertBefore(placeholder, el)
113+
;(el as HTMLElement & { teleportedFrom: Node }).teleportedFrom = placeholder
114+
;(placeholder as Comment & { teleportedTo: HTMLElement }).teleportedTo = el
115+
addUnbinder(placeholder, () => {
116+
removeNode(el)
117+
})
118+
teleportTo.appendChild(el)
119+
return true
120+
}
121+
122+
__queueTeleport(el: HTMLElement, selector: string): void {
123+
this.__pendingTeleports.set(el, selector)
124+
}
125+
126+
__flushPendingTeleports(): void {
127+
const pending = this.__pendingTeleports
128+
if (pending.size === 0) return
129+
this.__pendingTeleports = new Map<HTMLElement, string>()
130+
for (const [el, selector] of pending.entries()) {
131+
this.__performTeleport(el, selector)
132+
}
84133
}
85134

86135
__bindAttributes(element: Element, isRecursive: boolean): void {
@@ -141,25 +190,9 @@ export class Binder {
141190
): boolean {
142191
if (config !== teleportDirective) return false
143192
if (isNullOrWhitespace(valueExpression)) return true
144-
let queryRoot = document as ParentNode
145-
if (!queryRoot) {
146-
let root = el.parentNode
147-
while (root?.parentNode) root = root.parentNode
148-
if (root) queryRoot = root
149-
else return true
193+
if (!this.__performTeleport(el, valueExpression)) {
194+
this.__queueTeleport(el, valueExpression)
150195
}
151-
const teleportTo = queryRoot.querySelector(valueExpression)
152-
if (!teleportTo) return true
153-
const parent = el.parentElement
154-
if (!parent) return true
155-
const placeholder = new Comment(`teleported => '${valueExpression}'`)
156-
parent.insertBefore(placeholder, el)
157-
;(el as HTMLElement & { teleportedFrom: Node }).teleportedFrom = placeholder
158-
;(placeholder as Comment & { teleportedTo: HTMLElement }).teleportedTo = el
159-
addUnbinder(placeholder, () => {
160-
removeNode(el)
161-
})
162-
teleportTo.appendChild(el)
163196
return true
164197
}
165198

tests/app/createComponent.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,72 @@ test('nested component tree teleports when parent binds :r-teleport on ChildComp
842842
}
843843
})
844844

845+
test('r-teleport fallthrough can target markup rendered by another nested component tree lazily', () => {
846+
const cleanup = createDom('<html><body><div id="app"></div></body></html>')
847+
try {
848+
const root = document.querySelector('#app') as HTMLElement
849+
850+
const firstChild = defineComponent(
851+
html`<article class="first-child-root">
852+
<span class="first-payload">from first</span>
853+
</article>`,
854+
)
855+
const firstParent = defineComponent(
856+
html`<section class="first-parent-shell">
857+
<FirstChild
858+
:r-teleport="target"
859+
class="first-fallthrough"
860+
data-flow="from-parent-1"
861+
></FirstChild>
862+
</section>`,
863+
{
864+
context: () => ({
865+
target: '#second-child-target',
866+
}),
867+
},
868+
)
869+
870+
const secondChild = defineComponent(
871+
html`<div id="second-child-target" class="second-child-target">
872+
<p class="second-static">target host</p>
873+
</div>`,
874+
)
875+
const secondParent = defineComponent(
876+
html`<section class="second-parent-shell">
877+
<SecondChild></SecondChild>
878+
</section>`,
879+
)
880+
881+
createApp(
882+
{
883+
components: { firstParent, firstChild, secondParent, secondChild },
884+
},
885+
{
886+
element: root,
887+
// render second tree first so teleport target exists before first child bind
888+
template: html`<main>
889+
<FirstParent></FirstParent>
890+
<SecondParent></SecondParent>
891+
</main>`,
892+
},
893+
)
894+
const target = root.querySelector('#second-child-target') as HTMLElement
895+
const moved = target.querySelector(
896+
'.first-child-root',
897+
) as HTMLElement | null
898+
expect(moved).toBeTruthy()
899+
expect(moved?.classList.contains('first-fallthrough')).toBe(true)
900+
expect(moved?.getAttribute('data-flow')).toBe('from-parent-1')
901+
expect(target.querySelector('.first-payload')?.textContent?.trim()).toBe(
902+
'from first',
903+
)
904+
expect(root.querySelector('.first-parent-shell')).toBeTruthy()
905+
expect(root.innerHTML).toContain("teleported => '#second-child-target'")
906+
} finally {
907+
cleanup()
908+
}
909+
})
910+
845911
test('component named slot fallback renders when slot content is not provided', () => {
846912
const root = document.createElement('div')
847913
const slotComp = defineComponent(

0 commit comments

Comments
 (0)