Skip to content

feat(runtime-vapor): slot props #227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 141 additions & 16 deletions packages/runtime-vapor/__tests__/componentSlots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
renderEffect,
setText,
template,
withDestructure,
} from '../src'
import { makeRender } from './_utils'

Expand Down Expand Up @@ -319,7 +320,7 @@ describe('component: slots', () => {
const Comp = defineComponent(() => {
const n0 = template('<div></div>')()
insert(
createSlot('header', { title: () => 'header' }),
createSlot('header', [{ title: () => 'header' }]),
n0 as any as ParentNode,
)
return n0
Expand All @@ -330,26 +331,150 @@ describe('component: slots', () => {
Comp,
{},
{
header: ({ title }) => {
const el = template('<h1></h1>')()
renderEffect(() => {
setText(el, title())
})
return el
},
header: withDestructure(
({ title }) => [title],
ctx => {
const el = template('<h1></h1>')()
renderEffect(() => {
setText(el, ctx[0])
})
return el
},
),
},
)
}).render()

expect(host.innerHTML).toBe('<div><h1>header</h1></div>')
})

test('dynamic slot props', async () => {
let props: any

const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
const Comp = defineComponent(() =>
createSlot('default', [() => bindObj.value]),
)
define(() =>
createComponent(
Comp,
{},
{ default: _props => ((props = _props), []) },
),
).render()

expect(props).toEqual({ foo: 1, baz: 'qux' })

bindObj.value.foo = 2
await nextTick()
expect(props).toEqual({ foo: 2, baz: 'qux' })

delete bindObj.value.baz
await nextTick()
expect(props).toEqual({ foo: 2 })
})

test('dynamic slot props with static slot props', async () => {
let props: any

const foo = ref(0)
const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
const Comp = defineComponent(() =>
createSlot('default', [{ foo: () => foo.value }, () => bindObj.value]),
)
define(() =>
createComponent(
Comp,
{},
{ default: _props => ((props = _props), []) },
),
).render()

expect(props).toEqual({ foo: 100, baz: 'qux' })

foo.value = 2
await nextTick()
expect(props).toEqual({ foo: 100, baz: 'qux' })

delete bindObj.value.foo
await nextTick()
expect(props).toEqual({ foo: 2, baz: 'qux' })
})

test('slot class binding should be merged', async () => {
let props: any

const className = ref('foo')
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>')()
prepend(
n0 as any as ParentNode,
createSlot('header', { title: () => 'header' }),
createSlot('header', [{ title: () => 'header' }]),
)
return n0
})
Expand All @@ -359,7 +484,7 @@ describe('component: slots', () => {
return createComponent(Comp, {}, {}, [
() => ({
name: 'header',
fn: ({ title }) => template(`${title()}`)(),
fn: props => template(props.title)(),
}),
])
}).render()
Expand All @@ -374,7 +499,7 @@ describe('component: slots', () => {
n0 as any as ParentNode,
createSlot(
() => 'header', // dynamic slot outlet name
{ title: () => 'header' },
[{ title: () => 'header' }],
),
)
return n0
Expand All @@ -384,7 +509,7 @@ describe('component: slots', () => {
return createComponent(
Comp,
{},
{ header: ({ title }) => template(`${title()}`)() },
{ header: props => template(props.title)() },
)
}).render()

Expand All @@ -395,7 +520,7 @@ describe('component: slots', () => {
const Comp = defineComponent(() => {
const n0 = template('<div></div>')()
insert(
createSlot('header', {}, () => template('fallback')()),
createSlot('header', undefined, () => template('fallback')()),
n0 as any as ParentNode,
)
return n0
Expand All @@ -415,8 +540,8 @@ describe('component: slots', () => {
const temp0 = template('<p></p>')
const el0 = temp0()
const el1 = temp0()
const slot1 = createSlot('one', {}, () => template('one fallback')())
const slot2 = createSlot('two', {}, () => template('two fallback')())
const slot1 = createSlot('one', [], () => template('one fallback')())
const slot2 = createSlot('two', [], () => template('two fallback')())
insert(slot1, el0 as any as ParentNode)
insert(slot2, el1 as any as ParentNode)
return [el0, el1]
Expand Down Expand Up @@ -458,7 +583,7 @@ describe('component: slots', () => {
const el0 = temp0()
const slot1 = createSlot(
() => slotOutletName.value,
{},
undefined,
() => template('fallback')(),
)
insert(slot1, el0 as any as ParentNode)
Expand Down
67 changes: 64 additions & 3 deletions packages/runtime-vapor/src/componentSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { type Block, type Fragment, fragmentKey } from './apiRender'
import { firstEffect, renderEffect } from './renderEffect'
import { createComment, createTextNode, insert, remove } from './dom/element'
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { NormalizedRawProps } from './componentProps'
import type { Data } from '@vue/runtime-shared'
import { mergeProps } from './dom/prop'

// TODO: SSR

Expand Down Expand Up @@ -106,7 +109,7 @@ export function initSlots(

export function createSlot(
name: string | (() => string),
binds?: Record<string, (() => unknown) | undefined>,
binds?: NormalizedRawProps,
fallback?: () => Block,
): Block {
let block: Block | undefined
Expand All @@ -120,7 +123,7 @@ export function createSlot(

// When not using dynamic slots, simplify the process to improve performance
if (!isDynamicName && !isReactive(slots)) {
if ((branch = slots[name] || fallback)) {
if ((branch = withProps(slots[name]) || fallback)) {
return branch(binds)
} else {
return []
Expand All @@ -137,7 +140,7 @@ export function createSlot(

// TODO lifecycle hooks
renderEffect(() => {
if ((branch = getSlot() || fallback) !== oldBranch) {
if ((branch = withProps(getSlot()) || fallback) !== oldBranch) {
parent ||= anchor.parentNode
if (block) {
scope!.stop()
Expand All @@ -155,4 +158,62 @@ export function createSlot(
})

return fragment

function withProps<T extends (p: any) => any>(fn?: T) {
if (fn)
return (binds?: NormalizedRawProps): ReturnType<T> =>
fn(binds && normalizeSlotProps(binds))
}
}

function normalizeSlotProps(rawPropsList: NormalizedRawProps) {
const { length } = rawPropsList
const mergings = length > 1 ? shallowReactive<Data[]>([]) : undefined
const result = shallowReactive<Data>({})

for (let i = 0; i < length; i++) {
const rawProps = rawPropsList[i]
if (isFunction(rawProps)) {
// dynamic props
renderEffect(() => {
const props = rawProps()
if (mergings) {
mergings[i] = props
} else {
setDynamicProps(props)
}
})
} else {
// static props
const props = mergings
? (mergings[i] = shallowReactive<Data>({}))
: result
for (const key in rawProps) {
const valueSource = rawProps[key]
renderEffect(() => {
props[key] = valueSource()
})
}
}
}

if (mergings) {
renderEffect(() => {
setDynamicProps(mergeProps(...mergings))
})
}

return result

function setDynamicProps(props: Data) {
const otherExistingKeys = new Set(Object.keys(result))
for (const key in props) {
result[key] = props[key]
otherExistingKeys.delete(key)
}
// delete other stale props
for (const key of otherExistingKeys) {
delete result[key]
}
}
}
2 changes: 1 addition & 1 deletion packages/runtime-vapor/src/dom/prop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export function setDynamicProps(el: Element, ...args: any) {
}

// TODO copied from runtime-core
function mergeProps(...args: Data[]) {
export function mergeProps(...args: Data[]) {
const ret: Data = {}
for (let i = 0; i < args.length; i++) {
const toMerge = args[i]
Expand Down
Loading