componentEmits.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import {
  2. camelize,
  3. EMPTY_OBJ,
  4. toHandlerKey,
  5. extend,
  6. hasOwn,
  7. hyphenate,
  8. isArray,
  9. isFunction,
  10. isOn,
  11. toNumber,
  12. UnionToIntersection
  13. } from '@vue/shared'
  14. import {
  15. ComponentInternalInstance,
  16. ComponentOptions,
  17. ConcreteComponent,
  18. formatComponentName
  19. } from './component'
  20. import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
  21. import { warn } from './warning'
  22. import { devtoolsComponentEmit } from './devtools'
  23. import { AppContext } from './apiCreateApp'
  24. import { emit as compatInstanceEmit } from './compat/instanceEventEmitter'
  25. import {
  26. compatModelEventPrefix,
  27. compatModelEmit
  28. } from './compat/componentVModel'
  29. export type ObjectEmitsOptions = Record<
  30. string,
  31. ((...args: any[]) => any) | null
  32. >
  33. export type EmitsOptions = ObjectEmitsOptions | string[]
  34. export type EmitsToProps<T extends EmitsOptions> = T extends string[]
  35. ? {
  36. [K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
  37. }
  38. : T extends ObjectEmitsOptions
  39. ? {
  40. [K in string &
  41. `on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
  42. ? T[Uncapitalize<C>] extends null
  43. ? (...args: any[]) => any
  44. : (
  45. ...args: T[Uncapitalize<C>] extends (...args: infer P) => any
  46. ? P
  47. : never
  48. ) => any
  49. : never
  50. }
  51. : {}
  52. export type EmitFn<
  53. Options = ObjectEmitsOptions,
  54. Event extends keyof Options = keyof Options
  55. > = Options extends Array<infer V>
  56. ? (event: V, ...args: any[]) => void
  57. : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
  58. ? (event: string, ...args: any[]) => void
  59. : UnionToIntersection<
  60. {
  61. [key in Event]: Options[key] extends (...args: infer Args) => any
  62. ? (event: key, ...args: Args) => void
  63. : (event: key, ...args: any[]) => void
  64. }[Event]
  65. >
  66. export function emit(
  67. instance: ComponentInternalInstance,
  68. event: string,
  69. ...rawArgs: any[]
  70. ) {
  71. const props = instance.vnode.props || EMPTY_OBJ
  72. if (__DEV__) {
  73. const {
  74. emitsOptions,
  75. propsOptions: [propsOptions]
  76. } = instance
  77. if (emitsOptions) {
  78. if (
  79. !(event in emitsOptions) &&
  80. !(
  81. __COMPAT__ &&
  82. (event.startsWith('hook:') ||
  83. event.startsWith(compatModelEventPrefix))
  84. )
  85. ) {
  86. if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
  87. warn(
  88. `Component emitted event "${event}" but it is neither declared in ` +
  89. `the emits option nor as an "${toHandlerKey(event)}" prop.`
  90. )
  91. }
  92. } else {
  93. const validator = emitsOptions[event]
  94. if (isFunction(validator)) {
  95. const isValid = validator(...rawArgs)
  96. if (!isValid) {
  97. warn(
  98. `Invalid event arguments: event validation failed for event "${event}".`
  99. )
  100. }
  101. }
  102. }
  103. }
  104. }
  105. let args = rawArgs
  106. const isModelListener = event.startsWith('update:')
  107. // for v-model update:xxx events, apply modifiers on args
  108. const modelArg = isModelListener && event.slice(7)
  109. if (modelArg && modelArg in props) {
  110. const modifiersKey = `${
  111. modelArg === 'modelValue' ? 'model' : modelArg
  112. }Modifiers`
  113. const { number, trim } = props[modifiersKey] || EMPTY_OBJ
  114. if (trim) {
  115. args = rawArgs.map(a => a.trim())
  116. } else if (number) {
  117. args = rawArgs.map(toNumber)
  118. }
  119. }
  120. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  121. devtoolsComponentEmit(instance, event, args)
  122. }
  123. if (__DEV__) {
  124. const lowerCaseEvent = event.toLowerCase()
  125. if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) {
  126. warn(
  127. `Event "${lowerCaseEvent}" is emitted in component ` +
  128. `${formatComponentName(
  129. instance,
  130. instance.type
  131. )} but the handler is registered for "${event}". ` +
  132. `Note that HTML attributes are case-insensitive and you cannot use ` +
  133. `v-on to listen to camelCase events when using in-DOM templates. ` +
  134. `You should probably use "${hyphenate(event)}" instead of "${event}".`
  135. )
  136. }
  137. }
  138. let handlerName
  139. let handler =
  140. props[(handlerName = toHandlerKey(event))] ||
  141. // also try camelCase event handler (#2249)
  142. props[(handlerName = toHandlerKey(camelize(event)))]
  143. // for v-model update:xxx events, also trigger kebab-case equivalent
  144. // for props passed via kebab-case
  145. if (!handler && isModelListener) {
  146. handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
  147. }
  148. if (handler) {
  149. callWithAsyncErrorHandling(
  150. handler,
  151. instance,
  152. ErrorCodes.COMPONENT_EVENT_HANDLER,
  153. args
  154. )
  155. }
  156. const onceHandler = props[handlerName + `Once`]
  157. if (onceHandler) {
  158. if (!instance.emitted) {
  159. instance.emitted = {} as Record<any, boolean>
  160. } else if (instance.emitted[handlerName]) {
  161. return
  162. }
  163. instance.emitted[handlerName] = true
  164. callWithAsyncErrorHandling(
  165. onceHandler,
  166. instance,
  167. ErrorCodes.COMPONENT_EVENT_HANDLER,
  168. args
  169. )
  170. }
  171. if (__COMPAT__) {
  172. compatModelEmit(instance, event, args)
  173. return compatInstanceEmit(instance, event, args)
  174. }
  175. }
  176. export function normalizeEmitsOptions(
  177. comp: ConcreteComponent,
  178. appContext: AppContext,
  179. asMixin = false
  180. ): ObjectEmitsOptions | null {
  181. const cache = appContext.emitsCache
  182. const cached = cache.get(comp)
  183. if (cached !== undefined) {
  184. return cached
  185. }
  186. const raw = comp.emits
  187. let normalized: ObjectEmitsOptions = {}
  188. // apply mixin/extends props
  189. let hasExtends = false
  190. if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
  191. const extendEmits = (raw: ComponentOptions) => {
  192. const normalizedFromExtend = normalizeEmitsOptions(raw, appContext, true)
  193. if (normalizedFromExtend) {
  194. hasExtends = true
  195. extend(normalized, normalizedFromExtend)
  196. }
  197. }
  198. if (!asMixin && appContext.mixins.length) {
  199. appContext.mixins.forEach(extendEmits)
  200. }
  201. if (comp.extends) {
  202. extendEmits(comp.extends)
  203. }
  204. if (comp.mixins) {
  205. comp.mixins.forEach(extendEmits)
  206. }
  207. }
  208. if (!raw && !hasExtends) {
  209. cache.set(comp, null)
  210. return null
  211. }
  212. if (isArray(raw)) {
  213. raw.forEach(key => (normalized[key] = null))
  214. } else {
  215. extend(normalized, raw)
  216. }
  217. cache.set(comp, normalized)
  218. return normalized
  219. }
  220. // Check if an incoming prop key is a declared emit event listener.
  221. // e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
  222. // both considered matched listeners.
  223. export function isEmitListener(
  224. options: ObjectEmitsOptions | null,
  225. key: string
  226. ): boolean {
  227. if (!options || !isOn(key)) {
  228. return false
  229. }
  230. if (__COMPAT__ && key.startsWith(compatModelEventPrefix)) {
  231. return true
  232. }
  233. key = key.slice(2).replace(/Once$/, '')
  234. return (
  235. hasOwn(options, key[0].toLowerCase() + key.slice(1)) ||
  236. hasOwn(options, hyphenate(key)) ||
  237. hasOwn(options, key)
  238. )
  239. }