componentProps.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import { toRaw, lock, unlock } from '@vue/reactivity'
  2. import {
  3. EMPTY_OBJ,
  4. camelize,
  5. hyphenate,
  6. capitalize,
  7. isString,
  8. isFunction,
  9. isArray,
  10. isObject,
  11. hasOwn,
  12. toRawType,
  13. PatchFlags,
  14. makeMap,
  15. isReservedProp
  16. } from '@vue/shared'
  17. import { warn } from './warning'
  18. import { Data, ComponentInternalInstance } from './component'
  19. export type ComponentPropsOptions<P = Data> =
  20. | ComponentObjectPropsOptions<P>
  21. | string[]
  22. export type ComponentObjectPropsOptions<P = Data> = {
  23. [K in keyof P]: Prop<P[K]> | null
  24. }
  25. export type Prop<T> = PropOptions<T> | PropType<T>
  26. type DefaultFactory<T> = () => T | null | undefined
  27. interface PropOptions<T = any> {
  28. type?: PropType<T> | true | null
  29. required?: boolean
  30. default?: T | DefaultFactory<T> | null | undefined
  31. validator?(value: unknown): boolean
  32. }
  33. export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
  34. type PropConstructor<T = any> = { new (...args: any[]): T & object } | { (): T }
  35. type RequiredKeys<T, MakeDefaultRequired> = {
  36. [K in keyof T]: T[K] extends
  37. | { required: true }
  38. | (MakeDefaultRequired extends true ? { default: any } : never)
  39. ? K
  40. : never
  41. }[keyof T]
  42. type OptionalKeys<T, MakeDefaultRequired> = Exclude<
  43. keyof T,
  44. RequiredKeys<T, MakeDefaultRequired>
  45. >
  46. type InferPropType<T> = T extends null
  47. ? any // null & true would fail to infer
  48. : T extends { type: null | true }
  49. ? any // somehow `ObjectConstructor` when inferred from { (): T } becomes `any`
  50. : T extends ObjectConstructor | { type: ObjectConstructor }
  51. ? { [key: string]: any }
  52. : T extends Prop<infer V> ? V : T
  53. export type ExtractPropTypes<
  54. O,
  55. MakeDefaultRequired extends boolean = true
  56. > = O extends object
  57. ? { [K in RequiredKeys<O, MakeDefaultRequired>]: InferPropType<O[K]> } &
  58. { [K in OptionalKeys<O, MakeDefaultRequired>]?: InferPropType<O[K]> }
  59. : { [K in string]: any }
  60. const enum BooleanFlags {
  61. shouldCast,
  62. shouldCastTrue
  63. }
  64. type NormalizedProp =
  65. | null
  66. | (PropOptions & {
  67. [BooleanFlags.shouldCast]?: boolean
  68. [BooleanFlags.shouldCastTrue]?: boolean
  69. })
  70. // normalized value is a tuple of the actual normalized options
  71. // and an array of prop keys that need value casting (booleans and defaults)
  72. type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
  73. // resolve raw VNode data.
  74. // - filter out reserved keys (key, ref)
  75. // - extract class and style into $attrs (to be merged onto child
  76. // component root)
  77. // - for the rest:
  78. // - if has declared props: put declared ones in `props`, the rest in `attrs`
  79. // - else: everything goes in `props`.
  80. export function resolveProps(
  81. instance: ComponentInternalInstance,
  82. rawProps: Data | null,
  83. _options: ComponentPropsOptions | void
  84. ) {
  85. const hasDeclaredProps = _options != null
  86. if (!rawProps && !hasDeclaredProps) {
  87. return
  88. }
  89. const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
  90. const props: Data = {}
  91. let attrs: Data | undefined = undefined
  92. // update the instance propsProxy (passed to setup()) to trigger potential
  93. // changes
  94. const propsProxy = instance.propsProxy
  95. const setProp = propsProxy
  96. ? (key: string, val: unknown) => {
  97. props[key] = val
  98. propsProxy[key] = val
  99. }
  100. : (key: string, val: unknown) => {
  101. props[key] = val
  102. }
  103. // allow mutation of propsProxy (which is readonly by default)
  104. unlock()
  105. if (rawProps != null) {
  106. for (const key in rawProps) {
  107. const value = rawProps[key]
  108. // key, ref are reserved and never passed down
  109. if (isReservedProp(key)) {
  110. continue
  111. }
  112. // prop option names are camelized during normalization, so to support
  113. // kebab -> camel conversion here we need to camelize the key.
  114. if (hasDeclaredProps) {
  115. const camelKey = camelize(key)
  116. if (hasOwn(options, camelKey)) {
  117. setProp(camelKey, value)
  118. } else {
  119. // Any non-declared props are put into a separate `attrs` object
  120. // for spreading. Make sure to preserve original key casing
  121. ;(attrs || (attrs = {}))[key] = value
  122. }
  123. } else {
  124. setProp(key, value)
  125. }
  126. }
  127. }
  128. if (hasDeclaredProps) {
  129. // set default values & cast booleans
  130. for (let i = 0; i < needCastKeys.length; i++) {
  131. const key = needCastKeys[i]
  132. let opt = options[key]
  133. if (opt == null) continue
  134. const hasDefault = hasOwn(opt, 'default')
  135. const currentValue = props[key]
  136. // default values
  137. if (hasDefault && currentValue === undefined) {
  138. const defaultValue = opt.default
  139. setProp(key, isFunction(defaultValue) ? defaultValue() : defaultValue)
  140. }
  141. // boolean casting
  142. if (opt[BooleanFlags.shouldCast]) {
  143. if (!hasOwn(props, key) && !hasDefault) {
  144. setProp(key, false)
  145. } else if (
  146. opt[BooleanFlags.shouldCastTrue] &&
  147. (currentValue === '' || currentValue === hyphenate(key))
  148. ) {
  149. setProp(key, true)
  150. }
  151. }
  152. }
  153. // validation
  154. if (__DEV__ && rawProps) {
  155. for (const key in options) {
  156. let opt = options[key]
  157. if (opt == null) continue
  158. validateProp(key, props[key], opt, !hasOwn(props, key))
  159. }
  160. }
  161. } else {
  162. // if component has no declared props, $attrs === $props
  163. attrs = props
  164. }
  165. // in case of dynamic props, check if we need to delete keys from
  166. // the props proxy
  167. const { patchFlag } = instance.vnode
  168. if (
  169. propsProxy !== null &&
  170. (patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)
  171. ) {
  172. const rawInitialProps = toRaw(propsProxy)
  173. for (const key in rawInitialProps) {
  174. if (!hasOwn(props, key)) {
  175. delete propsProxy[key]
  176. }
  177. }
  178. }
  179. // lock readonly
  180. lock()
  181. instance.props = props
  182. instance.attrs = attrs || EMPTY_OBJ
  183. }
  184. const normalizationMap = new WeakMap<
  185. ComponentPropsOptions,
  186. NormalizedPropsOptions
  187. >()
  188. function validatePropName(key: string) {
  189. if (key[0] !== '$') {
  190. return true
  191. } else if (__DEV__) {
  192. warn(`Invalid prop name: "${key}" is a reserved property.`)
  193. }
  194. return false
  195. }
  196. function normalizePropsOptions(
  197. raw: ComponentPropsOptions | void
  198. ): NormalizedPropsOptions {
  199. if (!raw) {
  200. return [] as any
  201. }
  202. if (normalizationMap.has(raw)) {
  203. return normalizationMap.get(raw)!
  204. }
  205. const options: NormalizedPropsOptions[0] = {}
  206. const needCastKeys: NormalizedPropsOptions[1] = []
  207. if (isArray(raw)) {
  208. for (let i = 0; i < raw.length; i++) {
  209. if (__DEV__ && !isString(raw[i])) {
  210. warn(`props must be strings when using array syntax.`, raw[i])
  211. }
  212. const normalizedKey = camelize(raw[i])
  213. if (validatePropName(normalizedKey)) {
  214. options[normalizedKey] = EMPTY_OBJ
  215. }
  216. }
  217. } else {
  218. if (__DEV__ && !isObject(raw)) {
  219. warn(`invalid props options`, raw)
  220. }
  221. for (const key in raw) {
  222. const normalizedKey = camelize(key)
  223. if (validatePropName(normalizedKey)) {
  224. const opt = raw[key]
  225. const prop: NormalizedProp = (options[normalizedKey] =
  226. isArray(opt) || isFunction(opt) ? { type: opt } : opt)
  227. if (prop != null) {
  228. const booleanIndex = getTypeIndex(Boolean, prop.type)
  229. const stringIndex = getTypeIndex(String, prop.type)
  230. prop[BooleanFlags.shouldCast] = booleanIndex > -1
  231. prop[BooleanFlags.shouldCastTrue] =
  232. stringIndex < 0 || booleanIndex < stringIndex
  233. // if the prop needs boolean casting or default value
  234. if (booleanIndex > -1 || hasOwn(prop, 'default')) {
  235. needCastKeys.push(normalizedKey)
  236. }
  237. }
  238. }
  239. }
  240. }
  241. const normalized: NormalizedPropsOptions = [options, needCastKeys]
  242. normalizationMap.set(raw, normalized)
  243. return normalized
  244. }
  245. // use function string name to check type constructors
  246. // so that it works across vms / iframes.
  247. function getType(ctor: Prop<any>): string {
  248. const match = ctor && ctor.toString().match(/^\s*function (\w+)/)
  249. return match ? match[1] : ''
  250. }
  251. function isSameType(a: Prop<any>, b: Prop<any>): boolean {
  252. return getType(a) === getType(b)
  253. }
  254. function getTypeIndex(
  255. type: Prop<any>,
  256. expectedTypes: PropType<any> | void | null | true
  257. ): number {
  258. if (isArray(expectedTypes)) {
  259. for (let i = 0, len = expectedTypes.length; i < len; i++) {
  260. if (isSameType(expectedTypes[i], type)) {
  261. return i
  262. }
  263. }
  264. } else if (isFunction(expectedTypes)) {
  265. return isSameType(expectedTypes, type) ? 0 : -1
  266. }
  267. return -1
  268. }
  269. type AssertionResult = {
  270. valid: boolean
  271. expectedType: string
  272. }
  273. function validateProp(
  274. name: string,
  275. value: unknown,
  276. prop: PropOptions,
  277. isAbsent: boolean
  278. ) {
  279. const { type, required, validator } = prop
  280. // required!
  281. if (required && isAbsent) {
  282. warn('Missing required prop: "' + name + '"')
  283. return
  284. }
  285. // missing but optional
  286. if (value == null && !prop.required) {
  287. return
  288. }
  289. // type check
  290. if (type != null && type !== true) {
  291. let isValid = false
  292. const types = isArray(type) ? type : [type]
  293. const expectedTypes = []
  294. // value is valid as long as one of the specified types match
  295. for (let i = 0; i < types.length && !isValid; i++) {
  296. const { valid, expectedType } = assertType(value, types[i])
  297. expectedTypes.push(expectedType || '')
  298. isValid = valid
  299. }
  300. if (!isValid) {
  301. warn(getInvalidTypeMessage(name, value, expectedTypes))
  302. return
  303. }
  304. }
  305. // custom validator
  306. if (validator && !validator(value)) {
  307. warn('Invalid prop: custom validator check failed for prop "' + name + '".')
  308. }
  309. }
  310. const isSimpleType = /*#__PURE__*/ makeMap(
  311. 'String,Number,Boolean,Function,Symbol'
  312. )
  313. function assertType(value: unknown, type: PropConstructor): AssertionResult {
  314. let valid
  315. const expectedType = getType(type)
  316. if (isSimpleType(expectedType)) {
  317. const t = typeof value
  318. valid = t === expectedType.toLowerCase()
  319. // for primitive wrapper objects
  320. if (!valid && t === 'object') {
  321. valid = value instanceof type
  322. }
  323. } else if (expectedType === 'Object') {
  324. valid = toRawType(value) === 'Object'
  325. } else if (expectedType === 'Array') {
  326. valid = isArray(value)
  327. } else {
  328. valid = value instanceof type
  329. }
  330. return {
  331. valid,
  332. expectedType
  333. }
  334. }
  335. function getInvalidTypeMessage(
  336. name: string,
  337. value: unknown,
  338. expectedTypes: string[]
  339. ): string {
  340. let message =
  341. `Invalid prop: type check failed for prop "${name}".` +
  342. ` Expected ${expectedTypes.map(capitalize).join(', ')}`
  343. const expectedType = expectedTypes[0]
  344. const receivedType = toRawType(value)
  345. const expectedValue = styleValue(value, expectedType)
  346. const receivedValue = styleValue(value, receivedType)
  347. // check if we need to specify expected value
  348. if (
  349. expectedTypes.length === 1 &&
  350. isExplicable(expectedType) &&
  351. !isBoolean(expectedType, receivedType)
  352. ) {
  353. message += ` with value ${expectedValue}`
  354. }
  355. message += `, got ${receivedType} `
  356. // check if we need to specify received value
  357. if (isExplicable(receivedType)) {
  358. message += `with value ${receivedValue}.`
  359. }
  360. return message
  361. }
  362. function styleValue(value: unknown, type: string): string {
  363. if (type === 'String') {
  364. return `"${value}"`
  365. } else if (type === 'Number') {
  366. return `${Number(value)}`
  367. } else {
  368. return `${value}`
  369. }
  370. }
  371. function isExplicable(type: string): boolean {
  372. const explicitTypes = ['string', 'number', 'boolean']
  373. return explicitTypes.some(elem => type.toLowerCase() === elem)
  374. }
  375. function isBoolean(...args: string[]): boolean {
  376. return args.some(elem => elem.toLowerCase() === 'boolean')
  377. }