Skip to content
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

feat: add defineSlots macro and slots option #7982

Merged
merged 14 commits into from Apr 3, 2023
Expand Up @@ -1785,6 +1785,51 @@ return { props, emit }
})"
`;

exports[`SFC compile <script setup> > with TypeScript > defineSlots() > basic usage 1`] = `
"import { useSlots as _useSlots, defineComponent as _defineComponent } from 'vue'

export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();

const slots = _useSlots()

return { slots }
}

})"
`;

exports[`SFC compile <script setup> > with TypeScript > defineSlots() > w/o generic params 1`] = `
"import { useSlots as _useSlots } from 'vue'

export default {
setup(__props, { expose: __expose }) {
__expose();

const slots = _useSlots()

return { slots }
}

}"
`;

exports[`SFC compile <script setup> > with TypeScript > defineSlots() > w/o return value 1`] = `
"import { defineComponent as _defineComponent } from 'vue'

export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();



return { }
}

})"
`;

exports[`SFC compile <script setup> > with TypeScript > hoist type declarations 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export interface Foo {}
Expand Down
39 changes: 39 additions & 0 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Expand Up @@ -1585,6 +1585,45 @@ const emit = defineEmits(['a', 'b'])
assertCode(content)
})

describe('defineSlots()', () => {
test('basic usage', () => {
const { content } = compile(`
<script setup lang="ts">
const slots = defineSlots<{
default: { msg: string }
}>()
</script>
`)
assertCode(content)
expect(content).toMatch(`const slots = _useSlots()`)
expect(content).not.toMatch('defineSlots')
})

test('w/o return value', () => {
const { content } = compile(`
<script setup lang="ts">
defineSlots<{
default: { msg: string }
}>()
</script>
`)
assertCode(content)
expect(content).not.toMatch('defineSlots')
expect(content).not.toMatch(`_useSlots`)
})

test('w/o generic params', () => {
const { content } = compile(`
<script setup>
const slots = defineSlots()
</script>
`)
assertCode(content)
expect(content).toMatch(`const slots = _useSlots()`)
expect(content).not.toMatch('defineSlots')
})
})

test('runtime Enum', () => {
const { content, bindings } = compile(
`<script setup lang="ts">
Expand Down
42 changes: 40 additions & 2 deletions packages/compiler-sfc/src/compileScript.ts
Expand Up @@ -67,6 +67,7 @@ const DEFINE_EMITS = 'defineEmits'
const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults'
const DEFINE_OPTIONS = 'defineOptions'
const DEFINE_SLOTS = 'defineSlots'

const isBuiltInDir = makeMap(
`once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
Expand Down Expand Up @@ -312,6 +313,7 @@ export function compileScript(
let hasDefaultExportName = false
let hasDefaultExportRender = false
let hasDefineOptionsCall = false
let hasDefineSlotsCall = false
let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: Node | undefined
let propsDestructureDecl: Node | undefined
Expand Down Expand Up @@ -590,6 +592,30 @@ export function compileScript(
return true
}

function processDefineSlots(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_SLOTS)) {
return false
}
if (hasDefineSlotsCall) {
error(`duplicate ${DEFINE_SLOTS}() call`, node)
}
hasDefineSlotsCall = true

if (node.arguments.length > 0) {
error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
}

if (declId) {
s.overwrite(
startOffset + node.start!,
startOffset + node.end!,
`${helper('useSlots')}()`
)
}

return true
}

function getAstBody(): Statement[] {
return scriptAst
? [...scriptSetupAst.body, ...scriptAst.body]
Expand Down Expand Up @@ -683,6 +709,7 @@ export function compileScript(
let propsOption = undefined
let emitsOption = undefined
let exposeOption = undefined
let slotsOption = undefined
if (optionsRuntimeDecl.type === 'ObjectExpression') {
for (const prop of optionsRuntimeDecl.properties) {
if (
Expand All @@ -692,6 +719,7 @@ export function compileScript(
if (prop.key.name === 'props') propsOption = prop
if (prop.key.name === 'emits') emitsOption = prop
if (prop.key.name === 'expose') exposeOption = prop
if (prop.key.name === 'slots') slotsOption = prop
}
}
}
Expand All @@ -714,6 +742,12 @@ export function compileScript(
exposeOption
)
}
if (slotsOption) {
error(
`${DEFINE_OPTIONS}() cannot be used to declare slots. Use ${DEFINE_SLOTS}() instead.`,
slotsOption
)
}

return true
}
Expand Down Expand Up @@ -1286,7 +1320,8 @@ export function compileScript(
processDefineProps(expr) ||
processDefineEmits(expr) ||
processDefineOptions(expr) ||
processWithDefaults(expr)
processWithDefaults(expr) ||
processDefineSlots(expr)
) {
s.remove(node.start! + startOffset, node.end! + startOffset)
} else if (processDefineExpose(expr)) {
Expand Down Expand Up @@ -1320,7 +1355,10 @@ export function compileScript(
const isDefineProps =
processDefineProps(init, decl.id) ||
processWithDefaults(init, decl.id)
const isDefineEmits = processDefineEmits(init, decl.id)
const isDefineEmits =
!isDefineProps && processDefineEmits(init, decl.id)
!isDefineEmits && processDefineSlots(init, decl.id)

if (isDefineProps || isDefineEmits) {
if (left === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset)
Expand Down
69 changes: 68 additions & 1 deletion packages/dts-test/defineComponent.test-d.tsx
Expand Up @@ -8,7 +8,10 @@ import {
ComponentPublicInstance,
ComponentOptions,
SetupContext,
h
h,
SlotsType,
Slots,
VNode
} from 'vue'
import { describe, expectType, IsUnion } from './utils'

Expand Down Expand Up @@ -1406,6 +1409,69 @@ export default {
})
}

describe('slots', () => {
const comp1 = defineComponent({
slots: Object as SlotsType<{
default: { foo: string; bar: number }
optional?: { data: string }
undefinedScope: undefined | { data: string }
optionalUndefinedScope?: undefined | { data: string }
}>,
setup(props, { slots }) {
expectType<(scope: { foo: string; bar: number }) => VNode[]>(
slots.default
)
expectType<((scope: { data: string }) => VNode[]) | undefined>(
slots.optional
)

slots.default({ foo: 'foo', bar: 1 })

// @ts-expect-error it's optional
slots.optional({ data: 'foo' })
slots.optional?.({ data: 'foo' })

expectType<{
(): VNode[]
(scope: undefined | { data: string }): VNode[]
}>(slots.undefinedScope)

expectType<
| { (): VNode[]; (scope: undefined | { data: string }): VNode[] }
| undefined
>(slots.optionalUndefinedScope)

slots.default({ foo: 'foo', bar: 1 })
// @ts-expect-error it's optional
slots.optional({ data: 'foo' })
slots.optional?.({ data: 'foo' })
slots.undefinedScope()
slots.undefinedScope(undefined)
// @ts-expect-error
slots.undefinedScope('foo')

slots.optionalUndefinedScope?.()
slots.optionalUndefinedScope?.(undefined)
slots.optionalUndefinedScope?.({ data: 'foo' })
// @ts-expect-error
slots.optionalUndefinedScope()
// @ts-expect-error
slots.optionalUndefinedScope?.('foo')

expectType<typeof slots | undefined>(new comp1().$slots)
}
})

const comp2 = defineComponent({
setup(props, { slots }) {
// unknown slots
expectType<Slots>(slots)
expectType<((...args: any[]) => VNode[]) | undefined>(slots.default)
}
})
expectType<Slots | undefined>(new comp2().$slots)
})

import {
DefineComponent,
ComponentOptionsMixin,
Expand All @@ -1428,6 +1494,7 @@ declare const MyButton: DefineComponent<
ComponentOptionsMixin,
EmitsOptions,
string,
{},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed this part when I was reviewing - we cannot actually change the type arguments order of DefineComponent because it's a publicly-exported type and used in library type definitions.

VNodeProps & AllowedComponentProps & ComponentCustomProps,
Readonly<ExtractPropTypes<{}>>,
{}
Expand Down
28 changes: 27 additions & 1 deletion packages/dts-test/functionalComponent.test-d.tsx
@@ -1,4 +1,4 @@
import { h, Text, FunctionalComponent, Component } from 'vue'
import { h, Text, FunctionalComponent, Component, VNode } from 'vue'
import { expectType } from './utils'

// simple function signature
Expand Down Expand Up @@ -68,3 +68,29 @@ const Qux: FunctionalComponent<{}, ['foo', 'bar']> = (props, { emit }) => {
}

expectType<Component>(Qux)

const Quux: FunctionalComponent<
{},
{},
{
default: { foo: number }
optional?: { foo: number }
}
> = (props, { emit, slots }) => {
expectType<{
default: (scope: { foo: number }) => VNode[]
optional?: (scope: { foo: number }) => VNode[]
}>(slots)

slots.default({ foo: 123 })
// @ts-expect-error
slots.default({ foo: 'fesf' })

slots.optional?.({ foo: 123 })
// @ts-expect-error
slots.optional?.({ foo: 'fesf' })
// @ts-expect-error
slots.optional({ foo: 123 })
}
expectType<Component>(Quux)
;<Quux />
25 changes: 24 additions & 1 deletion packages/dts-test/setupHelpers.test-d.ts
Expand Up @@ -4,7 +4,9 @@ import {
useAttrs,
useSlots,
withDefaults,
Slots
Slots,
defineSlots,
VNode
} from 'vue'
import { describe, expectType } from './utils'

Expand Down Expand Up @@ -179,6 +181,27 @@ describe('defineEmits w/ runtime declaration', () => {
emit2('baz')
})

describe('defineSlots', () => {
// short syntax
const slots = defineSlots<{
default: { foo: string; bar: number }
optional?: string
}>()
expectType<(scope: { foo: string; bar: number }) => VNode[]>(slots.default)
expectType<undefined | ((scope: string) => VNode[])>(slots.optional)

// literal fn syntax (allow for specifying return type)
const fnSlots = defineSlots<{
default(props: { foo: string; bar: number }): any
optional?(props: string): any
}>()
expectType<(scope: { foo: string; bar: number }) => VNode[]>(fnSlots.default)
expectType<undefined | ((scope: string) => VNode[])>(fnSlots.optional)

const slotsUntype = defineSlots()
expectType<Slots>(slotsUntype)
})

describe('useAttrs', () => {
const attrs = useAttrs()
expectType<Record<string, unknown>>(attrs)
Expand Down