Skip to content

Commit

Permalink
feat(runtime-vapor): component slot (#143)
Browse files Browse the repository at this point in the history
Co-authored-by: 三咲智子 Kevin Deng <[email protected]>
  • Loading branch information
ubugeeei and sxzz authored Mar 24, 2024
1 parent bd888b9 commit 78f74ce
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/compiler-vapor/src/generators/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { genExpression } from './expression'
import { genPropKey } from './prop'

// TODO: generate component slots
export function genCreateComponent(
oper: CreateComponentIRNode,
context: CodegenContext,
Expand Down
1 change: 0 additions & 1 deletion packages/runtime-vapor/__tests__/apiInject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
createComponent,
createTextNode,
createVaporApp,
getCurrentInstance,
hasInjectionContext,
inject,
nextTick,
Expand Down
8 changes: 8 additions & 0 deletions packages/runtime-vapor/__tests__/componentAttrs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ describe('attribute fallthrough', () => {
id: () => _ctx.id,
},
],
null,
null,
true,
)
},
Expand Down Expand Up @@ -85,6 +87,8 @@ describe('attribute fallthrough', () => {
id: () => _ctx.id,
},
],
null,
null,
true,
)
},
Expand Down Expand Up @@ -123,6 +127,8 @@ describe('attribute fallthrough', () => {
'custom-attr': () => 'custom-attr',
},
],
null,
null,
true,
)
return n0
Expand All @@ -144,6 +150,8 @@ describe('attribute fallthrough', () => {
id: () => _ctx.id,
},
],
null,
null,
true,
)
},
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime-vapor/__tests__/componentProps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ describe('component: props', () => {
foo: () => _ctx.foo,
id: () => _ctx.id,
},
null,
null,
true,
)
},
Expand Down
191 changes: 191 additions & 0 deletions packages/runtime-vapor/__tests__/componentSlots.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.

import {
createComponent,
createVaporApp,
defineComponent,
getCurrentInstance,
nextTick,
ref,
template,
} from '../src'
import { makeRender } from './_utils'

const define = makeRender<any>()
function renderWithSlots(slots: any): any {
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, {}, slots)
},
})

render()
return instance
}

describe('component: slots', () => {
test('initSlots: instance.slots should be set correctly', () => {
const { slots } = renderWithSlots({ _: 1 })
expect(slots).toMatchObject({ _: 1 })
})

// 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)',
() => {},
)

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'),
)
})

// 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', () => {
const { slots } = renderWithSlots({
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'))
})

test('updateSlots: instance.slots should be updated correctly', async () => {
const flag1 = ref(true)

let instance: any
const Child = () => {
instance = getCurrentInstance()
return template('child')()
}

const { render } = define({
render() {
return createComponent(Child, {}, { _: 2 as any }, () => [
flag1.value
? { name: 'one', fn: () => template('<span></span>')() }
: { name: 'two', fn: () => template('<div></div>')() },
])
},
})

render()

expect(instance.slots).toHaveProperty('one')
expect(instance.slots).not.toHaveProperty('two')

flag1.value = false
await nextTick()

expect(instance.slots).not.toHaveProperty('one')
expect(instance.slots).toHaveProperty('two')
})

// NOTE: it is not supported
// 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.todo('should respect $stable flag', async () => {
// TODO: $stable flag?
})

test.todo('should not warn when mounting another app in setup', () => {
// TODO: warning
const Comp = defineComponent({
render() {
const i = getCurrentInstance()
return i!.slots.default!()
},
})
const mountComp = () => {
createVaporApp({
render() {
return createComponent(
Comp,
{},
{ default: () => template('msg')() },
)!
},
})
}
const App = {
setup() {
mountComp()
},
render() {
return null!
},
}
createVaporApp(App).mount(document.createElement('div'))
expect(
'Slot "default" invoked outside of the render function',
).not.toHaveBeenWarned()
})
})
5 changes: 5 additions & 0 deletions packages/runtime-vapor/src/apiCreateComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import {
} from './component'
import { setupComponent } from './apiRender'
import type { RawProps } from './componentProps'
import type { DynamicSlots, Slots } from './componentSlots'
import { withAttrs } from './componentAttrs'

export function createComponent(
comp: Component,
rawProps: RawProps | null = null,
slots: Slots | null = null,
dynamicSlots: DynamicSlots | null = null,
singleRoot: boolean = false,
) {
const current = currentInstance!
const instance = createComponentInstance(
comp,
singleRoot ? withAttrs(rawProps) : rawProps,
slots,
dynamicSlots,
)
setupComponent(instance, singleRoot)

Expand Down
8 changes: 7 additions & 1 deletion packages/runtime-vapor/src/apiCreateVaporApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ export function createVaporApp(

mount(rootContainer): any {
if (!instance) {
instance = createComponentInstance(rootComponent, rootProps, context)
instance = createComponentInstance(
rootComponent,
rootProps,
null,
null,
context,
)
setupComponent(instance)
render(instance, rootContainer)
return instance
Expand Down
36 changes: 32 additions & 4 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import {
emit,
normalizeEmitsOptions,
} from './componentEmits'
import {
type DynamicSlots,
type InternalSlots,
type Slots,
initSlots,
} from './componentSlots'
import { VaporLifecycleHooks } from './apiLifecycle'
import { warn } from './warning'
import { type AppContext, createAppContext } from './apiCreateVaporApp'
Expand All @@ -32,7 +38,7 @@ export type SetupContext<E = EmitsOptions> = E extends any
attrs: Data
emit: EmitFn<E>
expose: (exposed?: Record<string, any>) => void
// TODO slots
slots: Readonly<InternalSlots>
}
: never

Expand All @@ -46,6 +52,9 @@ export function createSetupContext(
get attrs() {
return getAttrsProxy(instance)
},
get slots() {
return getSlotsProxy(instance)
},
get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args)
},
Expand All @@ -57,6 +66,7 @@ export function createSetupContext(
return getAttrsProxy(instance)
},
emit: instance.emit,
slots: instance.slots,
expose: NOOP,
}
}
Expand Down Expand Up @@ -102,9 +112,11 @@ export interface ComponentInternalInstance {
emit: EmitFn
emitted: Record<string, boolean> | null
attrs: Data
slots: InternalSlots
refs: Data

attrsProxy: Data | null
attrsProxy?: Data
slotsProxy?: Slots

// lifecycle
isMounted: boolean
Expand Down Expand Up @@ -188,6 +200,8 @@ let uid = 0
export function createComponentInstance(
component: ObjectComponent | FunctionalComponent,
rawProps: RawProps | null,
slots: Slots | null = null,
dynamicSlots: DynamicSlots | null = null,
// application root node only
appContext: AppContext | null = null,
): ComponentInternalInstance {
Expand Down Expand Up @@ -224,10 +238,9 @@ export function createComponentInstance(
emit: null!,
emitted: null,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,

attrsProxy: null,

// lifecycle
isMounted: false,
isUnmounted: false,
Expand Down Expand Up @@ -283,6 +296,7 @@ export function createComponentInstance(
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
}
initProps(instance, rawProps, !isFunction(component))
initSlots(instance, slots, dynamicSlots)
instance.emit = emit.bind(null, instance)

return instance
Expand Down Expand Up @@ -315,3 +329,17 @@ function getAttrsProxy(instance: ComponentInternalInstance): Data {
))
)
}

/**
* Dev-only
*/
function getSlotsProxy(instance: ComponentInternalInstance): Slots {
return (
instance.slotsProxy ||
(instance.slotsProxy = new Proxy(instance.slots, {
get(target, key: string) {
return target[key]
},
}))
)
}
Loading

0 comments on commit 78f74ce

Please sign in to comment.