diff --git a/packages/runtime-vapor/__tests__/if.spec.ts b/packages/runtime-vapor/__tests__/if.spec.ts
index 5f8012ca9..ee4b87907 100644
--- a/packages/runtime-vapor/__tests__/if.spec.ts
+++ b/packages/runtime-vapor/__tests__/if.spec.ts
@@ -1,7 +1,9 @@
import { defineComponent } from 'vue'
import {
+ append,
children,
createIf,
+ fragment,
insert,
nextTick,
ref,
@@ -10,7 +12,6 @@ import {
setText,
template,
} from '../src'
-import { NOOP } from '@vue/shared'
import type { Mock } from 'vitest'
let host: HTMLElement
@@ -103,4 +104,65 @@ describe('createIf', () => {
expect(spyIfFn!).toHaveBeenCalledTimes(1)
expect(spyElseFn!).toHaveBeenCalledTimes(2)
})
+
+ test('should handle nested template', async () => {
+ // mock this template:
+ //
+ // Hello Vapor
+ //
+
+ const ok1 = ref(true)
+ const ok2 = ref(true)
+
+ const t0 = template('Vapor')
+ const t1 = template('Hello ')
+ const t2 = fragment()
+ render(
+ defineComponent({
+ setup() {
+ // render
+ return (() => {
+ const n0 = t2()
+ append(
+ n0,
+ createIf(
+ () => ok1.value,
+ () => {
+ const n2 = t1()
+ append(
+ n2,
+ createIf(
+ () => ok2.value,
+ () => t0(),
+ ),
+ )
+ return n2
+ },
+ ),
+ )
+ return n0
+ })()
+ },
+ }) as any,
+ {},
+ '#host',
+ )
+ expect(host.innerHTML).toBe('Hello Vapor')
+
+ ok1.value = false
+ await nextTick()
+ expect(host.innerHTML).toBe('')
+
+ ok1.value = true
+ await nextTick()
+ expect(host.innerHTML).toBe('Hello Vapor')
+
+ ok2.value = false
+ await nextTick()
+ expect(host.innerHTML).toBe('Hello ')
+
+ ok1.value = false
+ await nextTick()
+ expect(host.innerHTML).toBe('')
+ })
})
diff --git a/packages/runtime-vapor/__tests__/template.spec.ts b/packages/runtime-vapor/__tests__/template.spec.ts
index 16ac33833..84c45783d 100644
--- a/packages/runtime-vapor/__tests__/template.spec.ts
+++ b/packages/runtime-vapor/__tests__/template.spec.ts
@@ -4,12 +4,12 @@ describe('api: template', () => {
test('create element', () => {
const t = template('
')
const root = t()
- expect(root).toBeInstanceOf(DocumentFragment)
- expect(root.childNodes[0]).toBeInstanceOf(HTMLDivElement)
+ expect(root).toBeInstanceOf(Array)
+ expect(root[0]).toBeInstanceOf(HTMLDivElement)
- const div2 = t()
- expect(div2).toBeInstanceOf(DocumentFragment)
- expect(div2).not.toBe(root)
+ const root2 = t()
+ expect(root2).toBeInstanceOf(Array)
+ expect(root2).not.toBe(root)
})
test('create fragment', () => {
diff --git a/packages/runtime-vapor/src/dom.ts b/packages/runtime-vapor/src/dom.ts
index 4fe2f0635..eedb73722 100644
--- a/packages/runtime-vapor/src/dom.ts
+++ b/packages/runtime-vapor/src/dom.ts
@@ -5,85 +5,69 @@ export * from './dom/patchProp'
export * from './dom/templateRef'
export * from './dom/on'
-export function insert(block: Block, parent: Node, anchor: Node | null = null) {
+function normalizeBlock(block: Block): Node[] {
+ const nodes: Node[] = []
if (block instanceof Node) {
- parent.insertBefore(block, anchor)
+ nodes.push(block)
} else if (isArray(block)) {
- for (const child of block) insert(child, parent, anchor)
+ block.forEach(child => nodes.push(...normalizeBlock(child)))
+ } else if (block) {
+ nodes.push(...normalizeBlock(block.nodes))
+ block.anchor && nodes.push(block.anchor)
+ }
+ return nodes
+}
+
+export function insert(
+ block: Block,
+ parent: ParentBlock,
+ anchor: Node | null = null,
+) {
+ if (isArray(parent)) {
+ const index = anchor ? parent.indexOf(anchor) : -1
+ if (index > -1) {
+ parent.splice(index, 0, block)
+ } else {
+ parent.push(block)
+ }
} else {
- insert(block.nodes, parent, anchor)
- block.anchor && parent.insertBefore(block.anchor, anchor)
+ normalizeBlock(block).forEach(node => parent.insertBefore(node, anchor))
}
}
export function prepend(parent: ParentBlock, ...blocks: Block[]) {
- const nodes: Node[] = []
-
- for (const block of blocks) {
- if (block instanceof Node) {
- nodes.push(block)
- } else if (isArray(block)) {
- prepend(parent, ...block)
- } else {
- prepend(parent, block.nodes)
- block.anchor && prepend(parent, block.anchor)
- }
- }
-
- if (!nodes.length) return
-
- if (parent instanceof Node) {
- // TODO use insertBefore for better performance https://jsbench.me/rolpg250hh/1
- parent.prepend(...nodes)
- } else if (isArray(parent)) {
- parent.unshift(...nodes)
+ if (isArray(parent)) {
+ parent.unshift(...blocks)
+ } else {
+ parent.prepend(...normalizeBlock(blocks))
}
}
export function append(parent: ParentBlock, ...blocks: Block[]) {
- const nodes: Node[] = []
-
- for (const block of blocks) {
- if (block instanceof Node) {
- nodes.push(block)
- } else if (isArray(block)) {
- append(parent, ...block)
- } else {
- append(parent, block.nodes)
- block.anchor && append(parent, block.anchor)
- }
- }
-
- if (!nodes.length) return
-
- if (parent instanceof Node) {
- // TODO use insertBefore for better performance
- parent.append(...nodes)
- } else if (isArray(parent)) {
- parent.push(...nodes)
+ if (isArray(parent)) {
+ parent.push(...blocks)
+ } else {
+ parent.append(...normalizeBlock(blocks))
}
}
-export function remove(block: Block, parent: ParentNode) {
- if (block instanceof DocumentFragment) {
- remove(Array.from(block.childNodes), parent)
- } else if (block instanceof Node) {
- parent.removeChild(block)
- } else if (isArray(block)) {
- for (const child of block) remove(child, parent)
+export function remove(block: Block, parent: ParentBlock) {
+ if (isArray(parent)) {
+ const index = parent.indexOf(block)
+ if (index > -1) {
+ parent.splice(index, 1)
+ }
} else {
- remove(block.nodes, parent)
- block.anchor && parent.removeChild(block.anchor)
+ normalizeBlock(block).forEach(node => parent.removeChild(node))
}
}
type Children = Record
-export function children(n: Node): Children {
+export function children(nodes: ChildNode[]): Children {
const result: Children = {}
- const array = Array.from(n.childNodes)
- for (let i = 0; i < array.length; i++) {
- const n = array[i]
- result[i] = [n, children(n)]
+ for (let i = 0; i < nodes.length; i++) {
+ const n = nodes[i]
+ result[i] = [n, children(Array.from(n.childNodes))]
}
return result
}
diff --git a/packages/runtime-vapor/src/if.ts b/packages/runtime-vapor/src/if.ts
index a5072ed72..e2b50f266 100644
--- a/packages/runtime-vapor/src/if.ts
+++ b/packages/runtime-vapor/src/if.ts
@@ -1,8 +1,10 @@
import { renderWatch } from './renderWatch'
-import { type BlockFn, type Fragment, fragmentKey } from './render'
-import { effectScope, onEffectCleanup } from '@vue/reactivity'
+import { type Block, type Fragment, fragmentKey } from './render'
+import { type EffectScope, effectScope } from '@vue/reactivity'
import { createComment, createTextNode, insert, remove } from './dom'
+type BlockFn = () => Block
+
export const createIf = (
condition: () => any,
b1: BlockFn,
@@ -11,8 +13,14 @@ export const createIf = (
): Fragment => {
let branch: BlockFn | undefined
let parent: ParentNode | undefined | null
+ let block: Block | undefined
+ let scope: EffectScope | undefined
const anchor = __DEV__ ? createComment('if') : createTextNode('')
- const fragment: Fragment = { nodes: [], anchor, [fragmentKey]: true }
+ const fragment: Fragment = {
+ nodes: [],
+ anchor,
+ [fragmentKey]: true,
+ }
// TODO: SSR
// if (isHydrating) {
@@ -24,23 +32,16 @@ export const createIf = (
() => !!condition(),
value => {
parent ||= anchor.parentNode
+ if (block) {
+ scope!.stop()
+ remove(block, parent!)
+ }
if ((branch = value ? b1 : b2)) {
- let scope = effectScope()
- let block = scope.run(branch)!
-
- if (block instanceof DocumentFragment) {
- block = Array.from(block.childNodes)
- }
- fragment.nodes = block
-
+ scope = effectScope()
+ fragment.nodes = block = scope.run(branch)!
parent && insert(block, parent, anchor)
-
- onEffectCleanup(() => {
- parent ||= anchor.parentNode
- scope.stop()
- remove(block, parent!)
- })
} else {
+ scope = block = undefined
fragment.nodes = []
}
},
diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts
index 8e81cab21..8bae59953 100644
--- a/packages/runtime-vapor/src/render.ts
+++ b/packages/runtime-vapor/src/render.ts
@@ -15,13 +15,12 @@ import { queuePostRenderEffect } from './scheduler'
export const fragmentKey = Symbol('fragment')
export type Block = Node | Fragment | Block[]
-export type ParentBlock = ParentNode | Node[]
+export type ParentBlock = ParentNode | Block[]
export type Fragment = {
nodes: Block
anchor?: Node
[fragmentKey]: true
}
-export type BlockFn = (props?: any) => Block
export function render(
comp: Component,
diff --git a/packages/runtime-vapor/src/template.ts b/packages/runtime-vapor/src/template.ts
index 8b505a6ec..17ab8e5a4 100644
--- a/packages/runtime-vapor/src/template.ts
+++ b/packages/runtime-vapor/src/template.ts
@@ -1,4 +1,4 @@
-export const template = (str: string): (() => DocumentFragment) => {
+export function template(str: string): () => ChildNode[] {
let cached = false
let node: DocumentFragment
return () => {
@@ -10,16 +10,20 @@ export const template = (str: string): (() => DocumentFragment) => {
// first render: insert the node directly.
// this removes it from the template fragment to avoid keeping two copies
// of the inserted tree in memory, even if the template is used only once.
- return (node = t.content).cloneNode(true) as DocumentFragment
+ return fragmentToNodes((node = t.content))
} else {
// repeated renders: clone from cache. This is more performant and
// efficient when dealing with big lists where the template is repeated
// many times.
- return node.cloneNode(true) as DocumentFragment
+ return fragmentToNodes(node)
}
}
}
-export function fragment(): () => Node[] {
+function fragmentToNodes(node: DocumentFragment): ChildNode[] {
+ return Array.from((node.cloneNode(true) as DocumentFragment).childNodes)
+}
+
+export function fragment(): () => ChildNode[] {
return () => []
}