From: daiwei Date: Fri, 21 Mar 2025 06:43:27 +0000 (+0800) Subject: wip: save X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=257138810f51802fdb4bfb0bb7068b84f309e9eb;p=thirdparty%2Fvuejs%2Fcore.git wip: save --- diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts new file mode 100644 index 0000000000..373d170bd5 --- /dev/null +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -0,0 +1,252 @@ +import { + type LooseRawProps, + type VaporComponent, + createComponent as originalCreateComponent, +} from '../../src/component' +import { VaporTeleport, template } from '@vue/runtime-vapor' + +import { makeRender } from '../_utils' +import { nextTick, onBeforeUnmount, onUnmounted, ref, shallowRef } from 'vue' + +const define = makeRender() + +describe('renderer: VaporTeleport', () => { + describe('eager mode', () => { + runSharedTests(false) + }) + + describe('defer mode', () => { + runSharedTests(true) + }) +}) + +function runSharedTests(deferMode: boolean): void { + const createComponent = deferMode + ? ( + component: VaporComponent, + rawProps?: LooseRawProps | null, + ...args: any[] + ) => { + if (component === VaporTeleport) { + rawProps!.defer = () => true + } + return originalCreateComponent(component, rawProps, ...args) + } + : originalCreateComponent + + test('should work', () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const { mount } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => template('
teleported
')(), + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported
') + }) + + test.todo('should work with SVG', async () => {}) + + test('should update target', async () => { + const targetA = document.createElement('div') + const targetB = document.createElement('div') + const target = ref(targetA) + const root = document.createElement('div') + + const { mount } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target.value, + }, + { + default: () => template('
teleported
')(), + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe('
root
') + expect(targetA.innerHTML).toBe('
teleported
') + expect(targetB.innerHTML).toBe('') + + target.value = targetB + await nextTick() + + expect(root.innerHTML).toBe('
root
') + expect(targetA.innerHTML).toBe('') + expect(targetB.innerHTML).toBe('
teleported
') + }) + + test('should update children', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + const children = shallowRef([template('
teleported
')()]) + + const { mount } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => children.value, + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + mount(root) + + expect(target.innerHTML).toBe('
teleported
') + + children.value = [template('')()] + await nextTick() + expect(target.innerHTML).toBe('') + + children.value = [template('teleported')()] + await nextTick() + expect(target.innerHTML).toBe('teleported') + }) + + test('should remove children when unmounted', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + function testUnmount(props: any) { + const { app } = define({ + setup() { + const n0 = createComponent(VaporTeleport, props, { + default: () => template('
teleported
')(), + }) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + app.mount(root) + + expect(target.innerHTML).toBe( + props.disabled() ? '' : '
teleported
', + ) + + app.unmount() + expect(target.innerHTML).toBe('') + expect(target.children.length).toBe(0) + } + + testUnmount({ to: () => target, disabled: () => false }) + testUnmount({ to: () => target, disabled: () => true }) + testUnmount({ to: () => null, disabled: () => true }) + }) + + test('component with multi roots should be removed when unmounted', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const { component: Comp } = define({ + setup() { + return [template('

')(), template('

')()] + }, + }) + + const { app } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => createComponent(Comp), + }, + ) + const n1 = template('

root
')() + return [n0, n1] + }, + }).create() + + app.mount(root) + expect(target.innerHTML).toBe('

') + + app.unmount() + expect(target.innerHTML).toBe('') + }) + + test.todo( + 'descendent component should be unmounted when teleport is disabled and unmounted', + async () => { + const root = document.createElement('div') + const beforeUnmount = vi.fn() + const unmounted = vi.fn() + const { component: Comp } = define({ + setup() { + onBeforeUnmount(beforeUnmount) + onUnmounted(unmounted) + return [template('

')(), template('

')()] + }, + }) + + const { app } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => null, + disabled: () => true, + }, + { + default: () => createComponent(Comp), + }, + ) + return [n0] + }, + }).create() + app.mount(root) + + expect(beforeUnmount).toHaveBeenCalledTimes(0) + expect(unmounted).toHaveBeenCalledTimes(0) + + app.unmount() + expect(beforeUnmount).toHaveBeenCalledTimes(1) + expect(unmounted).toHaveBeenCalledTimes(1) + }, + ) + + test.todo('multiple teleport with same target', async () => {}) + test.todo('should work when using template ref as target', async () => {}) + test.todo('disabled', async () => {}) + test.todo('moving teleport while enabled', async () => {}) + test.todo('moving teleport while disabled', async () => {}) + test.todo('should work with block tree', async () => {}) + test.todo( + `the dir hooks of the Teleport's children should be called correctly`, + async () => {}, + ) + test.todo( + `ensure that target changes when disabled are updated correctly when enabled`, + async () => {}, + ) + test.todo('toggle sibling node inside target node', async () => {}) + test.todo('unmount previous sibling node inside target node', async () => {}) + test.todo('accessing template refs inside teleport', async () => {}) +} diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 762d69ce81..e12ad4d87e 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -18,6 +18,7 @@ import { createComment, createTextNode, querySelector } from '../dom/node' import type { LooseRawProps, LooseRawSlots } from '../component' import { rawPropsProxyHandlers } from '../componentProps' import { renderEffect } from '../renderEffect' +import { extend } from '@vue/shared' export const VaporTeleportImpl = { name: 'VaporTeleport', @@ -25,7 +26,6 @@ export const VaporTeleportImpl = { __vapor: true, process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment { - const children = slots.default && (slots.default as BlockFn)() const frag = __DEV__ ? new TeleportFragment('teleport') : new TeleportFragment() @@ -35,31 +35,11 @@ export const VaporTeleportImpl = { rawPropsProxyHandlers, ) as any as TeleportProps - renderEffect(() => frag.update(resolvedProps, children)) - - frag.remove = parent => { - const { - nodes, - target, - cachedTargetAnchor, - targetStart, - placeholder, - mainAnchor, - } = frag - - remove(nodes, target || parent) - - // remove anchors - if (targetStart) { - let parentNode = targetStart.parentNode! - remove(targetStart!, parentNode) - remove(cachedTargetAnchor!, parentNode) - } - if (placeholder && placeholder.isConnected) { - remove(placeholder!, parent) - remove(mainAnchor!, parent) - } - } + renderEffect(() => { + const children = slots.default && (slots.default as BlockFn)() + // access the props to trigger tracking + frag.update(extend({}, resolvedProps), children) + }) return frag }, @@ -67,12 +47,10 @@ export const VaporTeleportImpl = { export class TeleportFragment extends VaporFragment { anchor: Node - target?: ParentNode | null targetStart?: Node | null - targetAnchor?: Node | null - cachedTargetAnchor?: Node mainAnchor?: Node placeholder?: Node + currentParent?: ParentNode | null constructor(anchorLabel?: string) { super([]) @@ -81,33 +59,21 @@ export class TeleportFragment extends VaporFragment { } update(props: TeleportProps, children: Block): void { + // teardown previous + if (this.currentParent && this.nodes) { + remove(this.nodes, this.currentParent) + } + this.nodes = children + const disabled = isTeleportDisabled(props) const parent = this.anchor.parentNode - if (!this.mainAnchor) { - this.mainAnchor = __DEV__ - ? createComment('teleport end') - : createTextNode() - } - if (!this.placeholder) { - this.placeholder = __DEV__ - ? createComment('teleport start') - : createTextNode() - } - if (parent) { - insert(this.placeholder, parent, this.anchor) - insert(this.mainAnchor, parent, this.anchor) + const mount = (parent: ParentNode, anchor: Node | null) => { + insert(this.nodes, (this.currentParent = parent), anchor) } - const disabled = isTeleportDisabled(props) - if (disabled) { - this.target = this.anchor.parentNode - this.targetAnchor = parent ? this.mainAnchor : null - } else { - const target = (this.target = resolveTarget( - props, - querySelector, - ) as ParentNode) + const mountToTarget = () => { + const target = (this.target = resolveTarget(props, querySelector)) if (target) { if ( // initial mount @@ -116,21 +82,38 @@ export class TeleportFragment extends VaporFragment { this.targetStart.parentNode !== target ) { ;[this.targetAnchor, this.targetStart] = prepareAnchor(target) - this.cachedTargetAnchor = this.targetAnchor - } else { - // re-mount or target not changed, use cached target anchor - this.targetAnchor = this.cachedTargetAnchor } + + mount(target, this.targetAnchor!) } else if (__DEV__) { - warn('Invalid Teleport target on mount:', target, `(${typeof target})`) + warn( + `Invalid Teleport target on ${this.targetStart ? 'update' : 'mount'}:`, + target, + `(${typeof target})`, + ) } } - const mountToTarget = () => { - insert(this.nodes, this.target!, this.targetAnchor) + if (parent && disabled) { + if (!this.mainAnchor) { + this.mainAnchor = __DEV__ + ? createComment('teleport end') + : createTextNode() + } + if (!this.placeholder) { + this.placeholder = __DEV__ + ? createComment('teleport start') + : createTextNode() + } + if (!this.mainAnchor.isConnected) { + insert(this.placeholder, parent, this.anchor) + insert(this.mainAnchor, parent, this.anchor) + } + + mount(parent, this.mainAnchor) } - if (parent) { + if (!disabled) { if (isTeleportDeferred(props)) { queuePostFlushCb(mountToTarget) } else { @@ -139,14 +122,30 @@ export class TeleportFragment extends VaporFragment { } } + remove = (parent: ParentNode | undefined): void => { + // remove nodes + remove(this.nodes, this.currentParent || parent) + + // remove anchors + if (this.targetStart) { + let parentNode = this.targetStart.parentNode! + remove(this.targetStart!, parentNode) + remove(this.targetAnchor!, parentNode) + } + if (this.placeholder) { + remove(this.placeholder!, parent) + remove(this.mainAnchor!, parent) + } + } + hydrate(): void { // TODO } } function prepareAnchor(target: ParentNode | null) { - const targetStart = createTextNode('targetStart') - const targetAnchor = createTextNode('targetAnchor') + const targetStart = createTextNode('') + const targetAnchor = createTextNode('') // attach a special property, so we can skip teleported content in // renderer's nextSibling search