test(vapor): componentSlots
This commit is contained in:
parent
7f3b883aea
commit
dff54a17fb
|
@ -476,11 +476,6 @@ describe('component: props', () => {
|
||||||
expect(changeSpy).toHaveBeenCalledTimes(1)
|
expect(changeSpy).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
// #3371
|
|
||||||
test.todo(`avoid double-setting props when casting`, async () => {
|
|
||||||
// TODO: provide, slots
|
|
||||||
})
|
|
||||||
|
|
||||||
test('support null in required + multiple-type declarations', () => {
|
test('support null in required + multiple-type declarations', () => {
|
||||||
const { render } = define({
|
const { render } = define({
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -2,30 +2,29 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createComponent,
|
createComponent,
|
||||||
|
// @ts-expect-error
|
||||||
createForSlots,
|
createForSlots,
|
||||||
createSlot,
|
createSlot,
|
||||||
createVaporApp,
|
createVaporApp,
|
||||||
defineComponent,
|
defineVaporComponent,
|
||||||
getCurrentInstance,
|
|
||||||
insert,
|
insert,
|
||||||
nextTick,
|
|
||||||
prepend,
|
prepend,
|
||||||
ref,
|
|
||||||
renderEffect,
|
renderEffect,
|
||||||
setText,
|
setText,
|
||||||
template,
|
template,
|
||||||
withDestructure,
|
|
||||||
} from '../src'
|
} from '../src'
|
||||||
|
import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
|
||||||
import { makeRender } from './_utils'
|
import { makeRender } from './_utils'
|
||||||
|
|
||||||
const define = makeRender<any>()
|
const define = makeRender<any>()
|
||||||
|
|
||||||
function renderWithSlots(slots: any): any {
|
function renderWithSlots(slots: any): any {
|
||||||
let instance: any
|
let instance: any
|
||||||
const Comp = defineComponent({
|
const Comp = defineVaporComponent({
|
||||||
render() {
|
setup() {
|
||||||
const t0 = template('<div></div>')
|
const t0 = template('<div></div>')
|
||||||
const n0 = t0()
|
const n0 = t0()
|
||||||
instance = getCurrentInstance()
|
instance = currentInstance
|
||||||
return n0
|
return n0
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -40,51 +39,12 @@ function renderWithSlots(slots: any): any {
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.todo('component: slots', () => {
|
describe('component: slots', () => {
|
||||||
test('initSlots: instance.slots should be set correctly', () => {
|
|
||||||
let instance: any
|
|
||||||
const Comp = defineComponent({
|
|
||||||
render() {
|
|
||||||
const t0 = template('<div></div>')
|
|
||||||
const n0 = t0()
|
|
||||||
instance = getCurrentInstance()
|
|
||||||
return n0
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const { render } = define({
|
|
||||||
render() {
|
|
||||||
return createComponent(Comp, {}, { header: () => template('header')() })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
render()
|
|
||||||
|
|
||||||
expect(instance.slots.header()).toMatchObject(
|
|
||||||
document.createTextNode('header'),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// NOTE: slot normalization is not supported
|
|
||||||
test.todo(
|
|
||||||
'initSlots: should normalize object slots (when value is null, string, array)',
|
|
||||||
() => {},
|
|
||||||
)
|
|
||||||
test.todo(
|
|
||||||
'initSlots: should normalize object slots (when value is function)',
|
|
||||||
() => {},
|
|
||||||
)
|
|
||||||
|
|
||||||
// runtime-core's "initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
|
|
||||||
test('initSlots: instance.slots should be set correctly', () => {
|
test('initSlots: instance.slots should be set correctly', () => {
|
||||||
const { slots } = renderWithSlots({
|
const { slots } = renderWithSlots({
|
||||||
default: () => template('<span></span>')(),
|
default: () => template('<span></span>')(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// expect(
|
|
||||||
// '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
|
|
||||||
// ).toHaveBeenWarned()
|
|
||||||
|
|
||||||
expect(slots.default()).toMatchObject(document.createElement('span'))
|
expect(slots.default()).toMatchObject(document.createElement('span'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -93,18 +53,24 @@ describe.todo('component: slots', () => {
|
||||||
|
|
||||||
let instance: any
|
let instance: any
|
||||||
const Child = () => {
|
const Child = () => {
|
||||||
instance = getCurrentInstance()
|
instance = currentInstance
|
||||||
return template('child')()
|
return template('child')()
|
||||||
}
|
}
|
||||||
|
|
||||||
const { render } = define({
|
const { render } = define({
|
||||||
render() {
|
render() {
|
||||||
return createComponent(Child, {}, [
|
return createComponent(
|
||||||
() =>
|
Child,
|
||||||
flag1.value
|
{},
|
||||||
? { name: 'one', fn: () => template('<span></span>')() }
|
{
|
||||||
: { name: 'two', fn: () => template('<div></div>')() },
|
$: [
|
||||||
])
|
() =>
|
||||||
|
flag1.value
|
||||||
|
? { name: 'one', fn: () => template('<span></span>')() }
|
||||||
|
: { name: 'two', fn: () => template('<div></div>')() },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -120,196 +86,27 @@ describe.todo('component: slots', () => {
|
||||||
expect(instance.slots).toHaveProperty('two')
|
expect(instance.slots).toHaveProperty('two')
|
||||||
})
|
})
|
||||||
|
|
||||||
// NOTE: it is not supported
|
test.todo('should work with createFlorSlots', async () => {
|
||||||
// test('updateSlots: instance.slots should be updated correctly (when slotType is null)', () => {})
|
|
||||||
|
|
||||||
// runtime-core's "updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
|
|
||||||
test('updateSlots: instance.slots should be update correctly', async () => {
|
|
||||||
const flag1 = ref(true)
|
|
||||||
|
|
||||||
let instance: any
|
|
||||||
const Child = () => {
|
|
||||||
instance = getCurrentInstance()
|
|
||||||
return template('child')()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { render } = define({
|
|
||||||
setup() {
|
|
||||||
return createComponent(Child, {}, [
|
|
||||||
() =>
|
|
||||||
flag1.value
|
|
||||||
? { name: 'header', fn: () => template('header')() }
|
|
||||||
: { name: 'footer', fn: () => template('footer')() },
|
|
||||||
])
|
|
||||||
},
|
|
||||||
})
|
|
||||||
render()
|
|
||||||
|
|
||||||
expect(instance.slots).toHaveProperty('header')
|
|
||||||
flag1.value = false
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// expect(
|
|
||||||
// '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
|
|
||||||
// ).toHaveBeenWarned()
|
|
||||||
|
|
||||||
expect(instance.slots).toHaveProperty('footer')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('the current instance should be kept in the slot', async () => {
|
|
||||||
let instanceInDefaultSlot: any
|
|
||||||
let instanceInFooSlot: any
|
|
||||||
|
|
||||||
const Comp = defineComponent({
|
|
||||||
render() {
|
|
||||||
const instance = getCurrentInstance()
|
|
||||||
instance!.slots.default!()
|
|
||||||
instance!.slots.foo!()
|
|
||||||
return template('<div></div>')()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const { instance } = define({
|
|
||||||
render() {
|
|
||||||
return createComponent(Comp, {}, [
|
|
||||||
{
|
|
||||||
default: () => {
|
|
||||||
instanceInDefaultSlot = getCurrentInstance()
|
|
||||||
return template('content')()
|
|
||||||
},
|
|
||||||
foo: () => {
|
|
||||||
instanceInFooSlot = getCurrentInstance()
|
|
||||||
return template('content')()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
},
|
|
||||||
}).render()
|
|
||||||
|
|
||||||
expect(instanceInDefaultSlot).toBe(instance)
|
|
||||||
expect(instanceInFooSlot).toBe(instance)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('the current instance should be kept in the dynamic slots', async () => {
|
|
||||||
let instanceInDefaultSlot: any
|
|
||||||
let instanceInVForSlot: any
|
|
||||||
let instanceInVIfSlot: any
|
|
||||||
|
|
||||||
const Comp = defineComponent({
|
|
||||||
render() {
|
|
||||||
const instance = getCurrentInstance()
|
|
||||||
instance!.slots.default!()
|
|
||||||
instance!.slots.inVFor!()
|
|
||||||
instance!.slots.inVIf!()
|
|
||||||
return template('<div></div>')()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const { instance } = define({
|
|
||||||
render() {
|
|
||||||
return createComponent(Comp, {}, [
|
|
||||||
{
|
|
||||||
default: () => {
|
|
||||||
instanceInDefaultSlot = getCurrentInstance()
|
|
||||||
return template('content')()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
() => ({
|
|
||||||
name: 'inVFor',
|
|
||||||
fn: () => {
|
|
||||||
instanceInVForSlot = getCurrentInstance()
|
|
||||||
return template('content')()
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
() => ({
|
|
||||||
name: 'inVIf',
|
|
||||||
fn: () => {
|
|
||||||
instanceInVIfSlot = getCurrentInstance()
|
|
||||||
return template('content')()
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
},
|
|
||||||
}).render()
|
|
||||||
|
|
||||||
expect(instanceInDefaultSlot).toBe(instance)
|
|
||||||
expect(instanceInVForSlot).toBe(instance)
|
|
||||||
expect(instanceInVIfSlot).toBe(instance)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('dynamicSlots should update separately', async () => {
|
|
||||||
const flag1 = ref(true)
|
|
||||||
const flag2 = ref(true)
|
|
||||||
const slotFn1 = vitest.fn()
|
|
||||||
const slotFn2 = vitest.fn()
|
|
||||||
|
|
||||||
let instance: any
|
|
||||||
const Child = () => {
|
|
||||||
instance = getCurrentInstance()
|
|
||||||
return template('child')()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { render } = define({
|
|
||||||
render() {
|
|
||||||
return createComponent(Child, {}, [
|
|
||||||
() => {
|
|
||||||
slotFn1()
|
|
||||||
return flag1.value
|
|
||||||
? { name: 'one', fn: () => template('one')() }
|
|
||||||
: { name: 'two', fn: () => template('two')() }
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
slotFn2()
|
|
||||||
return flag2.value
|
|
||||||
? { name: 'three', fn: () => template('three')() }
|
|
||||||
: { name: 'four', fn: () => template('four')() }
|
|
||||||
},
|
|
||||||
])
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
render()
|
|
||||||
|
|
||||||
expect(instance.slots).toHaveProperty('one')
|
|
||||||
expect(instance.slots).toHaveProperty('three')
|
|
||||||
expect(slotFn1).toHaveBeenCalledTimes(1)
|
|
||||||
expect(slotFn2).toHaveBeenCalledTimes(1)
|
|
||||||
|
|
||||||
flag1.value = false
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
expect(instance.slots).toHaveProperty('two')
|
|
||||||
expect(instance.slots).toHaveProperty('three')
|
|
||||||
expect(slotFn1).toHaveBeenCalledTimes(2)
|
|
||||||
expect(slotFn2).toHaveBeenCalledTimes(1)
|
|
||||||
|
|
||||||
flag2.value = false
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
expect(instance.slots).toHaveProperty('two')
|
|
||||||
expect(instance.slots).toHaveProperty('four')
|
|
||||||
expect(slotFn1).toHaveBeenCalledTimes(2)
|
|
||||||
expect(slotFn2).toHaveBeenCalledTimes(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should work with createFlorSlots', async () => {
|
|
||||||
const loop = ref([1, 2, 3])
|
const loop = ref([1, 2, 3])
|
||||||
|
|
||||||
let instance: any
|
let instance: any
|
||||||
const Child = () => {
|
const Child = () => {
|
||||||
instance = getCurrentInstance()
|
instance = currentInstance
|
||||||
return template('child')()
|
return template('child')()
|
||||||
}
|
}
|
||||||
|
|
||||||
const { render } = define({
|
const { render } = define({
|
||||||
setup() {
|
setup() {
|
||||||
return createComponent(Child, {}, [
|
return createComponent(Child, null, {
|
||||||
() =>
|
$: [
|
||||||
createForSlots(loop.value, (item, i) => ({
|
() =>
|
||||||
name: item,
|
// @ts-expect-error
|
||||||
fn: () => template(item + i)(),
|
createForSlots(loop.value, (item, i) => ({
|
||||||
})),
|
name: item,
|
||||||
])
|
fn: () => template(item + i)(),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
render()
|
render()
|
||||||
|
@ -325,16 +122,11 @@ describe.todo('component: slots', () => {
|
||||||
expect(instance.slots).not.toHaveProperty('1')
|
expect(instance.slots).not.toHaveProperty('1')
|
||||||
})
|
})
|
||||||
|
|
||||||
test.todo('should respect $stable flag', async () => {
|
// passes but no warning for slot invocation in vapor currently
|
||||||
// TODO: $stable flag?
|
|
||||||
})
|
|
||||||
|
|
||||||
test.todo('should not warn when mounting another app in setup', () => {
|
test.todo('should not warn when mounting another app in setup', () => {
|
||||||
// TODO: warning
|
const Comp = defineVaporComponent({
|
||||||
const Comp = defineComponent({
|
setup(_, { slots }) {
|
||||||
render() {
|
return slots.default!()
|
||||||
const i = getCurrentInstance()
|
|
||||||
return i!.slots.default!()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const mountComp = () => {
|
const mountComp = () => {
|
||||||
|
@ -351,9 +143,7 @@ describe.todo('component: slots', () => {
|
||||||
const App = {
|
const App = {
|
||||||
setup() {
|
setup() {
|
||||||
mountComp()
|
mountComp()
|
||||||
},
|
return []
|
||||||
render() {
|
|
||||||
return null!
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
createVaporApp(App).mount(document.createElement('div'))
|
createVaporApp(App).mount(document.createElement('div'))
|
||||||
|
@ -363,63 +153,58 @@ describe.todo('component: slots', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('createSlot', () => {
|
describe('createSlot', () => {
|
||||||
test('slot should be render correctly', () => {
|
test('slot should be rendered correctly', () => {
|
||||||
const Comp = defineComponent(() => {
|
const Comp = defineVaporComponent(() => {
|
||||||
const n0 = template('<div>')()
|
const n0 = template('<div>')()
|
||||||
insert(createSlot('header'), n0 as any as ParentNode)
|
insert(createSlot('header'), n0 as any as ParentNode)
|
||||||
return n0
|
return n0
|
||||||
})
|
})
|
||||||
|
|
||||||
const { host } = define(() => {
|
const { host } = define(() => {
|
||||||
return createComponent(Comp, {}, { header: () => template('header')() })
|
return createComponent(Comp, null, {
|
||||||
|
header: () => template('header')(),
|
||||||
|
})
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<div>header</div>')
|
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('slot should be render correctly with binds', async () => {
|
test('slot should be rendered correctly with slot props', async () => {
|
||||||
const Comp = defineComponent(() => {
|
const Comp = defineVaporComponent(() => {
|
||||||
const n0 = template('<div></div>')()
|
const n0 = template('<div></div>')()
|
||||||
insert(
|
insert(
|
||||||
createSlot('header', [{ title: () => 'header' }]),
|
createSlot('header', { title: () => 'header' }),
|
||||||
n0 as any as ParentNode,
|
n0 as any as ParentNode,
|
||||||
)
|
)
|
||||||
return n0
|
return n0
|
||||||
})
|
})
|
||||||
|
|
||||||
const { host } = define(() => {
|
const { host } = define(() => {
|
||||||
return createComponent(Comp, {}, [
|
return createComponent(Comp, null, {
|
||||||
{
|
header: props => {
|
||||||
header: withDestructure(
|
const el = template('<h1></h1>')()
|
||||||
({ title }) => [title],
|
renderEffect(() => {
|
||||||
ctx => {
|
setText(el, props.title)
|
||||||
const el = template('<h1></h1>')()
|
})
|
||||||
renderEffect(() => {
|
return el
|
||||||
setText(el, ctx[0])
|
|
||||||
})
|
|
||||||
return el
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
])
|
})
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<div><h1>header</h1></div>')
|
expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dynamic slot props', async () => {
|
test('dynamic slot props', async () => {
|
||||||
let props: any
|
let props: any
|
||||||
|
|
||||||
const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
|
const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
|
||||||
const Comp = defineComponent(() =>
|
const Comp = defineVaporComponent(() =>
|
||||||
createSlot('default', [() => bindObj.value]),
|
createSlot('default', { $: [() => bindObj.value] }),
|
||||||
)
|
)
|
||||||
define(() =>
|
define(() =>
|
||||||
createComponent(
|
createComponent(Comp, null, {
|
||||||
Comp,
|
default: _props => ((props = _props), []),
|
||||||
{},
|
}),
|
||||||
{ default: _props => ((props = _props), []) },
|
|
||||||
),
|
|
||||||
).render()
|
).render()
|
||||||
|
|
||||||
expect(props).toEqual({ foo: 1, baz: 'qux' })
|
expect(props).toEqual({ foo: 1, baz: 'qux' })
|
||||||
|
@ -438,15 +223,16 @@ describe.todo('component: slots', () => {
|
||||||
|
|
||||||
const foo = ref(0)
|
const foo = ref(0)
|
||||||
const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
|
const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
|
||||||
const Comp = defineComponent(() =>
|
const Comp = defineVaporComponent(() =>
|
||||||
createSlot('default', [{ foo: () => foo.value }, () => bindObj.value]),
|
createSlot('default', {
|
||||||
|
foo: () => foo.value,
|
||||||
|
$: [() => bindObj.value],
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
define(() =>
|
define(() =>
|
||||||
createComponent(
|
createComponent(Comp, null, {
|
||||||
Comp,
|
default: _props => ((props = _props), []),
|
||||||
{},
|
}),
|
||||||
{ default: _props => ((props = _props), []) },
|
|
||||||
),
|
|
||||||
).render()
|
).render()
|
||||||
|
|
||||||
expect(props).toEqual({ foo: 100, baz: 'qux' })
|
expect(props).toEqual({ foo: 100, baz: 'qux' })
|
||||||
|
@ -460,123 +246,67 @@ describe.todo('component: slots', () => {
|
||||||
expect(props).toEqual({ foo: 2, baz: 'qux' })
|
expect(props).toEqual({ foo: 2, baz: 'qux' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('slot class binding should be merged', async () => {
|
test('dynamic slot should be rendered correctly with slot props', async () => {
|
||||||
let props: any
|
const val = ref('header')
|
||||||
|
|
||||||
const className = ref('foo')
|
const Comp = defineVaporComponent(() => {
|
||||||
const classObj = ref({ bar: true })
|
|
||||||
const Comp = defineComponent(() =>
|
|
||||||
createSlot('default', [
|
|
||||||
{ class: () => className.value },
|
|
||||||
() => ({ class: ['baz', 'qux'] }),
|
|
||||||
{ class: () => classObj.value },
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
define(() =>
|
|
||||||
createComponent(
|
|
||||||
Comp,
|
|
||||||
{},
|
|
||||||
{ default: _props => ((props = _props), []) },
|
|
||||||
),
|
|
||||||
).render()
|
|
||||||
|
|
||||||
expect(props).toEqual({ class: 'foo baz qux bar' })
|
|
||||||
|
|
||||||
classObj.value.bar = false
|
|
||||||
await nextTick()
|
|
||||||
expect(props).toEqual({ class: 'foo baz qux' })
|
|
||||||
|
|
||||||
className.value = ''
|
|
||||||
await nextTick()
|
|
||||||
expect(props).toEqual({ class: 'baz qux' })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('slot style binding should be merged', async () => {
|
|
||||||
let props: any
|
|
||||||
|
|
||||||
const style = ref<any>({ fontSize: '12px' })
|
|
||||||
const Comp = defineComponent(() =>
|
|
||||||
createSlot('default', [
|
|
||||||
{ style: () => style.value },
|
|
||||||
() => ({ style: { width: '100px', color: 'blue' } }),
|
|
||||||
{ style: () => 'color: red' },
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
define(() =>
|
|
||||||
createComponent(
|
|
||||||
Comp,
|
|
||||||
{},
|
|
||||||
{ default: _props => ((props = _props), []) },
|
|
||||||
),
|
|
||||||
).render()
|
|
||||||
|
|
||||||
expect(props).toEqual({
|
|
||||||
style: {
|
|
||||||
fontSize: '12px',
|
|
||||||
width: '100px',
|
|
||||||
color: 'red',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
style.value = null
|
|
||||||
await nextTick()
|
|
||||||
expect(props).toEqual({
|
|
||||||
style: {
|
|
||||||
width: '100px',
|
|
||||||
color: 'red',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('dynamic slot should be render correctly with binds', async () => {
|
|
||||||
const Comp = defineComponent(() => {
|
|
||||||
const n0 = template('<div></div>')()
|
const n0 = template('<div></div>')()
|
||||||
prepend(
|
prepend(
|
||||||
n0 as any as ParentNode,
|
n0 as any as ParentNode,
|
||||||
createSlot('header', [{ title: () => 'header' }]),
|
createSlot('header', { title: () => val.value }),
|
||||||
)
|
)
|
||||||
return n0
|
return n0
|
||||||
})
|
})
|
||||||
|
|
||||||
const { host } = define(() => {
|
const { host } = define(() => {
|
||||||
// dynamic slot
|
// dynamic slot
|
||||||
return createComponent(Comp, {}, [
|
return createComponent(Comp, null, {
|
||||||
() => ({
|
$: [
|
||||||
name: 'header',
|
() => ({
|
||||||
fn: (props: any) => template(props.title)(),
|
name: 'header',
|
||||||
}),
|
fn: (props: any) => template(props.title)(),
|
||||||
])
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
|
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
|
||||||
|
|
||||||
|
val.value = 'footer'
|
||||||
|
await nextTick()
|
||||||
|
expect(host.innerHTML).toBe('<div>footer<!--slot--></div>')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('dynamic slot outlet should be render correctly with binds', async () => {
|
test('dynamic slot outlet should be render correctly with slot props', async () => {
|
||||||
const Comp = defineComponent(() => {
|
const val = ref('header')
|
||||||
|
|
||||||
|
const Comp = defineVaporComponent(() => {
|
||||||
const n0 = template('<div></div>')()
|
const n0 = template('<div></div>')()
|
||||||
prepend(
|
prepend(
|
||||||
n0 as any as ParentNode,
|
n0 as any as ParentNode,
|
||||||
createSlot(
|
createSlot(
|
||||||
() => 'header', // dynamic slot outlet name
|
() => val.value, // dynamic slot outlet name
|
||||||
[{ title: () => 'header' }],
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return n0
|
return n0
|
||||||
})
|
})
|
||||||
|
|
||||||
const { host } = define(() => {
|
const { host } = define(() => {
|
||||||
return createComponent(
|
return createComponent(Comp, null, {
|
||||||
Comp,
|
header: () => template('header')(),
|
||||||
{},
|
footer: () => template('footer')(),
|
||||||
{ header: props => template(props.title)() },
|
})
|
||||||
)
|
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
|
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
|
||||||
|
|
||||||
|
val.value = 'footer'
|
||||||
|
await nextTick()
|
||||||
|
expect(host.innerHTML).toBe('<div>footer<!--slot--></div>')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fallback should be render correctly', () => {
|
test('fallback should be render correctly', () => {
|
||||||
const Comp = defineComponent(() => {
|
const Comp = defineVaporComponent(() => {
|
||||||
const n0 = template('<div></div>')()
|
const n0 = template('<div></div>')()
|
||||||
insert(
|
insert(
|
||||||
createSlot('header', undefined, () => template('fallback')()),
|
createSlot('header', undefined, () => template('fallback')()),
|
||||||
|
@ -595,24 +325,26 @@ describe.todo('component: slots', () => {
|
||||||
test('dynamic slot should be updated correctly', async () => {
|
test('dynamic slot should be updated correctly', async () => {
|
||||||
const flag1 = ref(true)
|
const flag1 = ref(true)
|
||||||
|
|
||||||
const Child = defineComponent(() => {
|
const Child = defineVaporComponent(() => {
|
||||||
const temp0 = template('<p></p>')
|
const temp0 = template('<p></p>')
|
||||||
const el0 = temp0()
|
const el0 = temp0()
|
||||||
const el1 = temp0()
|
const el1 = temp0()
|
||||||
const slot1 = createSlot('one', [], () => template('one fallback')())
|
const slot1 = createSlot('one', null, () => template('one fallback')())
|
||||||
const slot2 = createSlot('two', [], () => template('two fallback')())
|
const slot2 = createSlot('two', null, () => template('two fallback')())
|
||||||
insert(slot1, el0 as any as ParentNode)
|
insert(slot1, el0 as any as ParentNode)
|
||||||
insert(slot2, el1 as any as ParentNode)
|
insert(slot2, el1 as any as ParentNode)
|
||||||
return [el0, el1]
|
return [el0, el1]
|
||||||
})
|
})
|
||||||
|
|
||||||
const { host } = define(() => {
|
const { host } = define(() => {
|
||||||
return createComponent(Child, {}, [
|
return createComponent(Child, null, {
|
||||||
() =>
|
$: [
|
||||||
flag1.value
|
() =>
|
||||||
? { name: 'one', fn: () => template('one content')() }
|
flag1.value
|
||||||
: { name: 'two', fn: () => template('two content')() },
|
? { name: 'one', fn: () => template('one content')() }
|
||||||
])
|
: { name: 'two', fn: () => template('two content')() },
|
||||||
|
],
|
||||||
|
})
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe(
|
expect(host.innerHTML).toBe(
|
||||||
|
@ -637,7 +369,7 @@ describe.todo('component: slots', () => {
|
||||||
test('dynamic slot outlet should be updated correctly', async () => {
|
test('dynamic slot outlet should be updated correctly', async () => {
|
||||||
const slotOutletName = ref('one')
|
const slotOutletName = ref('one')
|
||||||
|
|
||||||
const Child = defineComponent(() => {
|
const Child = defineVaporComponent(() => {
|
||||||
const temp0 = template('<p>')
|
const temp0 = template('<p>')
|
||||||
const el0 = temp0()
|
const el0 = temp0()
|
||||||
const slot1 = createSlot(
|
const slot1 = createSlot(
|
||||||
|
@ -674,7 +406,7 @@ describe.todo('component: slots', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('non-exist slot', async () => {
|
test('non-exist slot', async () => {
|
||||||
const Child = defineComponent(() => {
|
const Child = defineVaporComponent(() => {
|
||||||
const el0 = template('<p>')()
|
const el0 = template('<p>')()
|
||||||
const slot = createSlot('not-exist', undefined)
|
const slot = createSlot('not-exist', undefined)
|
||||||
insert(slot, el0 as any as ParentNode)
|
insert(slot, el0 as any as ParentNode)
|
||||||
|
@ -685,7 +417,7 @@ describe.todo('component: slots', () => {
|
||||||
return createComponent(Child)
|
return createComponent(Child)
|
||||||
}).render()
|
}).render()
|
||||||
|
|
||||||
expect(host.innerHTML).toBe('<p></p>')
|
expect(host.innerHTML).toBe('<p><!--slot--></p>')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,7 +8,12 @@ import {
|
||||||
import { createComment } from './dom/node'
|
import { createComment } from './dom/node'
|
||||||
import { EffectScope } from '@vue/reactivity'
|
import { EffectScope } from '@vue/reactivity'
|
||||||
|
|
||||||
export type Block = Node | Fragment | VaporComponentInstance | Block[]
|
export type Block =
|
||||||
|
| Node
|
||||||
|
| Fragment
|
||||||
|
| DynamicFragment
|
||||||
|
| VaporComponentInstance
|
||||||
|
| Block[]
|
||||||
|
|
||||||
export type BlockFn = (...args: any[]) => Block
|
export type BlockFn = (...args: any[]) => Block
|
||||||
|
|
||||||
|
@ -45,13 +50,12 @@ export class DynamicFragment extends Fragment {
|
||||||
if (this.scope) {
|
if (this.scope) {
|
||||||
this.scope.stop()
|
this.scope.stop()
|
||||||
parent && remove(this.nodes, parent)
|
parent && remove(this.nodes, parent)
|
||||||
// TODO lifecycle unmount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (render) {
|
if (render) {
|
||||||
this.scope = new EffectScope()
|
this.scope = new EffectScope()
|
||||||
this.nodes = this.scope.run(render) || []
|
this.nodes = this.scope.run(render) || []
|
||||||
if (parent) insert(this.nodes, parent)
|
if (parent) insert(this.nodes, parent, this.anchor)
|
||||||
} else {
|
} else {
|
||||||
this.scope = undefined
|
this.scope = undefined
|
||||||
this.nodes = []
|
this.nodes = []
|
||||||
|
@ -99,10 +103,11 @@ export function isValidBlock(block: Block): boolean {
|
||||||
export function insert(
|
export function insert(
|
||||||
block: Block,
|
block: Block,
|
||||||
parent: ParentNode,
|
parent: ParentNode,
|
||||||
anchor: Node | null | 0 = null,
|
anchor: Node | null | 0 = null, // 0 means prepend
|
||||||
): void {
|
): void {
|
||||||
|
anchor = anchor === 0 ? parent.firstChild : anchor
|
||||||
if (block instanceof Node) {
|
if (block instanceof Node) {
|
||||||
parent.insertBefore(block, anchor === 0 ? parent.firstChild : anchor)
|
parent.insertBefore(block, anchor)
|
||||||
} else if (isVaporComponent(block)) {
|
} else if (isVaporComponent(block)) {
|
||||||
mountComponent(block, parent, anchor)
|
mountComponent(block, parent, anchor)
|
||||||
} else if (isArray(block)) {
|
} else if (isArray(block)) {
|
||||||
|
@ -134,5 +139,8 @@ export function remove(block: Block, parent: ParentNode): void {
|
||||||
// fragment
|
// fragment
|
||||||
remove(block.nodes, parent)
|
remove(block.nodes, parent)
|
||||||
if (block.anchor) remove(block.anchor, parent)
|
if (block.anchor) remove(block.anchor, parent)
|
||||||
|
if ((block as DynamicFragment).scope) {
|
||||||
|
;(block as DynamicFragment).scope!.stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,10 @@ interface SharedInternalOptions {
|
||||||
// 100% strict. Here we use intentionally wider types to make `createComponent`
|
// 100% strict. Here we use intentionally wider types to make `createComponent`
|
||||||
// more ergonomic in tests and internal call sites, where we immediately cast
|
// more ergonomic in tests and internal call sites, where we immediately cast
|
||||||
// them into the stricter types.
|
// them into the stricter types.
|
||||||
type LooseRawProps = Record<string, (() => unknown) | DynamicPropsSource[]> & {
|
export type LooseRawProps = Record<
|
||||||
|
string,
|
||||||
|
(() => unknown) | DynamicPropsSource[]
|
||||||
|
> & {
|
||||||
$?: DynamicPropsSource[]
|
$?: DynamicPropsSource[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,7 @@ export function getPropsProxyHandlers(
|
||||||
? ({
|
? ({
|
||||||
get: (target, key) => getProp(target, key),
|
get: (target, key) => getProp(target, key),
|
||||||
has: (_, key) => isProp(key),
|
has: (_, key) => isProp(key),
|
||||||
|
ownKeys: () => Object.keys(propsOptions),
|
||||||
getOwnPropertyDescriptor(target, key) {
|
getOwnPropertyDescriptor(target, key) {
|
||||||
if (isProp(key)) {
|
if (isProp(key)) {
|
||||||
return {
|
return {
|
||||||
|
@ -119,7 +120,6 @@ export function getPropsProxyHandlers(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ownKeys: () => Object.keys(propsOptions),
|
|
||||||
} satisfies ProxyHandler<VaporComponentInstance>)
|
} satisfies ProxyHandler<VaporComponentInstance>)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
@ -147,6 +147,7 @@ export function getPropsProxyHandlers(
|
||||||
const attrsHandlers = {
|
const attrsHandlers = {
|
||||||
get: (target, key: string) => getAttr(target.rawProps, key),
|
get: (target, key: string) => getAttr(target.rawProps, key),
|
||||||
has: (target, key: string) => hasAttr(target.rawProps, key),
|
has: (target, key: string) => hasAttr(target.rawProps, key),
|
||||||
|
ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
|
||||||
getOwnPropertyDescriptor(target, key: string) {
|
getOwnPropertyDescriptor(target, key: string) {
|
||||||
if (hasAttr(target.rawProps, key)) {
|
if (hasAttr(target.rawProps, key)) {
|
||||||
return {
|
return {
|
||||||
|
@ -156,25 +157,6 @@ export function getPropsProxyHandlers(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ownKeys(target) {
|
|
||||||
const rawProps = target.rawProps
|
|
||||||
const keys: string[] = []
|
|
||||||
for (const key in rawProps) {
|
|
||||||
if (isAttr(key)) keys.push(key)
|
|
||||||
}
|
|
||||||
const dynamicSources = rawProps.$
|
|
||||||
if (dynamicSources) {
|
|
||||||
let i = dynamicSources.length
|
|
||||||
let source
|
|
||||||
while (i--) {
|
|
||||||
source = resolveSource(dynamicSources[i])
|
|
||||||
for (const key in source) {
|
|
||||||
if (isAttr(key)) keys.push(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(new Set(keys))
|
|
||||||
},
|
|
||||||
} satisfies ProxyHandler<VaporComponentInstance>
|
} satisfies ProxyHandler<VaporComponentInstance>
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
@ -221,6 +203,25 @@ export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
|
||||||
return hasOwn(rawProps, key)
|
return hasOwn(rawProps, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getKeysFromRawProps(rawProps: RawProps): string[] {
|
||||||
|
const keys: string[] = []
|
||||||
|
for (const key in rawProps) {
|
||||||
|
if (key !== '$') keys.push(key)
|
||||||
|
}
|
||||||
|
const dynamicSources = rawProps.$
|
||||||
|
if (dynamicSources) {
|
||||||
|
let i = dynamicSources.length
|
||||||
|
let source
|
||||||
|
while (i--) {
|
||||||
|
source = resolveSource(dynamicSources[i])
|
||||||
|
for (const key in source) {
|
||||||
|
keys.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(new Set(keys))
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizePropsOptions(
|
export function normalizePropsOptions(
|
||||||
comp: VaporComponent,
|
comp: VaporComponent,
|
||||||
): NormalizedPropsOptions {
|
): NormalizedPropsOptions {
|
||||||
|
|
|
@ -3,10 +3,11 @@ import { type Block, type BlockFn, DynamicFragment } from './block'
|
||||||
import {
|
import {
|
||||||
type RawProps,
|
type RawProps,
|
||||||
getAttrFromRawProps,
|
getAttrFromRawProps,
|
||||||
|
getKeysFromRawProps,
|
||||||
hasAttrFromRawProps,
|
hasAttrFromRawProps,
|
||||||
} from './componentProps'
|
} from './componentProps'
|
||||||
import { currentInstance } from '@vue/runtime-core'
|
import { currentInstance } from '@vue/runtime-core'
|
||||||
import type { VaporComponentInstance } from './component'
|
import type { LooseRawProps, VaporComponentInstance } from './component'
|
||||||
import { renderEffect } from './renderEffect'
|
import { renderEffect } from './renderEffect'
|
||||||
|
|
||||||
export type RawSlots = Record<string, Slot> & {
|
export type RawSlots = Record<string, Slot> & {
|
||||||
|
@ -86,7 +87,16 @@ export function getSlot(target: RawSlots, key: string): Slot | undefined {
|
||||||
const dynamicSlotsPropsProxyHandlers: ProxyHandler<RawProps> = {
|
const dynamicSlotsPropsProxyHandlers: ProxyHandler<RawProps> = {
|
||||||
get: getAttrFromRawProps,
|
get: getAttrFromRawProps,
|
||||||
has: hasAttrFromRawProps,
|
has: hasAttrFromRawProps,
|
||||||
ownKeys: target => Object.keys(target).filter(k => k !== '$'),
|
ownKeys: getKeysFromRawProps,
|
||||||
|
getOwnPropertyDescriptor(target, key: string) {
|
||||||
|
if (hasAttrFromRawProps(target, key)) {
|
||||||
|
return {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
get: () => getAttrFromRawProps(target, key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO how to handle empty slot return blocks?
|
// TODO how to handle empty slot return blocks?
|
||||||
|
@ -95,11 +105,11 @@ const dynamicSlotsPropsProxyHandlers: ProxyHandler<RawProps> = {
|
||||||
// and make the v-if use it as fallback
|
// and make the v-if use it as fallback
|
||||||
export function createSlot(
|
export function createSlot(
|
||||||
name: string | (() => string),
|
name: string | (() => string),
|
||||||
rawProps?: RawProps,
|
rawProps?: LooseRawProps | null,
|
||||||
fallback?: Slot,
|
fallback?: Slot,
|
||||||
): Block {
|
): Block {
|
||||||
|
const instance = currentInstance as VaporComponentInstance
|
||||||
const fragment = new DynamicFragment('slot')
|
const fragment = new DynamicFragment('slot')
|
||||||
const rawSlots = (currentInstance as VaporComponentInstance)!.rawSlots
|
|
||||||
const slotProps = rawProps
|
const slotProps = rawProps
|
||||||
? new Proxy(rawProps, dynamicSlotsPropsProxyHandlers)
|
? new Proxy(rawProps, dynamicSlotsPropsProxyHandlers)
|
||||||
: EMPTY_OBJ
|
: EMPTY_OBJ
|
||||||
|
@ -107,7 +117,7 @@ export function createSlot(
|
||||||
// always create effect because a slot may contain dynamic root inside
|
// always create effect because a slot may contain dynamic root inside
|
||||||
// which affects fallback
|
// which affects fallback
|
||||||
renderEffect(() => {
|
renderEffect(() => {
|
||||||
const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
|
const slot = getSlot(instance.rawSlots, isFunction(name) ? name() : name)
|
||||||
if (slot) {
|
if (slot) {
|
||||||
fragment.update(
|
fragment.update(
|
||||||
() => slot(slotProps) || (fallback && fallback()),
|
() => slot(slotProps) || (fallback && fallback()),
|
||||||
|
|
Loading…
Reference in New Issue