feat(runtime-core): type and attr fallthrough support for emits option
This commit is contained in:
parent
c409d4f297
commit
bf473a64ea
@ -8,7 +8,8 @@ import {
|
|||||||
onUpdated,
|
onUpdated,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
openBlock,
|
openBlock,
|
||||||
createBlock
|
createBlock,
|
||||||
|
FunctionalComponent
|
||||||
} from '@vue/runtime-dom'
|
} from '@vue/runtime-dom'
|
||||||
import { mockWarn } from '@vue/shared'
|
import { mockWarn } from '@vue/shared'
|
||||||
|
|
||||||
@ -428,4 +429,70 @@ describe('attribute fallthrough', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
|
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not let listener fallthrough when declared in emits (stateful)', () => {
|
||||||
|
const Child = defineComponent({
|
||||||
|
emits: ['click'],
|
||||||
|
render() {
|
||||||
|
return h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
onClick: () => {
|
||||||
|
this.$emit('click', 'custom')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'hello'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onClick = jest.fn()
|
||||||
|
const App = {
|
||||||
|
render() {
|
||||||
|
return h(Child, {
|
||||||
|
onClick
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.createElement('div')
|
||||||
|
document.body.appendChild(root)
|
||||||
|
render(h(App), root)
|
||||||
|
|
||||||
|
const node = root.children[0] as HTMLElement
|
||||||
|
node.dispatchEvent(new CustomEvent('click'))
|
||||||
|
expect(onClick).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onClick).toHaveBeenCalledWith('custom')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not let listener fallthrough when declared in emits (functional)', () => {
|
||||||
|
const Child: FunctionalComponent<{}, { click: any }> = (_, { emit }) => {
|
||||||
|
// should not be in props
|
||||||
|
expect((_ as any).onClick).toBeUndefined()
|
||||||
|
return h('button', {
|
||||||
|
onClick: () => {
|
||||||
|
emit('click', 'custom')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Child.emits = ['click']
|
||||||
|
|
||||||
|
const onClick = jest.fn()
|
||||||
|
const App = {
|
||||||
|
render() {
|
||||||
|
return h(Child, {
|
||||||
|
onClick
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.createElement('div')
|
||||||
|
document.body.appendChild(root)
|
||||||
|
render(h(App), root)
|
||||||
|
|
||||||
|
const node = root.children[0] as HTMLElement
|
||||||
|
node.dispatchEvent(new CustomEvent('click'))
|
||||||
|
expect(onClick).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onClick).toHaveBeenCalledWith('custom')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
MethodOptions,
|
MethodOptions,
|
||||||
ComponentOptionsWithoutProps,
|
ComponentOptionsWithoutProps,
|
||||||
ComponentOptionsWithArrayProps,
|
ComponentOptionsWithArrayProps,
|
||||||
ComponentOptionsWithObjectProps
|
ComponentOptionsWithObjectProps,
|
||||||
|
EmitsOptions
|
||||||
} from './apiOptions'
|
} from './apiOptions'
|
||||||
import { SetupContext, RenderFunction } from './component'
|
import { SetupContext, RenderFunction } from './component'
|
||||||
import { ComponentPublicInstance } from './componentProxy'
|
import { ComponentPublicInstance } from './componentProxy'
|
||||||
@ -39,13 +40,15 @@ export function defineComponent<Props, RawBindings = object>(
|
|||||||
// (uses user defined props interface)
|
// (uses user defined props interface)
|
||||||
// return type is for Vetur and TSX support
|
// return type is for Vetur and TSX support
|
||||||
export function defineComponent<
|
export function defineComponent<
|
||||||
Props,
|
Props = {},
|
||||||
RawBindings,
|
RawBindings = {},
|
||||||
D,
|
D = {},
|
||||||
C extends ComputedOptions = {},
|
C extends ComputedOptions = {},
|
||||||
M extends MethodOptions = {}
|
M extends MethodOptions = {},
|
||||||
|
E extends EmitsOptions = Record<string, any>,
|
||||||
|
EE extends string = string
|
||||||
>(
|
>(
|
||||||
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M>
|
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, E, EE>
|
||||||
): {
|
): {
|
||||||
new (): ComponentPublicInstance<
|
new (): ComponentPublicInstance<
|
||||||
Props,
|
Props,
|
||||||
@ -53,6 +56,7 @@ export function defineComponent<
|
|||||||
D,
|
D,
|
||||||
C,
|
C,
|
||||||
M,
|
M,
|
||||||
|
E,
|
||||||
VNodeProps & Props
|
VNodeProps & Props
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
@ -65,12 +69,22 @@ export function defineComponent<
|
|||||||
RawBindings,
|
RawBindings,
|
||||||
D,
|
D,
|
||||||
C extends ComputedOptions = {},
|
C extends ComputedOptions = {},
|
||||||
M extends MethodOptions = {}
|
M extends MethodOptions = {},
|
||||||
|
E extends EmitsOptions = Record<string, any>,
|
||||||
|
EE extends string = string
|
||||||
>(
|
>(
|
||||||
options: ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M>
|
options: ComponentOptionsWithArrayProps<
|
||||||
|
PropNames,
|
||||||
|
RawBindings,
|
||||||
|
D,
|
||||||
|
C,
|
||||||
|
M,
|
||||||
|
E,
|
||||||
|
EE
|
||||||
|
>
|
||||||
): {
|
): {
|
||||||
// array props technically doesn't place any contraints on props in TSX
|
// array props technically doesn't place any contraints on props in TSX
|
||||||
new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M>
|
new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M, E>
|
||||||
}
|
}
|
||||||
|
|
||||||
// overload 4: object format with object props declaration
|
// overload 4: object format with object props declaration
|
||||||
@ -82,9 +96,19 @@ export function defineComponent<
|
|||||||
RawBindings,
|
RawBindings,
|
||||||
D,
|
D,
|
||||||
C extends ComputedOptions = {},
|
C extends ComputedOptions = {},
|
||||||
M extends MethodOptions = {}
|
M extends MethodOptions = {},
|
||||||
|
E extends EmitsOptions = Record<string, any>,
|
||||||
|
EE extends string = string
|
||||||
>(
|
>(
|
||||||
options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M>
|
options: ComponentOptionsWithObjectProps<
|
||||||
|
PropsOptions,
|
||||||
|
RawBindings,
|
||||||
|
D,
|
||||||
|
C,
|
||||||
|
M,
|
||||||
|
E,
|
||||||
|
EE
|
||||||
|
>
|
||||||
): {
|
): {
|
||||||
new (): ComponentPublicInstance<
|
new (): ComponentPublicInstance<
|
||||||
ExtractPropTypes<PropsOptions>,
|
ExtractPropTypes<PropsOptions>,
|
||||||
@ -92,6 +116,7 @@ export function defineComponent<
|
|||||||
D,
|
D,
|
||||||
C,
|
C,
|
||||||
M,
|
M,
|
||||||
|
E,
|
||||||
VNodeProps & ExtractPropTypes<PropsOptions, false>
|
VNodeProps & ExtractPropTypes<PropsOptions, false>
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
@ -50,12 +50,14 @@ export interface ComponentOptionsBase<
|
|||||||
RawBindings,
|
RawBindings,
|
||||||
D,
|
D,
|
||||||
C extends ComputedOptions,
|
C extends ComputedOptions,
|
||||||
M extends MethodOptions
|
M extends MethodOptions,
|
||||||
> extends LegacyOptions<Props, RawBindings, D, C, M>, SFCInternalOptions {
|
E extends EmitsOptions,
|
||||||
|
EE extends string = string
|
||||||
|
> extends LegacyOptions<Props, D, C, M>, SFCInternalOptions {
|
||||||
setup?: (
|
setup?: (
|
||||||
this: void,
|
this: void,
|
||||||
props: Props,
|
props: Props,
|
||||||
ctx: SetupContext
|
ctx: SetupContext<E>
|
||||||
) => RawBindings | RenderFunction | void
|
) => RawBindings | RenderFunction | void
|
||||||
name?: string
|
name?: string
|
||||||
template?: string | object // can be a direct DOM node
|
template?: string | object // can be a direct DOM node
|
||||||
@ -75,6 +77,7 @@ export interface ComponentOptionsBase<
|
|||||||
components?: Record<string, PublicAPIComponent>
|
components?: Record<string, PublicAPIComponent>
|
||||||
directives?: Record<string, Directive>
|
directives?: Record<string, Directive>
|
||||||
inheritAttrs?: boolean
|
inheritAttrs?: boolean
|
||||||
|
emits?: E | EE[]
|
||||||
|
|
||||||
// Internal ------------------------------------------------------------------
|
// Internal ------------------------------------------------------------------
|
||||||
|
|
||||||
@ -97,10 +100,14 @@ export type ComponentOptionsWithoutProps<
|
|||||||
RawBindings = {},
|
RawBindings = {},
|
||||||
D = {},
|
D = {},
|
||||||
C extends ComputedOptions = {},
|
C extends ComputedOptions = {},
|
||||||
M extends MethodOptions = {}
|
M extends MethodOptions = {},
|
||||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
E extends EmitsOptions = Record<string, any>,
|
||||||
|
EE extends string = string
|
||||||
|
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||||
props?: undefined
|
props?: undefined
|
||||||
} & ThisType<ComponentPublicInstance<{}, RawBindings, D, C, M, Readonly<Props>>>
|
} & ThisType<
|
||||||
|
ComponentPublicInstance<{}, RawBindings, D, C, M, E, Readonly<Props>>
|
||||||
|
>
|
||||||
|
|
||||||
export type ComponentOptionsWithArrayProps<
|
export type ComponentOptionsWithArrayProps<
|
||||||
PropNames extends string = string,
|
PropNames extends string = string,
|
||||||
@ -108,10 +115,12 @@ export type ComponentOptionsWithArrayProps<
|
|||||||
D = {},
|
D = {},
|
||||||
C extends ComputedOptions = {},
|
C extends ComputedOptions = {},
|
||||||
M extends MethodOptions = {},
|
M extends MethodOptions = {},
|
||||||
|
E extends EmitsOptions = Record<string, any>,
|
||||||
|
EE extends string = string,
|
||||||
Props = Readonly<{ [key in PropNames]?: any }>
|
Props = Readonly<{ [key in PropNames]?: any }>
|
||||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||||
props: PropNames[]
|
props: PropNames[]
|
||||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
|
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
|
||||||
|
|
||||||
export type ComponentOptionsWithObjectProps<
|
export type ComponentOptionsWithObjectProps<
|
||||||
PropsOptions = ComponentObjectPropsOptions,
|
PropsOptions = ComponentObjectPropsOptions,
|
||||||
@ -119,10 +128,12 @@ export type ComponentOptionsWithObjectProps<
|
|||||||
D = {},
|
D = {},
|
||||||
C extends ComputedOptions = {},
|
C extends ComputedOptions = {},
|
||||||
M extends MethodOptions = {},
|
M extends MethodOptions = {},
|
||||||
|
E extends EmitsOptions = Record<string, any>,
|
||||||
|
EE extends string = string,
|
||||||
Props = Readonly<ExtractPropTypes<PropsOptions>>
|
Props = Readonly<ExtractPropTypes<PropsOptions>>
|
||||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||||
props: PropsOptions
|
props: PropsOptions
|
||||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
|
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
|
||||||
|
|
||||||
export type ComponentOptions =
|
export type ComponentOptions =
|
||||||
| ComponentOptionsWithoutProps<any, any, any, any, any>
|
| ComponentOptionsWithoutProps<any, any, any, any, any>
|
||||||
@ -138,6 +149,8 @@ export interface MethodOptions {
|
|||||||
[key: string]: Function
|
[key: string]: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EmitsOptions = Record<string, any> | string[]
|
||||||
|
|
||||||
export type ExtractComputedReturns<T extends any> = {
|
export type ExtractComputedReturns<T extends any> = {
|
||||||
[key in keyof T]: T[key] extends { get: Function }
|
[key in keyof T]: T[key] extends { get: Function }
|
||||||
? ReturnType<T[key]['get']>
|
? ReturnType<T[key]['get']>
|
||||||
@ -162,7 +175,6 @@ type ComponentInjectOptions =
|
|||||||
|
|
||||||
export interface LegacyOptions<
|
export interface LegacyOptions<
|
||||||
Props,
|
Props,
|
||||||
RawBindings,
|
|
||||||
D,
|
D,
|
||||||
C extends ComputedOptions,
|
C extends ComputedOptions,
|
||||||
M extends MethodOptions
|
M extends MethodOptions
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
} from './errorHandling'
|
} from './errorHandling'
|
||||||
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
|
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
|
||||||
import { Directive, validateDirectiveName } from './directives'
|
import { Directive, validateDirectiveName } from './directives'
|
||||||
import { applyOptions, ComponentOptions } from './apiOptions'
|
import { applyOptions, ComponentOptions, EmitsOptions } from './apiOptions'
|
||||||
import {
|
import {
|
||||||
EMPTY_OBJ,
|
EMPTY_OBJ,
|
||||||
isFunction,
|
isFunction,
|
||||||
@ -52,9 +52,13 @@ export interface SFCInternalOptions {
|
|||||||
__hmrUpdated?: boolean
|
__hmrUpdated?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionalComponent<P = {}> extends SFCInternalOptions {
|
export interface FunctionalComponent<
|
||||||
(props: P, ctx: SetupContext): VNodeChild
|
P = {},
|
||||||
|
E extends EmitsOptions = Record<string, any>
|
||||||
|
> extends SFCInternalOptions {
|
||||||
|
(props: P, ctx: SetupContext<E>): any
|
||||||
props?: ComponentPropsOptions<P>
|
props?: ComponentPropsOptions<P>
|
||||||
|
emits?: E | (keyof E)[]
|
||||||
inheritAttrs?: boolean
|
inheritAttrs?: boolean
|
||||||
displayName?: string
|
displayName?: string
|
||||||
}
|
}
|
||||||
@ -92,12 +96,29 @@ export const enum LifecycleHooks {
|
|||||||
ERROR_CAPTURED = 'ec'
|
ERROR_CAPTURED = 'ec'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Emit = (event: string, ...args: unknown[]) => any[]
|
type UnionToIntersection<U> = (U extends any
|
||||||
|
? (k: U) => void
|
||||||
|
: never) extends ((k: infer I) => void)
|
||||||
|
? I
|
||||||
|
: never
|
||||||
|
|
||||||
export interface SetupContext {
|
export type Emit<
|
||||||
|
Options = Record<string, any>,
|
||||||
|
Event extends keyof Options = keyof Options
|
||||||
|
> = Options extends any[]
|
||||||
|
? (event: Options[0], ...args: any[]) => unknown[]
|
||||||
|
: UnionToIntersection<
|
||||||
|
{
|
||||||
|
[key in Event]: Options[key] extends ((...args: infer Args) => any)
|
||||||
|
? (event: key, ...args: Args) => unknown[]
|
||||||
|
: (event: key, ...args: any[]) => unknown[]
|
||||||
|
}[Event]
|
||||||
|
>
|
||||||
|
|
||||||
|
export interface SetupContext<E = Record<string, any>> {
|
||||||
attrs: Data
|
attrs: Data
|
||||||
slots: Slots
|
slots: Slots
|
||||||
emit: Emit
|
emit: Emit<E>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RenderFunction = {
|
export type RenderFunction = {
|
||||||
@ -248,7 +269,7 @@ export function createComponentInstance(
|
|||||||
rtc: null,
|
rtc: null,
|
||||||
ec: null,
|
ec: null,
|
||||||
|
|
||||||
emit: (event, ...args): any[] => {
|
emit: (event: string, ...args: any[]): any[] => {
|
||||||
const props = instance.vnode.props || EMPTY_OBJ
|
const props = instance.vnode.props || EMPTY_OBJ
|
||||||
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||||
if (!handler && event.indexOf('update:') === 0) {
|
if (!handler && event.indexOf('update:') === 0) {
|
||||||
@ -303,9 +324,8 @@ export function setupComponent(
|
|||||||
isSSR = false
|
isSSR = false
|
||||||
) {
|
) {
|
||||||
isInSSRComponentSetup = isSSR
|
isInSSRComponentSetup = isSSR
|
||||||
const propsOptions = instance.type.props
|
|
||||||
const { props, children, shapeFlag } = instance.vnode
|
const { props, children, shapeFlag } = instance.vnode
|
||||||
resolveProps(instance, props, propsOptions)
|
resolveProps(instance, props)
|
||||||
resolveSlots(instance, children)
|
resolveSlots(instance, children)
|
||||||
|
|
||||||
// setup stateful logic
|
// setup stateful logic
|
||||||
|
@ -13,10 +13,13 @@ import {
|
|||||||
PatchFlags,
|
PatchFlags,
|
||||||
makeMap,
|
makeMap,
|
||||||
isReservedProp,
|
isReservedProp,
|
||||||
EMPTY_ARR
|
EMPTY_ARR,
|
||||||
|
ShapeFlags,
|
||||||
|
isOn
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { Data, ComponentInternalInstance } from './component'
|
import { Data, ComponentInternalInstance } from './component'
|
||||||
|
import { EmitsOptions } from './apiOptions'
|
||||||
|
|
||||||
export type ComponentPropsOptions<P = Data> =
|
export type ComponentPropsOptions<P = Data> =
|
||||||
| ComponentObjectPropsOptions<P>
|
| ComponentObjectPropsOptions<P>
|
||||||
@ -103,15 +106,17 @@ type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
|
|||||||
|
|
||||||
export function resolveProps(
|
export function resolveProps(
|
||||||
instance: ComponentInternalInstance,
|
instance: ComponentInternalInstance,
|
||||||
rawProps: Data | null,
|
rawProps: Data | null
|
||||||
_options: ComponentPropsOptions | void
|
|
||||||
) {
|
) {
|
||||||
|
const _options = instance.type.props
|
||||||
const hasDeclaredProps = !!_options
|
const hasDeclaredProps = !!_options
|
||||||
if (!rawProps && !hasDeclaredProps) {
|
if (!rawProps && !hasDeclaredProps) {
|
||||||
|
instance.props = instance.attrs = EMPTY_OBJ
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
|
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
|
||||||
|
const emits = normalizeEmitsOptions(instance.type.emits)
|
||||||
const props: Data = {}
|
const props: Data = {}
|
||||||
let attrs: Data | undefined = undefined
|
let attrs: Data | undefined = undefined
|
||||||
|
|
||||||
@ -139,20 +144,18 @@ export function resolveProps(
|
|||||||
}
|
}
|
||||||
// prop option names are camelized during normalization, so to support
|
// prop option names are camelized during normalization, so to support
|
||||||
// kebab -> camel conversion here we need to camelize the key.
|
// kebab -> camel conversion here we need to camelize the key.
|
||||||
if (hasDeclaredProps) {
|
let camelKey
|
||||||
const camelKey = camelize(key)
|
if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) {
|
||||||
if (hasOwn(options, camelKey)) {
|
setProp(camelKey, value)
|
||||||
setProp(camelKey, value)
|
} else if (!emits || !isListener(emits, key)) {
|
||||||
} else {
|
// Any non-declared (either as a prop or an emitted event) props are put
|
||||||
// Any non-declared props are put into a separate `attrs` object
|
// into a separate `attrs` object for spreading. Make sure to preserve
|
||||||
// for spreading. Make sure to preserve original key casing
|
// original key casing
|
||||||
;(attrs || (attrs = {}))[key] = value
|
;(attrs || (attrs = {}))[key] = value
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setProp(key, value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDeclaredProps) {
|
if (hasDeclaredProps) {
|
||||||
// set default values & cast booleans
|
// set default values & cast booleans
|
||||||
for (let i = 0; i < needCastKeys.length; i++) {
|
for (let i = 0; i < needCastKeys.length; i++) {
|
||||||
@ -186,15 +189,16 @@ export function resolveProps(
|
|||||||
validateProp(key, props[key], opt, !hasOwn(props, key))
|
validateProp(key, props[key], opt, !hasOwn(props, key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// if component has no declared props, $attrs === $props
|
|
||||||
attrs = props
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// in case of dynamic props, check if we need to delete keys from
|
// in case of dynamic props, check if we need to delete keys from
|
||||||
// the props proxy
|
// the props proxy
|
||||||
const { patchFlag } = instance.vnode
|
const { patchFlag } = instance.vnode
|
||||||
if (propsProxy && (patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)) {
|
if (
|
||||||
|
hasDeclaredProps &&
|
||||||
|
propsProxy &&
|
||||||
|
(patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)
|
||||||
|
) {
|
||||||
const rawInitialProps = toRaw(propsProxy)
|
const rawInitialProps = toRaw(propsProxy)
|
||||||
for (const key in rawInitialProps) {
|
for (const key in rawInitialProps) {
|
||||||
if (!hasOwn(props, key)) {
|
if (!hasOwn(props, key)) {
|
||||||
@ -206,15 +210,18 @@ export function resolveProps(
|
|||||||
// lock readonly
|
// lock readonly
|
||||||
lock()
|
lock()
|
||||||
|
|
||||||
instance.props = props
|
if (
|
||||||
|
instance.vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT &&
|
||||||
|
!hasDeclaredProps
|
||||||
|
) {
|
||||||
|
// functional component with optional props: use attrs as props
|
||||||
|
instance.props = attrs || EMPTY_OBJ
|
||||||
|
} else {
|
||||||
|
instance.props = props
|
||||||
|
}
|
||||||
instance.attrs = attrs || EMPTY_OBJ
|
instance.attrs = attrs || EMPTY_OBJ
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizationMap = new WeakMap<
|
|
||||||
ComponentPropsOptions,
|
|
||||||
NormalizedPropsOptions
|
|
||||||
>()
|
|
||||||
|
|
||||||
function validatePropName(key: string) {
|
function validatePropName(key: string) {
|
||||||
if (key[0] !== '$') {
|
if (key[0] !== '$') {
|
||||||
return true
|
return true
|
||||||
@ -230,10 +237,10 @@ export function normalizePropsOptions(
|
|||||||
if (!raw) {
|
if (!raw) {
|
||||||
return EMPTY_ARR as any
|
return EMPTY_ARR as any
|
||||||
}
|
}
|
||||||
if (normalizationMap.has(raw)) {
|
if ((raw as any)._n) {
|
||||||
return normalizationMap.get(raw)!
|
return (raw as any)._n
|
||||||
}
|
}
|
||||||
const options: NormalizedPropsOptions[0] = {}
|
const normalized: NormalizedPropsOptions[0] = {}
|
||||||
const needCastKeys: NormalizedPropsOptions[1] = []
|
const needCastKeys: NormalizedPropsOptions[1] = []
|
||||||
if (isArray(raw)) {
|
if (isArray(raw)) {
|
||||||
for (let i = 0; i < raw.length; i++) {
|
for (let i = 0; i < raw.length; i++) {
|
||||||
@ -242,7 +249,7 @@ export function normalizePropsOptions(
|
|||||||
}
|
}
|
||||||
const normalizedKey = camelize(raw[i])
|
const normalizedKey = camelize(raw[i])
|
||||||
if (validatePropName(normalizedKey)) {
|
if (validatePropName(normalizedKey)) {
|
||||||
options[normalizedKey] = EMPTY_OBJ
|
normalized[normalizedKey] = EMPTY_OBJ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -253,7 +260,7 @@ export function normalizePropsOptions(
|
|||||||
const normalizedKey = camelize(key)
|
const normalizedKey = camelize(key)
|
||||||
if (validatePropName(normalizedKey)) {
|
if (validatePropName(normalizedKey)) {
|
||||||
const opt = raw[key]
|
const opt = raw[key]
|
||||||
const prop: NormalizedProp = (options[normalizedKey] =
|
const prop: NormalizedProp = (normalized[normalizedKey] =
|
||||||
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
|
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
|
||||||
if (prop) {
|
if (prop) {
|
||||||
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
||||||
@ -269,9 +276,38 @@ export function normalizePropsOptions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const normalized: NormalizedPropsOptions = [options, needCastKeys]
|
const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
|
||||||
normalizationMap.set(raw, normalized)
|
Object.defineProperty(raw, '_n', { value: normalizedEntry })
|
||||||
return normalized
|
return normalizedEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEmitsOptions(
|
||||||
|
options: EmitsOptions | undefined
|
||||||
|
): Record<string, any> | undefined {
|
||||||
|
if (!options) {
|
||||||
|
return
|
||||||
|
} else if (isArray(options)) {
|
||||||
|
if ((options as any)._n) {
|
||||||
|
return (options as any)._n
|
||||||
|
}
|
||||||
|
const normalized: Record<string, null> = {}
|
||||||
|
options.forEach(key => (normalized[key] = null))
|
||||||
|
Object.defineProperty(options, '_n', normalized)
|
||||||
|
return normalized
|
||||||
|
} else {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isListener(emits: Record<string, any>, key: string): boolean {
|
||||||
|
if (!isOn(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const eventName = key.slice(2)
|
||||||
|
return (
|
||||||
|
hasOwn(emits, eventName) ||
|
||||||
|
hasOwn(emits, eventName[0].toLowerCase() + eventName.slice(1))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use function string name to check type constructors
|
// use function string name to check type constructors
|
||||||
|
@ -7,7 +7,8 @@ import {
|
|||||||
ComponentOptionsBase,
|
ComponentOptionsBase,
|
||||||
ComputedOptions,
|
ComputedOptions,
|
||||||
MethodOptions,
|
MethodOptions,
|
||||||
resolveMergedOptions
|
resolveMergedOptions,
|
||||||
|
EmitsOptions
|
||||||
} from './apiOptions'
|
} from './apiOptions'
|
||||||
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
|
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
@ -26,6 +27,7 @@ export type ComponentPublicInstance<
|
|||||||
D = {}, // return from data()
|
D = {}, // return from data()
|
||||||
C extends ComputedOptions = {},
|
C extends ComputedOptions = {},
|
||||||
M extends MethodOptions = {},
|
M extends MethodOptions = {},
|
||||||
|
E extends EmitsOptions = {},
|
||||||
PublicProps = P
|
PublicProps = P
|
||||||
> = {
|
> = {
|
||||||
$: ComponentInternalInstance
|
$: ComponentInternalInstance
|
||||||
@ -36,9 +38,9 @@ export type ComponentPublicInstance<
|
|||||||
$slots: Slots
|
$slots: Slots
|
||||||
$root: ComponentInternalInstance | null
|
$root: ComponentInternalInstance | null
|
||||||
$parent: ComponentInternalInstance | null
|
$parent: ComponentInternalInstance | null
|
||||||
$emit: Emit
|
$emit: Emit<E>
|
||||||
$el: any
|
$el: any
|
||||||
$options: ComponentOptionsBase<P, B, D, C, M>
|
$options: ComponentOptionsBase<P, B, D, C, M, E>
|
||||||
$forceUpdate: ReactiveEffect
|
$forceUpdate: ReactiveEffect
|
||||||
$nextTick: typeof nextTick
|
$nextTick: typeof nextTick
|
||||||
$watch: typeof instanceWatch
|
$watch: typeof instanceWatch
|
||||||
|
@ -15,7 +15,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
createComponentInstance,
|
createComponentInstance,
|
||||||
Component,
|
|
||||||
Data,
|
Data,
|
||||||
setupComponent
|
setupComponent
|
||||||
} from './component'
|
} from './component'
|
||||||
@ -1249,7 +1248,7 @@ function baseCreateRenderer(
|
|||||||
nextVNode.component = instance
|
nextVNode.component = instance
|
||||||
instance.vnode = nextVNode
|
instance.vnode = nextVNode
|
||||||
instance.next = null
|
instance.next = null
|
||||||
resolveProps(instance, nextVNode.props, (nextVNode.type as Component).props)
|
resolveProps(instance, nextVNode.props)
|
||||||
resolveSlots(instance, nextVNode.children)
|
resolveSlots(instance, nextVNode.children)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,35 +177,35 @@ describe('with object props', () => {
|
|||||||
expectError(<MyComponent b="foo" dd={{ n: 'string' }} ddd={['foo']} />)
|
expectError(<MyComponent b="foo" dd={{ n: 'string' }} ddd={['foo']} />)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('type inference w/ optional props declaration', () => {
|
// describe('type inference w/ optional props declaration', () => {
|
||||||
const MyComponent = defineComponent({
|
// const MyComponent = defineComponent({
|
||||||
setup(_props: { msg: string }) {
|
// setup(_props: { msg: string }) {
|
||||||
return {
|
// return {
|
||||||
a: 1
|
// a: 1
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
render() {
|
// render() {
|
||||||
expectType<string>(this.$props.msg)
|
// expectType<string>(this.$props.msg)
|
||||||
// props should be readonly
|
// // props should be readonly
|
||||||
expectError((this.$props.msg = 'foo'))
|
// expectError((this.$props.msg = 'foo'))
|
||||||
// should not expose on `this`
|
// // should not expose on `this`
|
||||||
expectError(this.msg)
|
// expectError(this.msg)
|
||||||
expectType<number>(this.a)
|
// expectType<number>(this.a)
|
||||||
return null
|
// return null
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
expectType<JSX.Element>(<MyComponent msg="foo" />)
|
// expectType<JSX.Element>(<MyComponent msg="foo" />)
|
||||||
expectError(<MyComponent />)
|
// expectError(<MyComponent />)
|
||||||
expectError(<MyComponent msg={1} />)
|
// expectError(<MyComponent msg={1} />)
|
||||||
})
|
// })
|
||||||
|
|
||||||
describe('type inference w/ direct setup function', () => {
|
// describe('type inference w/ direct setup function', () => {
|
||||||
const MyComponent = defineComponent((_props: { msg: string }) => {})
|
// const MyComponent = defineComponent((_props: { msg: string }) => {})
|
||||||
expectType<JSX.Element>(<MyComponent msg="foo" />)
|
// expectType<JSX.Element>(<MyComponent msg="foo" />)
|
||||||
expectError(<MyComponent />)
|
// expectError(<MyComponent />)
|
||||||
expectError(<MyComponent msg={1} />)
|
// expectError(<MyComponent msg={1} />)
|
||||||
})
|
// })
|
||||||
|
|
||||||
describe('type inference w/ array props declaration', () => {
|
describe('type inference w/ array props declaration', () => {
|
||||||
defineComponent({
|
defineComponent({
|
||||||
@ -320,3 +320,57 @@ describe('defineComponent', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('emits', () => {
|
||||||
|
// Note: for TSX inference, ideally we want to map emits to onXXX props,
|
||||||
|
// but that requires type-level string constant concatenation as suggested in
|
||||||
|
// https://github.com/Microsoft/TypeScript/issues/12754
|
||||||
|
|
||||||
|
// The workaround for TSX users is instead of using emits, declare onXXX props
|
||||||
|
// and call them instead. Since `v-on:click` compiles to an `onClick` prop,
|
||||||
|
// this would also support other users consuming the component in templates
|
||||||
|
// with `v-on` listeners.
|
||||||
|
|
||||||
|
// with object emits
|
||||||
|
defineComponent({
|
||||||
|
emits: {
|
||||||
|
click: (n: number) => typeof n === 'number',
|
||||||
|
input: (b: string) => null
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
emit('click', 1)
|
||||||
|
emit('input', 'foo')
|
||||||
|
expectError(emit('nope'))
|
||||||
|
expectError(emit('click'))
|
||||||
|
expectError(emit('click', 'foo'))
|
||||||
|
expectError(emit('input'))
|
||||||
|
expectError(emit('input', 1))
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$emit('click', 1)
|
||||||
|
this.$emit('input', 'foo')
|
||||||
|
expectError(this.$emit('nope'))
|
||||||
|
expectError(this.$emit('click'))
|
||||||
|
expectError(this.$emit('click', 'foo'))
|
||||||
|
expectError(this.$emit('input'))
|
||||||
|
expectError(this.$emit('input', 1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// with array emits
|
||||||
|
defineComponent({
|
||||||
|
emits: ['foo', 'bar'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
emit('foo')
|
||||||
|
emit('foo', 123)
|
||||||
|
emit('bar')
|
||||||
|
expectError(emit('nope'))
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$emit('foo')
|
||||||
|
this.$emit('foo', 123)
|
||||||
|
this.$emit('bar')
|
||||||
|
expectError(this.$emit('nope'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
39
test-dts/functionalComponent.test-d.tsx
Normal file
39
test-dts/functionalComponent.test-d.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { expectError, expectType } from 'tsd'
|
||||||
|
import { FunctionalComponent } from './index'
|
||||||
|
|
||||||
|
// simple function signature
|
||||||
|
const Foo = (props: { foo: number }) => props.foo
|
||||||
|
|
||||||
|
// TSX
|
||||||
|
expectType<JSX.Element>(<Foo foo={1} />)
|
||||||
|
expectError(<Foo />)
|
||||||
|
expectError(<Foo foo="bar" />)
|
||||||
|
|
||||||
|
// Explicit signature with props + emits
|
||||||
|
const Bar: FunctionalComponent<
|
||||||
|
{ foo: number },
|
||||||
|
{ update: (value: number) => void }
|
||||||
|
> = (props, { emit }) => {
|
||||||
|
expectType<number>(props.foo)
|
||||||
|
|
||||||
|
emit('update', 123)
|
||||||
|
expectError(emit('nope'))
|
||||||
|
expectError(emit('update'))
|
||||||
|
expectError(emit('update', 'nope'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// assigning runtime options
|
||||||
|
Bar.props = {
|
||||||
|
foo: Number
|
||||||
|
}
|
||||||
|
expectError((Bar.props = { foo: String }))
|
||||||
|
|
||||||
|
Bar.emits = {
|
||||||
|
update: value => value > 1
|
||||||
|
}
|
||||||
|
expectError((Bar.emits = { baz: () => void 0 }))
|
||||||
|
|
||||||
|
// TSX
|
||||||
|
expectType<JSX.Element>(<Bar foo={1} />)
|
||||||
|
expectError(<Bar />)
|
||||||
|
expectError(<Bar foo="bar" />)
|
Loading…
Reference in New Issue
Block a user