wip(vapor): v-show work on components

This commit is contained in:
Evan You 2025-02-02 12:18:51 +08:00
parent 250127c13d
commit d51403c1d3
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
3 changed files with 123 additions and 33 deletions

View File

@ -13,8 +13,8 @@ export const vShowHidden: unique symbol = Symbol('_vsh')
*/
export interface VShowElement extends HTMLElement {
// _vod = vue original display
[vShowOriginalDisplay]: string
[vShowHidden]: boolean
[vShowOriginalDisplay]?: string
[vShowHidden]?: boolean
}
export const vShow: ObjectDirective<VShowElement> & { name?: 'show' } = {
@ -58,7 +58,7 @@ if (__DEV__) {
}
function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el[vShowOriginalDisplay] : 'none'
el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
el[vShowHidden] = !value
}

View File

@ -1,14 +1,13 @@
import {
applyVShow,
children,
createComponent,
createIf,
defineVaporComponent,
on,
template,
// @ts-expect-error
vShow,
// @ts-expect-error
withDirectives,
} from '../../src'
import { nextTick, ref } from 'vue'
import { type VShowElement, nextTick, ref } from 'vue'
import { describe, expect, test } from 'vitest'
import { makeRender } from '../_utils'
@ -26,12 +25,12 @@ const createDemo = (defaultValue: boolean) =>
const n0 = t0()
const n1 = children(n0, 0)
const n2 = children(n0, 1)
withDirectives(n2, [[vShow, () => visible.value]])
applyVShow(n2 as VShowElement, () => visible.value)
on(n1 as HTMLElement, 'click', () => handleClick)
return n0
})
describe.todo('directive: v-show', () => {
describe('directive: v-show', () => {
test('basic', async () => {
const { host } = createDemo(true).render()
const btn = host.querySelector('button')
@ -56,36 +55,87 @@ describe.todo('directive: v-show', () => {
test('should work on component', async () => {
const t0 = template('<div>child</div>')
const t1 = template('<button>toggle</button>')
const n0 = t0()
const visible = ref(true)
function handleClick() {
visible.value = !visible.value
}
const { component: Child } = define({
render() {
return n0
setup() {
return t0()
},
})
const { host } = define({
render() {
const n1 = t1()
const n2 = createComponent(Child, null, null, true)
withDirectives(n2, [[vShow, () => visible.value]])
on(n1 as HTMLElement, 'click', () => handleClick)
return [n1, n2]
setup() {
const n1 = createComponent(Child, null, null, true)
applyVShow(n1, () => visible.value)
return n1
},
}).render()
expect(host.innerHTML).toBe('<button>toggle</button><div>child</div>')
expect(host.innerHTML).toBe('<div>child</div>')
const btn = host.querySelector('button')
btn?.click()
visible.value = !visible.value
await nextTick()
expect(host.innerHTML).toBe('<div style="display: none;">child</div>')
})
test('warn on non-single-element-root component', () => {
const Child = defineVaporComponent({
setup() {
return document.createTextNode('b')
},
})
define({
setup() {
const n1 = createComponent(Child)
applyVShow(n1, () => true)
return n1
},
}).render()
expect(
'v-show used on component with non-single-element root node',
).toHaveBeenWarned()
})
test('should work on component with dynamic fragment root', async () => {
const t0 = template('<div>child</div>')
const t1 = template('<span>child</span>')
const childIf = ref(true)
const visible = ref(true)
const { component: Child } = define({
setup() {
return createIf(
() => childIf.value,
() => t0(),
() => t1(),
)
},
})
const { host } = define({
setup() {
const n1 = createComponent(Child, null, null, true)
applyVShow(n1, () => visible.value)
return n1
},
}).render()
expect(host.innerHTML).toBe('<div>child</div><!--if-->')
visible.value = !visible.value
await nextTick()
expect(host.innerHTML).toBe(
'<button>toggle</button><div style="display: none;">child</div>',
'<div style="display: none;">child</div><!--if-->',
)
childIf.value = !childIf.value
await nextTick()
expect(host.innerHTML).toBe(
'<span style="display: none;">child</span><!--if-->',
)
visible.value = !visible.value
await nextTick()
expect(host.innerHTML).toBe('<span style="">child</span><!--if-->')
})
})

View File

@ -2,15 +2,55 @@ import {
type VShowElement,
vShowHidden,
vShowOriginalDisplay,
warn,
} from '@vue/runtime-dom'
import { renderEffect } from '../renderEffect'
import { isVaporComponent } from '../component'
import { type Block, DynamicFragment } from '../block'
import { isArray } from '@vue/shared'
export function applyVShow(el: VShowElement, source: () => any): void {
el[vShowOriginalDisplay] = el.style.display === 'none' ? '' : el.style.display
renderEffect(() => setDisplay(el, source()))
export function applyVShow(target: Block, source: () => any): void {
if (isVaporComponent(target)) {
return applyVShow(target.block, source)
}
if (isArray(target) && target.length === 1) {
return applyVShow(target[0], source)
}
if (target instanceof DynamicFragment) {
const update = target.update
target.update = (render, key) => {
update.call(target, render, key)
setDisplay(target, source())
}
}
renderEffect(() => setDisplay(target, source()))
}
function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el[vShowOriginalDisplay] : 'none'
el[vShowHidden] = !value
function setDisplay(target: Block, value: unknown): void {
if (isVaporComponent(target)) {
return setDisplay(target, value)
}
if (isArray(target) && target.length === 1) {
return setDisplay(target[0], value)
}
if (target instanceof DynamicFragment) {
return setDisplay(target.nodes, value)
}
if (target instanceof Element) {
const el = target as VShowElement
if (!(vShowOriginalDisplay in el)) {
el[vShowOriginalDisplay] =
el.style.display === 'none' ? '' : el.style.display
}
el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
el[vShowHidden] = !value
} else if (__DEV__) {
warn(
`v-show used on component with non-single-element root node ` +
`and will be ignored.`,
)
}
}