componentProps.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import {
  2. type Data,
  3. EMPTY_ARR,
  4. EMPTY_OBJ,
  5. camelize,
  6. extend,
  7. hasOwn,
  8. hyphenate,
  9. isArray,
  10. isFunction,
  11. } from '@vue/shared'
  12. import { baseWatch, shallowReactive } from '@vue/reactivity'
  13. import { warn } from './warning'
  14. import {
  15. type Component,
  16. type ComponentInternalInstance,
  17. setCurrentInstance,
  18. } from './component'
  19. import { patchAttrs } from './componentAttrs'
  20. import { createVaporPreScheduler } from './scheduler'
  21. export type ComponentPropsOptions<P = Data> =
  22. | ComponentObjectPropsOptions<P>
  23. | string[]
  24. export type ComponentObjectPropsOptions<P = Data> = {
  25. [K in keyof P]: Prop<P[K]> | null
  26. }
  27. export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
  28. type DefaultFactory<T> = (props: Data) => T | null | undefined
  29. export interface PropOptions<T = any, D = T> {
  30. type?: PropType<T> | true | null
  31. required?: boolean
  32. default?: D | DefaultFactory<D> | null | undefined | object
  33. validator?(value: unknown, props: Data): boolean
  34. /**
  35. * @internal
  36. */
  37. skipFactory?: boolean
  38. }
  39. export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
  40. type PropConstructor<T = any> =
  41. | { new (...args: any[]): T & {} }
  42. | { (): T }
  43. | PropMethod<T>
  44. type PropMethod<T, TConstructor = any> = [T] extends [
  45. ((...args: any) => any) | undefined,
  46. ] // if is function with args, allowing non-required functions
  47. ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
  48. : never
  49. enum BooleanFlags {
  50. shouldCast,
  51. shouldCastTrue,
  52. }
  53. type NormalizedProp =
  54. | null
  55. | (PropOptions & {
  56. [BooleanFlags.shouldCast]?: boolean
  57. [BooleanFlags.shouldCastTrue]?: boolean
  58. })
  59. export type NormalizedProps = Record<string, NormalizedProp>
  60. export type NormalizedPropsOptions =
  61. | [props: NormalizedProps, needCastKeys: string[]]
  62. | []
  63. type StaticProps = Record<string, () => unknown>
  64. type DynamicProps = () => Data
  65. export type NormalizedRawProps = Array<StaticProps | DynamicProps>
  66. export type RawProps = NormalizedRawProps | StaticProps | null
  67. export function initProps(
  68. instance: ComponentInternalInstance,
  69. rawProps: RawProps,
  70. isStateful: boolean,
  71. ) {
  72. const props: Data = {}
  73. const attrs = (instance.attrs = shallowReactive<Data>({}))
  74. if (!rawProps) rawProps = []
  75. else if (!isArray(rawProps)) rawProps = [rawProps]
  76. instance.rawProps = rawProps
  77. const [options] = instance.propsOptions
  78. const hasDynamicProps = rawProps.some(isFunction)
  79. if (options) {
  80. if (hasDynamicProps) {
  81. for (const key in options) {
  82. const getter = () =>
  83. getDynamicPropValue(rawProps as NormalizedRawProps, key)
  84. registerProp(instance, props, key, getter, true)
  85. }
  86. } else {
  87. for (const key in options) {
  88. const rawKey = rawProps[0] && getRawKey(rawProps[0] as StaticProps, key)
  89. if (rawKey) {
  90. registerProp(
  91. instance,
  92. props,
  93. key,
  94. (rawProps[0] as StaticProps)[rawKey],
  95. )
  96. } else {
  97. registerProp(instance, props, key, undefined, false, true)
  98. }
  99. }
  100. }
  101. }
  102. // validation
  103. if (__DEV__) {
  104. validateProps(rawProps, props, options || {})
  105. }
  106. if (hasDynamicProps) {
  107. baseWatch(() => patchAttrs(instance), undefined, {
  108. scheduler: createVaporPreScheduler(instance),
  109. })
  110. } else {
  111. patchAttrs(instance)
  112. }
  113. if (isStateful) {
  114. instance.props = /* isSSR ? props : */ shallowReactive(props)
  115. } else {
  116. // functional w/ optional props, props === attrs
  117. instance.props = instance.propsOptions === EMPTY_ARR ? attrs : props
  118. }
  119. }
  120. function registerProp(
  121. instance: ComponentInternalInstance,
  122. props: Data,
  123. rawKey: string,
  124. getter?: (() => unknown) | (() => DynamicPropResult),
  125. isDynamic?: boolean,
  126. isAbsent?: boolean,
  127. ) {
  128. const key = camelize(rawKey)
  129. if (key in props) return
  130. const [options, needCastKeys] = instance.propsOptions
  131. const needCast = needCastKeys && needCastKeys.includes(key)
  132. const withCast = (value: unknown, absent?: boolean) =>
  133. resolvePropValue(options!, props, key, value, instance, absent)
  134. if (isAbsent) {
  135. props[key] = needCast ? withCast(undefined, true) : undefined
  136. } else {
  137. const get: () => unknown = isDynamic
  138. ? needCast
  139. ? () => withCast(...(getter!() as DynamicPropResult))
  140. : () => (getter!() as DynamicPropResult)[0]
  141. : needCast
  142. ? () => withCast(getter!())
  143. : getter!
  144. Object.defineProperty(props, key, {
  145. get,
  146. enumerable: true,
  147. })
  148. }
  149. }
  150. function getRawKey(obj: Data, key: string) {
  151. return Object.keys(obj).find(k => camelize(k) === key)
  152. }
  153. type DynamicPropResult = [value: unknown, absent: boolean]
  154. function getDynamicPropValue(
  155. rawProps: NormalizedRawProps,
  156. key: string,
  157. ): DynamicPropResult {
  158. for (const props of Array.from(rawProps).reverse()) {
  159. if (isFunction(props)) {
  160. const resolved = props()
  161. const rawKey = getRawKey(resolved, key)
  162. if (rawKey) return [resolved[rawKey], false]
  163. } else {
  164. const rawKey = getRawKey(props, key)
  165. if (rawKey) return [props[rawKey](), false]
  166. }
  167. }
  168. return [undefined, true]
  169. }
  170. function resolvePropValue(
  171. options: NormalizedProps,
  172. props: Data,
  173. key: string,
  174. value: unknown,
  175. instance: ComponentInternalInstance,
  176. isAbsent?: boolean,
  177. ) {
  178. const opt = options[key]
  179. if (opt != null) {
  180. const hasDefault = hasOwn(opt, 'default')
  181. // default values
  182. if (hasDefault && value === undefined) {
  183. const defaultValue = opt.default
  184. if (
  185. opt.type !== Function &&
  186. !opt.skipFactory &&
  187. isFunction(defaultValue)
  188. ) {
  189. // TODO: caching?
  190. // const { propsDefaults } = instance
  191. // if (key in propsDefaults) {
  192. // value = propsDefaults[key]
  193. // } else {
  194. const reset = setCurrentInstance(instance)
  195. value = defaultValue.call(null, props)
  196. reset()
  197. // }
  198. } else {
  199. value = defaultValue
  200. }
  201. }
  202. // boolean casting
  203. if (opt[BooleanFlags.shouldCast]) {
  204. if (isAbsent && !hasDefault) {
  205. value = false
  206. } else if (
  207. opt[BooleanFlags.shouldCastTrue] &&
  208. (value === '' || value === hyphenate(key))
  209. ) {
  210. value = true
  211. }
  212. }
  213. }
  214. return value
  215. }
  216. export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
  217. // TODO: cahching?
  218. const raw = comp.props
  219. const normalized: NormalizedProps | undefined = {}
  220. const needCastKeys: NormalizedPropsOptions[1] = []
  221. if (!raw) {
  222. return EMPTY_ARR as []
  223. }
  224. if (isArray(raw)) {
  225. for (let i = 0; i < raw.length; i++) {
  226. const normalizedKey = camelize(raw[i])
  227. if (validatePropName(normalizedKey)) {
  228. normalized[normalizedKey] = EMPTY_OBJ
  229. }
  230. }
  231. } else {
  232. for (const key in raw) {
  233. const normalizedKey = camelize(key)
  234. if (validatePropName(normalizedKey)) {
  235. const opt = raw[key]
  236. const prop: NormalizedProp = (normalized[normalizedKey] =
  237. isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
  238. if (prop) {
  239. const booleanIndex = getTypeIndex(Boolean, prop.type)
  240. const stringIndex = getTypeIndex(String, prop.type)
  241. prop[BooleanFlags.shouldCast] = booleanIndex > -1
  242. prop[BooleanFlags.shouldCastTrue] =
  243. stringIndex < 0 || booleanIndex < stringIndex
  244. // if the prop needs boolean casting or default value
  245. if (booleanIndex > -1 || hasOwn(prop, 'default')) {
  246. needCastKeys.push(normalizedKey)
  247. }
  248. }
  249. }
  250. }
  251. }
  252. const res: NormalizedPropsOptions = [normalized, needCastKeys]
  253. return res
  254. }
  255. function validatePropName(key: string) {
  256. if (key[0] !== '$') {
  257. return true
  258. } else if (__DEV__) {
  259. warn(`Invalid prop name: "${key}" is a reserved property.`)
  260. }
  261. return false
  262. }
  263. function getType(ctor: Prop<any>): string {
  264. const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/)
  265. return match ? match[2] : ctor === null ? 'null' : ''
  266. }
  267. function isSameType(a: Prop<any>, b: Prop<any>): boolean {
  268. return getType(a) === getType(b)
  269. }
  270. function getTypeIndex(
  271. type: Prop<any>,
  272. expectedTypes: PropType<any> | void | null | true,
  273. ): number {
  274. if (isArray(expectedTypes)) {
  275. return expectedTypes.findIndex(t => isSameType(t, type))
  276. } else if (isFunction(expectedTypes)) {
  277. return isSameType(expectedTypes, type) ? 0 : -1
  278. }
  279. return -1
  280. }
  281. /**
  282. * dev only
  283. */
  284. function validateProps(
  285. rawProps: NormalizedRawProps,
  286. props: Data,
  287. options: NormalizedProps,
  288. ) {
  289. const presentKeys: string[] = []
  290. for (const props of rawProps) {
  291. presentKeys.push(...Object.keys(isFunction(props) ? props() : props))
  292. }
  293. for (const key in options) {
  294. const opt = options[key]
  295. if (opt != null)
  296. validateProp(
  297. key,
  298. props[key],
  299. opt,
  300. props,
  301. !presentKeys.some(k => camelize(k) === key),
  302. )
  303. }
  304. }
  305. /**
  306. * dev only
  307. */
  308. function validateProp(
  309. name: string,
  310. value: unknown,
  311. option: PropOptions,
  312. props: Data,
  313. isAbsent: boolean,
  314. ) {
  315. const { required, validator } = option
  316. // required!
  317. if (required && isAbsent) {
  318. warn('Missing required prop: "' + name + '"')
  319. return
  320. }
  321. // missing but optional
  322. if (value == null && !required) {
  323. return
  324. }
  325. // NOTE: type check is not supported in vapor
  326. // // type check
  327. // if (type != null && type !== true) {
  328. // let isValid = false
  329. // const types = isArray(type) ? type : [type]
  330. // const expectedTypes = []
  331. // // value is valid as long as one of the specified types match
  332. // for (let i = 0; i < types.length && !isValid; i++) {
  333. // const { valid, expectedType } = assertType(value, types[i])
  334. // expectedTypes.push(expectedType || '')
  335. // isValid = valid
  336. // }
  337. // if (!isValid) {
  338. // warn(getInvalidTypeMessage(name, value, expectedTypes))
  339. // return
  340. // }
  341. // }
  342. // custom validator
  343. if (validator && !validator(value, props)) {
  344. warn('Invalid prop: custom validator check failed for prop "' + name + '".')
  345. }
  346. }