mirror of https://github.com/vuejs/core.git
wip: save
This commit is contained in:
parent
9be697b38c
commit
257138810f
|
@ -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('<div>teleported</div>')(),
|
||||
},
|
||||
)
|
||||
const n1 = template('<div>root</div>')()
|
||||
return [n0, n1]
|
||||
},
|
||||
}).create()
|
||||
mount(root)
|
||||
|
||||
expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
|
||||
expect(target.innerHTML).toBe('<div>teleported</div>')
|
||||
})
|
||||
|
||||
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('<div>teleported</div>')(),
|
||||
},
|
||||
)
|
||||
const n1 = template('<div>root</div>')()
|
||||
return [n0, n1]
|
||||
},
|
||||
}).create()
|
||||
mount(root)
|
||||
|
||||
expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
|
||||
expect(targetA.innerHTML).toBe('<div>teleported</div>')
|
||||
expect(targetB.innerHTML).toBe('')
|
||||
|
||||
target.value = targetB
|
||||
await nextTick()
|
||||
|
||||
expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
|
||||
expect(targetA.innerHTML).toBe('')
|
||||
expect(targetB.innerHTML).toBe('<div>teleported</div>')
|
||||
})
|
||||
|
||||
test('should update children', async () => {
|
||||
const target = document.createElement('div')
|
||||
const root = document.createElement('div')
|
||||
const children = shallowRef([template('<div>teleported</div>')()])
|
||||
|
||||
const { mount } = define({
|
||||
setup() {
|
||||
const n0 = createComponent(
|
||||
VaporTeleport,
|
||||
{
|
||||
to: () => target,
|
||||
},
|
||||
{
|
||||
default: () => children.value,
|
||||
},
|
||||
)
|
||||
const n1 = template('<div>root</div>')()
|
||||
return [n0, n1]
|
||||
},
|
||||
}).create()
|
||||
mount(root)
|
||||
|
||||
expect(target.innerHTML).toBe('<div>teleported</div>')
|
||||
|
||||
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('<div>teleported</div>')(),
|
||||
})
|
||||
const n1 = template('<div>root</div>')()
|
||||
return [n0, n1]
|
||||
},
|
||||
}).create()
|
||||
app.mount(root)
|
||||
|
||||
expect(target.innerHTML).toBe(
|
||||
props.disabled() ? '' : '<div>teleported</div>',
|
||||
)
|
||||
|
||||
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('<p>')(), template('<p>')()]
|
||||
},
|
||||
})
|
||||
|
||||
const { app } = define({
|
||||
setup() {
|
||||
const n0 = createComponent(
|
||||
VaporTeleport,
|
||||
{
|
||||
to: () => target,
|
||||
},
|
||||
{
|
||||
default: () => createComponent(Comp),
|
||||
},
|
||||
)
|
||||
const n1 = template('<div>root</div>')()
|
||||
return [n0, n1]
|
||||
},
|
||||
}).create()
|
||||
|
||||
app.mount(root)
|
||||
expect(target.innerHTML).toBe('<p></p><p></p>')
|
||||
|
||||
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('<p>')(), template('<p>')()]
|
||||
},
|
||||
})
|
||||
|
||||
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 () => {})
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue