onUpdated,
defineComponent,
openBlock,
- createBlock
+ createBlock,
+ FunctionalComponent
} from '@vue/runtime-dom'
import { mockWarn } from '@vue/shared'
await nextTick()
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')
+ })
})
MethodOptions,
ComponentOptionsWithoutProps,
ComponentOptionsWithArrayProps,
- ComponentOptionsWithObjectProps
+ ComponentOptionsWithObjectProps,
+ EmitsOptions
} from './apiOptions'
import { SetupContext, RenderFunction } from './component'
import { ComponentPublicInstance } from './componentProxy'
// (uses user defined props interface)
// return type is for Vetur and TSX support
export function defineComponent<
- Props,
- RawBindings,
- D,
+ Props = {},
+ RawBindings = {},
+ D = {},
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<
Props,
D,
C,
M,
+ E,
VNodeProps & Props
>
}
RawBindings,
D,
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
- new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M>
+ new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M, E>
}
// overload 4: object format with object props declaration
RawBindings,
D,
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<
ExtractPropTypes<PropsOptions>,
D,
C,
M,
+ E,
VNodeProps & ExtractPropTypes<PropsOptions, false>
>
}
RawBindings,
D,
C extends ComputedOptions,
- M extends MethodOptions
-> extends LegacyOptions<Props, RawBindings, D, C, M>, SFCInternalOptions {
+ M extends MethodOptions,
+ E extends EmitsOptions,
+ EE extends string = string
+> extends LegacyOptions<Props, D, C, M>, SFCInternalOptions {
setup?: (
this: void,
props: Props,
- ctx: SetupContext
+ ctx: SetupContext<E>
) => RawBindings | RenderFunction | void
name?: string
template?: string | object // can be a direct DOM node
components?: Record<string, PublicAPIComponent>
directives?: Record<string, Directive>
inheritAttrs?: boolean
+ emits?: E | EE[]
// Internal ------------------------------------------------------------------
RawBindings = {},
D = {},
C extends ComputedOptions = {},
- M extends MethodOptions = {}
-> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
+ M extends MethodOptions = {},
+ E extends EmitsOptions = Record<string, any>,
+ EE extends string = string
+> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
props?: undefined
-} & ThisType<ComponentPublicInstance<{}, RawBindings, D, C, M, Readonly<Props>>>
+} & ThisType<
+ ComponentPublicInstance<{}, RawBindings, D, C, M, E, Readonly<Props>>
+ >
export type ComponentOptionsWithArrayProps<
PropNames extends string = string,
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
+ E extends EmitsOptions = Record<string, any>,
+ EE extends string = string,
Props = Readonly<{ [key in PropNames]?: any }>
-> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
+> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
props: PropNames[]
-} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
+} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions,
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
+ E extends EmitsOptions = Record<string, any>,
+ EE extends string = string,
Props = Readonly<ExtractPropTypes<PropsOptions>>
-> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
+> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
props: PropsOptions
-} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
+} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
export type ComponentOptions =
| ComponentOptionsWithoutProps<any, any, any, any, any>
[key: string]: Function
}
+export type EmitsOptions = Record<string, any> | string[]
+
export type ExtractComputedReturns<T extends any> = {
[key in keyof T]: T[key] extends { get: Function }
? ReturnType<T[key]['get']>
export interface LegacyOptions<
Props,
- RawBindings,
D,
C extends ComputedOptions,
M extends MethodOptions
} from './errorHandling'
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
import { Directive, validateDirectiveName } from './directives'
-import { applyOptions, ComponentOptions } from './apiOptions'
+import { applyOptions, ComponentOptions, EmitsOptions } from './apiOptions'
import {
EMPTY_OBJ,
isFunction,
__hmrUpdated?: boolean
}
-export interface FunctionalComponent<P = {}> extends SFCInternalOptions {
- (props: P, ctx: SetupContext): VNodeChild
+export interface FunctionalComponent<
+ P = {},
+ E extends EmitsOptions = Record<string, any>
+> extends SFCInternalOptions {
+ (props: P, ctx: SetupContext<E>): any
props?: ComponentPropsOptions<P>
+ emits?: E | (keyof E)[]
inheritAttrs?: boolean
displayName?: string
}
ERROR_CAPTURED = 'ec'
}
-export type Emit = (event: string, ...args: unknown[]) => any[]
-
-export interface SetupContext {
+type UnionToIntersection<U> = (U extends any
+ ? (k: U) => void
+ : never) extends ((k: infer I) => void)
+ ? I
+ : never
+
+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
slots: Slots
- emit: Emit
+ emit: Emit<E>
}
export type RenderFunction = {
rtc: null,
ec: null,
- emit: (event, ...args): any[] => {
+ emit: (event: string, ...args: any[]): any[] => {
const props = instance.vnode.props || EMPTY_OBJ
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
if (!handler && event.indexOf('update:') === 0) {
isSSR = false
) {
isInSSRComponentSetup = isSSR
- const propsOptions = instance.type.props
const { props, children, shapeFlag } = instance.vnode
- resolveProps(instance, props, propsOptions)
+ resolveProps(instance, props)
resolveSlots(instance, children)
// setup stateful logic
PatchFlags,
makeMap,
isReservedProp,
- EMPTY_ARR
+ EMPTY_ARR,
+ ShapeFlags,
+ isOn
} from '@vue/shared'
import { warn } from './warning'
import { Data, ComponentInternalInstance } from './component'
+import { EmitsOptions } from './apiOptions'
export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P>
export function resolveProps(
instance: ComponentInternalInstance,
- rawProps: Data | null,
- _options: ComponentPropsOptions | void
+ rawProps: Data | null
) {
+ const _options = instance.type.props
const hasDeclaredProps = !!_options
if (!rawProps && !hasDeclaredProps) {
+ instance.props = instance.attrs = EMPTY_OBJ
return
}
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
+ const emits = normalizeEmitsOptions(instance.type.emits)
const props: Data = {}
let attrs: Data | undefined = undefined
}
// prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key.
- if (hasDeclaredProps) {
- const camelKey = camelize(key)
- if (hasOwn(options, camelKey)) {
- setProp(camelKey, value)
- } else {
- // Any non-declared props are put into a separate `attrs` object
- // for spreading. Make sure to preserve original key casing
- ;(attrs || (attrs = {}))[key] = value
- }
- } else {
- setProp(key, value)
+ let camelKey
+ if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) {
+ setProp(camelKey, value)
+ } else if (!emits || !isListener(emits, key)) {
+ // Any non-declared (either as a prop or an emitted event) props are put
+ // into a separate `attrs` object for spreading. Make sure to preserve
+ // original key casing
+ ;(attrs || (attrs = {}))[key] = value
}
}
}
+
if (hasDeclaredProps) {
// set default values & cast booleans
for (let i = 0; i < needCastKeys.length; i++) {
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
// the props proxy
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)
for (const key in rawInitialProps) {
if (!hasOwn(props, key)) {
// lock readonly
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
}
-const normalizationMap = new WeakMap<
- ComponentPropsOptions,
- NormalizedPropsOptions
->()
-
function validatePropName(key: string) {
if (key[0] !== '$') {
return true
if (!raw) {
return EMPTY_ARR as any
}
- if (normalizationMap.has(raw)) {
- return normalizationMap.get(raw)!
+ if ((raw as any)._n) {
+ return (raw as any)._n
}
- const options: NormalizedPropsOptions[0] = {}
+ const normalized: NormalizedPropsOptions[0] = {}
const needCastKeys: NormalizedPropsOptions[1] = []
if (isArray(raw)) {
for (let i = 0; i < raw.length; i++) {
}
const normalizedKey = camelize(raw[i])
if (validatePropName(normalizedKey)) {
- options[normalizedKey] = EMPTY_OBJ
+ normalized[normalizedKey] = EMPTY_OBJ
}
}
} else {
const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) {
const opt = raw[key]
- const prop: NormalizedProp = (options[normalizedKey] =
+ const prop: NormalizedProp = (normalized[normalizedKey] =
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
if (prop) {
const booleanIndex = getTypeIndex(Boolean, prop.type)
}
}
}
- const normalized: NormalizedPropsOptions = [options, needCastKeys]
- normalizationMap.set(raw, normalized)
- return normalized
+ const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
+ Object.defineProperty(raw, '_n', { value: normalizedEntry })
+ 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
ComponentOptionsBase,
ComputedOptions,
MethodOptions,
- resolveMergedOptions
+ resolveMergedOptions,
+ EmitsOptions
} from './apiOptions'
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
import { warn } from './warning'
D = {}, // return from data()
C extends ComputedOptions = {},
M extends MethodOptions = {},
+ E extends EmitsOptions = {},
PublicProps = P
> = {
$: ComponentInternalInstance
$slots: Slots
$root: ComponentInternalInstance | null
$parent: ComponentInternalInstance | null
- $emit: Emit
+ $emit: Emit<E>
$el: any
- $options: ComponentOptionsBase<P, B, D, C, M>
+ $options: ComponentOptionsBase<P, B, D, C, M, E>
$forceUpdate: ReactiveEffect
$nextTick: typeof nextTick
$watch: typeof instanceWatch
import {
ComponentInternalInstance,
createComponentInstance,
- Component,
Data,
setupComponent
} from './component'
nextVNode.component = instance
instance.vnode = nextVNode
instance.next = null
- resolveProps(instance, nextVNode.props, (nextVNode.type as Component).props)
+ resolveProps(instance, nextVNode.props)
resolveSlots(instance, nextVNode.children)
}
expectError(<MyComponent b="foo" dd={{ n: 'string' }} ddd={['foo']} />)
})
-describe('type inference w/ optional props declaration', () => {
- const MyComponent = defineComponent({
- setup(_props: { msg: string }) {
- return {
- a: 1
- }
- },
- render() {
- expectType<string>(this.$props.msg)
- // props should be readonly
- expectError((this.$props.msg = 'foo'))
- // should not expose on `this`
- expectError(this.msg)
- expectType<number>(this.a)
- return null
- }
- })
+// describe('type inference w/ optional props declaration', () => {
+// const MyComponent = defineComponent({
+// setup(_props: { msg: string }) {
+// return {
+// a: 1
+// }
+// },
+// render() {
+// expectType<string>(this.$props.msg)
+// // props should be readonly
+// expectError((this.$props.msg = 'foo'))
+// // should not expose on `this`
+// expectError(this.msg)
+// expectType<number>(this.a)
+// return null
+// }
+// })
- expectType<JSX.Element>(<MyComponent msg="foo" />)
- expectError(<MyComponent />)
- expectError(<MyComponent msg={1} />)
-})
+// expectType<JSX.Element>(<MyComponent msg="foo" />)
+// expectError(<MyComponent />)
+// expectError(<MyComponent msg={1} />)
+// })
-describe('type inference w/ direct setup function', () => {
- const MyComponent = defineComponent((_props: { msg: string }) => {})
- expectType<JSX.Element>(<MyComponent msg="foo" />)
- expectError(<MyComponent />)
- expectError(<MyComponent msg={1} />)
-})
+// describe('type inference w/ direct setup function', () => {
+// const MyComponent = defineComponent((_props: { msg: string }) => {})
+// expectType<JSX.Element>(<MyComponent msg="foo" />)
+// expectError(<MyComponent />)
+// expectError(<MyComponent msg={1} />)
+// })
describe('type inference w/ array props declaration', () => {
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'))
+ }
+ })
+})
--- /dev/null
+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" />)