})
describe('HMR', () => {
- test('rerender', async () => {
+ test('rerender child + rerender parent', async () => {
const target = document.createElement('div')
const root = document.createElement('div')
- const childId = 'test1-child'
- const parentId = 'test1-parent'
+ const childId = 'test1-child-rerender'
+ const parentId = 'test1-parent-rerender'
const { component: Child } = define({
__hmrId: childId,
expect(target.innerHTML).toBe('<div>teleported 2</div>')
})
- test('reload', async () => {
+ test('parent rerender + toggle disabled', async () => {
const target = document.createElement('div')
const root = document.createElement('div')
- const childId = 'test2-child'
- const parentId = 'test2-parent'
+ const parentId = 'test3-parent-rerender'
+ const disabled = ref(true)
+
+ const Child = defineVaporComponent({
+ render() {
+ return template('<div>teleported</div>')()
+ },
+ })
+
+ const { mount, component: Parent } = define({
+ __hmrId: parentId,
+ render() {
+ const n2 = template('<div><div>root</div></div>', true)() as any
+ setInsertionState(n2, 0)
+ createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => disabled.value,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ return n2
+ },
+ }).create()
+ createRecord(parentId, Parent as any)
+ mount(root)
+
+ expect(root.innerHTML).toBe(
+ '<div><!--teleport--><div>teleported</div><div>root</div></div>',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // rerender parent
+ rerender(parentId, () => {
+ const n2 = template('<div><div>root 2</div></div>', true)() as any
+ setInsertionState(n2, 0)
+ createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => disabled.value,
+ },
+ {
+ default: () => createComp(Child),
+ },
+ )
+ return n2
+ })
+
+ expect(root.innerHTML).toBe(
+ '<div><!--teleport--><div>teleported</div><div>root 2</div></div>',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // toggle disabled
+ disabled.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('<div><!--teleport--><div>root 2</div></div>')
+ expect(target.innerHTML).toBe('<div>teleported</div>')
+ })
+
+ test('reload child + reload parent', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const childId = 'test1-child-reload'
+ const parentId = 'test1-parent-reload'
const { component: Child } = define({
__hmrId: childId,
expect(target.innerHTML).toBe('')
})
- test('reload child + toggle disabled', async () => {
+ test('reload single root child + toggle disabled', async () => {
const target = document.createElement('div')
const root = document.createElement('div')
- const childId = 'test3-child'
- const parentId = 'test3-parent'
+ const childId = 'test2-child-reload'
+ const parentId = 'test2-parent-reload'
const disabled = ref(true)
const { component: Child } = define({
disabled: () => ctx.disabled,
},
{
+ // with single root child
default: () => createComp(Child),
},
)
expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
expect(target.innerHTML).toBe('<div>teleported 3</div>')
})
+
+ test('reload multiple root children + toggle disabled', async () => {
+ const target = document.createElement('div')
+ const root = document.createElement('div')
+ const childId = 'test3-child-reload'
+ const parentId = 'test3-parent-reload'
+
+ const disabled = ref(true)
+ const { component: Child } = define({
+ __hmrId: childId,
+ setup() {
+ const msg = ref('teleported')
+ return { msg }
+ },
+ render(ctx) {
+ const n0 = template(`<div> </div>`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ createRecord(childId, Child as any)
+
+ const { mount, component: Parent } = define({
+ __hmrId: parentId,
+ setup() {
+ const msg = ref('root')
+ return { msg, disabled }
+ },
+ render(ctx) {
+ const n0 = createComp(
+ VaporTeleport,
+ {
+ to: () => target,
+ disabled: () => ctx.disabled,
+ },
+ {
+ default: () => {
+ // with multiple root children
+ return [createComp(Child), template(`<span>child</span>`)()]
+ },
+ },
+ )
+ const n1 = template(`<div> </div>`)()
+ const x0 = child(n1 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0, n1]
+ },
+ }).create()
+ createRecord(parentId, Parent as any)
+ mount(root)
+
+ expect(root.innerHTML).toBe(
+ '<div>teleported</div><span>child</span><!--teleport--><div>root</div>',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // reload child by changing msg
+ reload(childId, {
+ __hmrId: childId,
+ __vapor: true,
+ setup() {
+ const msg = ref('teleported 2')
+ return { msg }
+ },
+ render(ctx: any) {
+ const n0 = template(`<div> </div>`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ expect(root.innerHTML).toBe(
+ '<div>teleported 2</div><span>child</span><!--teleport--><div>root</div>',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // reload child again by changing msg
+ reload(childId, {
+ __hmrId: childId,
+ __vapor: true,
+ setup() {
+ const msg = ref('teleported 3')
+ return { msg }
+ },
+ render(ctx: any) {
+ const n0 = template(`<div> </div>`)()
+ const x0 = child(n0 as any)
+ renderEffect(() => setText(x0 as any, ctx.msg))
+ return [n0]
+ },
+ })
+ expect(root.innerHTML).toBe(
+ '<div>teleported 3</div><span>child</span><!--teleport--><div>root</div>',
+ )
+ expect(target.innerHTML).toBe('')
+
+ // toggle disabled
+ disabled.value = false
+ await nextTick()
+ expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
+ expect(target.innerHTML).toBe('<div>teleported 3</div><span>child</span>')
+ })
})
})
import {
TeleportEndKey,
type TeleportProps,
+ currentInstance,
isTeleportDeferred,
isTeleportDisabled,
queuePostFlushCb,
import { rawPropsProxyHandlers } from '../componentProps'
import { renderEffect } from '../renderEffect'
import { extend, isArray } from '@vue/shared'
-import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
import { VaporFragment } from '../fragment'
export const teleportStack: TeleportFragment[] = __DEV__
> = __DEV__ ? new WeakMap() : (undefined as any)
/**
- * dev only.
+ * dev only
* when the root child component updates, synchronously update
- * the TeleportFragment's children and nodes.
+ * the TeleportFragment's nodes.
*/
export function handleTeleportRootComponentHmrReload(
instance: VaporComponentInstance,
if (teleport) {
instanceToTeleportMap.set(newInstance, teleport)
if (teleport.nodes === instance) {
- teleport.children = teleport.nodes = newInstance
+ teleport.nodes = newInstance
} else if (isArray(teleport.nodes)) {
const i = teleport.nodes.indexOf(instance)
- if (i > -1) {
- ;(teleport.children as Block[])[i] = teleport.nodes[i] = newInstance
- }
+ if (i !== -1) teleport.nodes[i] = newInstance
}
}
}
? new TeleportFragment('teleport')
: new TeleportFragment()
- pauseTracking()
- const scope = (frag.scope = new EffectScope())
- scope!.run(() => {
- renderEffect(() => {
- __DEV__ && teleportStack.push(frag)
- frag.updateChildren(
- (frag.children = slots.default && (slots.default as BlockFn)()),
- )
- __DEV__ && teleportStack.pop()
- })
+ const updateChildrenEffect = renderEffect(() => {
+ __DEV__ && teleportStack.push(frag)
+ frag.updateChildren(slots.default && (slots.default as BlockFn)())
+ __DEV__ && teleportStack.pop()
+ })
- renderEffect(() => {
- frag.update(
- // access the props to trigger tracking
- extend(
- {},
- new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps,
- ),
- frag.children!,
- )
- })
+ const updateEffect = renderEffect(() => {
+ frag.update(
+ // access the props to trigger tracking
+ extend(
+ {},
+ new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps,
+ ),
+ )
})
- resetTracking()
if (__DEV__) {
- // used in normalizeBlock to get the nodes of a TeleportFragment
- // during hmr update. return empty array if the teleport content
- // is mounted into the target container.
+ // used in `normalizeBlock` to get nodes of TeleportFragment during
+ // HMR updates. returns empty array if content is mounted in target
+ // container to prevent incorrect parent node lookup.
frag.getNodes = () => {
return frag.parent !== frag.currentParent ? [] : frag.nodes
}
+
+ // for HMR re-render
+ const instance = currentInstance as VaporComponentInstance
+ ;(
+ instance!.hmrRerenderEffects || (instance!.hmrRerenderEffects = [])
+ ).push(() => {
+ // remove the teleport content
+ frag.remove(frag.anchor.parentNode!)
+
+ // stop effects
+ updateChildrenEffect.stop()
+ updateEffect.stop()
+ })
}
return frag
class TeleportFragment extends VaporFragment {
anchor: Node
- scope: EffectScope | undefined
- children: Block | undefined
private targetStart?: Node
private mainAnchor?: Node
}
updateChildren(children: Block): void {
- // not mounted yet, early return
- if (!this.parent) return
-
- // teardown previous children
- remove(this.nodes, this.currentParent)
-
- // mount new
- insert((this.nodes = children), this.currentParent, this.currentAnchor)
+ // not mounted yet
+ if (!this.parent) {
+ this.nodes = children
+ } else {
+ // teardown previous nodes
+ remove(this.nodes, this.currentParent)
+ // mount new nodes
+ insert((this.nodes = children), this.currentParent, this.currentAnchor)
+ }
}
- update(props: TeleportProps, children: Block): void {
- this.nodes = children
-
+ update(props: TeleportProps): void {
const mount = (parent: ParentNode, anchor: Node | null) => {
insert(
this.nodes,
}
remove = (parent: ParentNode | undefined): void => {
- // stop effect scope
- if (this.scope) {
- this.scope.stop()
- this.scope = undefined
- }
-
// remove nodes
if (this.nodes) {
remove(this.nodes, this.currentParent)
- this.children = this.nodes = []
+ this.nodes = []
}
// remove anchors