feat(sfc): withDefaults helper

This commit is contained in:
Evan You
2021-06-26 21:11:57 -04:00
parent 3ffc7be864
commit 4c5844a9ca
9 changed files with 492 additions and 165 deletions

View File

@@ -8,8 +8,11 @@ import {
import {
defineEmits,
defineProps,
defineExpose,
withDefaults,
useAttrs,
useSlots
useSlots,
mergeDefaults
} from '../src/apiSetupHelpers'
describe('SFC <script setup> helpers', () => {
@@ -19,6 +22,12 @@ describe('SFC <script setup> helpers', () => {
defineEmits()
expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
defineExpose()
expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
withDefaults({}, {})
expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
})
test('useSlots / useAttrs (no args)', () => {
@@ -58,4 +67,26 @@ describe('SFC <script setup> helpers', () => {
expect(slots).toBe(ctx!.slots)
expect(attrs).toBe(ctx!.attrs)
})
test('mergeDefaults', () => {
const merged = mergeDefaults(
{
foo: null,
bar: { type: String, required: false }
},
{
foo: 1,
bar: 'baz'
}
)
expect(merged).toMatchObject({
foo: { default: 1 },
bar: { type: String, required: false, default: 'baz' }
})
mergeDefaults({}, { foo: 1 })
expect(
`props default key "foo" has no corresponding declaration`
).toHaveBeenWarned()
})
})

View File

@@ -4,63 +4,104 @@ import {
createSetupContext
} from './component'
import { EmitFn, EmitsOptions } from './componentEmits'
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
import {
ComponentObjectPropsOptions,
PropOptions,
ExtractPropTypes
} from './componentProps'
import { warn } from './warning'
type InferDefaults<T> = {
[K in keyof T]?: NonNullable<T[K]> extends object
? () => NonNullable<T[K]>
: NonNullable<T[K]>
}
// dev only
const warnRuntimeUsage = (method: string) =>
warn(
`${method}() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. Its arguments should be ` +
`compiled away and passing it at runtime has no effect.`
)
/**
* Compile-time-only helper used for declaring props inside `<script setup>`.
* This is stripped away in the compiled code and should never be actually
* called at runtime.
* Vue `<script setup>` compiler macro for declaring component props. The
* expected argument is the same as the component `props` option.
*
* Example runtime declaration:
* ```js
* // using Array syntax
* const props = defineProps(['foo', 'bar'])
* // using Object syntax
* const props = defineProps({
* foo: String,
* bar: {
* type: Number,
* required: true
* }
* })
* ```
*
* Equivalent type-based decalration:
* ```ts
* // will be compiled into equivalent runtime declarations
* const props = defineProps<{
* foo?: string
* bar: number
* }>()
* ```
*
* This is only usable inside `<script setup>`, is compiled away in the
* output and should **not** be actually called at runtime.
*/
// overload 1: string props
// overload 1: runtime props w/ array
export function defineProps<PropNames extends string = string>(
props: PropNames[]
): Readonly<{ [key in PropNames]?: any }>
// overload 2: runtime props w/ object
export function defineProps<
TypeProps = undefined,
PropNames extends string = string,
InferredProps = { [key in PropNames]?: any }
>(
props?: PropNames[]
): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
// overload 2: object props
export function defineProps<
TypeProps = undefined,
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
InferredProps = ExtractPropTypes<PP>
>(
props?: PP,
defaults?: InferDefaults<TypeProps>
): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions
>(props: PP): Readonly<ExtractPropTypes<PP>>
// overload 3: typed-based declaration
export function defineProps<TypeProps>(): Readonly<TypeProps>
// implementation
export function defineProps() {
if (__DEV__) {
warn(
`defineProps() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. Its arguments should be ` +
`compiled away and passing it at runtime has no effect.`
)
warnRuntimeUsage(`defineProps`)
}
return null as any
}
export function defineEmits<
TypeEmit = undefined,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
InferredEmit = EmitFn<E>
>(emitOptions?: E | EE[]): TypeEmit extends undefined ? InferredEmit : TypeEmit
/**
* Vue `<script setup>` compiler macro for declaring a component's emitted
* events. The expected argument is the same as the component `emits` option.
*
* Example runtime declaration:
* ```js
* const emit = defineEmits(['change', 'update'])
* ```
*
* Example type-based decalration:
* ```ts
* const emit = defineEmits<{
* (event: 'change'): void
* (event: 'update', id: number): void
* }>()
*
* emit('change')
* emit('update', 1)
* ```
*
* This is only usable inside `<script setup>`, is compiled away in the
* output and should **not** be actually called at runtime.
*/
// overload 1: runtime emits w/ array
export function defineEmits<EE extends string = string>(
emitOptions: EE[]
): EmitFn<EE[]>
export function defineEmits<E extends EmitsOptions = EmitsOptions>(
emitOptions: E
): EmitFn<E>
export function defineEmits<TypeEmit>(): TypeEmit
// implementation
export function defineEmits() {
if (__DEV__) {
warn(
`defineEmits() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. Its arguments should be ` +
`compiled away and passing it at runtime has no effect.`
)
warnRuntimeUsage(`defineEmits`)
}
return null as any
}
@@ -70,16 +111,70 @@ export function defineEmits() {
*/
export const defineEmit = defineEmits
/**
* Vue `<script setup>` compiler macro for declaring a component's exposed
* instance properties when it is accessed by a parent component via template
* refs.
*
* `<script setup>` components are closed by default - i.e. varaibles inside
* the `<script setup>` scope is not exposed to parent unless explicitly exposed
* via `defineExpose`.
*
* This is only usable inside `<script setup>`, is compiled away in the
* output and should **not** be actually called at runtime.
*/
export function defineExpose(exposed?: Record<string, any>) {
if (__DEV__) {
warn(
`defineExpose() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. Its usage should be ` +
`compiled away and calling it at runtime has no effect.`
)
warnRuntimeUsage(`defineExpose`)
}
}
type NotUndefined<T> = T extends undefined ? never : T
type InferDefaults<T> = {
[K in keyof T]?: NotUndefined<T[K]> extends (
| number
| string
| boolean
| symbol
| Function)
? NotUndefined<T[K]>
: (props: T) => NotUndefined<T[K]>
}
type PropsWithDefaults<Base, Defaults> = Base &
{
[K in keyof Defaults]: K extends keyof Base ? NotUndefined<Base[K]> : never
}
/**
* Vue `<script setup>` compiler macro for providing props default values when
* using type-based `defineProps` decalration.
*
* Example usage:
* ```ts
* withDefaults(defineProps<{
* size?: number
* labels?: string[]
* }>(), {
* size: 3,
* labels: () => ['default label']
* })
* ```
*
* This is only usable inside `<script setup>`, is compiled away in the output
* and should **not** be actually called at runtime.
*/
export function withDefaults<Props, Defaults extends InferDefaults<Props>>(
props: Props,
defaults: Defaults
): PropsWithDefaults<Props, Defaults> {
if (__DEV__) {
warnRuntimeUsage(`withDefaults`)
}
return null as any
}
/**
* @deprecated use `useSlots` and `useAttrs` instead.
*/
@@ -93,6 +188,14 @@ export function useContext(): SetupContext {
return getContext()
}
export function useSlots(): SetupContext['slots'] {
return getContext().slots
}
export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs
}
function getContext(): SetupContext {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
@@ -101,10 +204,25 @@ function getContext(): SetupContext {
return i.setupContext || (i.setupContext = createSetupContext(i))
}
export function useSlots(): SetupContext['slots'] {
return getContext().slots
}
export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs
/**
* Runtime helper for merging default declarations. Imported by compiled code
* only.
* @internal
*/
export function mergeDefaults(
// the base props is compiler-generated and guaranteed to be in this shape.
props: Record<string, PropOptions | null>,
defaults: Record<string, any>
) {
for (const key in defaults) {
const val = props[key]
if (val) {
val.default = defaults[key]
} else if (val === null) {
props[key] = { default: defaults[key] }
} else if (__DEV__) {
warn(`props default key "${key}" has no corresponding declaration.`)
}
}
return props
}

View File

@@ -51,7 +51,7 @@ export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
type DefaultFactory<T> = (props: Data) => T | null | undefined
interface PropOptions<T = any, D = T> {
export interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null
required?: boolean
default?: D | DefaultFactory<D> | null | undefined | object

View File

@@ -44,9 +44,17 @@ export { provide, inject } from './apiInject'
export { nextTick } from './scheduler'
export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
// <script setup> API ----------------------------------------------------------
export {
defineProps,
defineEmits,
defineExpose,
withDefaults,
// internal
mergeDefaults,
// deprecated
defineEmit,
useContext
} from './apiSetupHelpers'
@@ -140,7 +148,6 @@ export {
DeepReadonly
} from '@vue/reactivity'
export {
// types
WatchEffect,
WatchOptions,
WatchOptionsBase,