componentProps.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import { immutable, unwrap, lock, unlock } from '@vue/observer'
  2. import {
  3. EMPTY_OBJ,
  4. camelize,
  5. hyphenate,
  6. capitalize,
  7. isString,
  8. isFunction,
  9. isArray,
  10. isObject
  11. } from '@vue/shared'
  12. import { warn } from './warning'
  13. import { Data, ComponentInstance } from './component'
  14. export type ComponentPropsOptions<P = Data> = {
  15. [K in keyof P]: PropValidator<P[K]>
  16. }
  17. export type Prop<T> = { (): T } | { new (...args: any[]): T & object }
  18. export type PropType<T> = Prop<T> | Prop<T>[]
  19. export type PropValidator<T> = PropOptions<T> | PropType<T>
  20. export interface PropOptions<T = any> {
  21. type?: PropType<T> | true | null
  22. required?: boolean
  23. default?: T | null | undefined | (() => T | null | undefined)
  24. validator?(value: T): boolean
  25. }
  26. const enum BooleanFlags {
  27. shouldCast = '1',
  28. shouldCastTrue = '2'
  29. }
  30. type NormalizedProp = PropOptions & {
  31. [BooleanFlags.shouldCast]?: boolean
  32. [BooleanFlags.shouldCastTrue]?: boolean
  33. }
  34. type NormalizedPropsOptions = Record<string, NormalizedProp>
  35. const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$'
  36. // resolve raw VNode data.
  37. // - filter out reserved keys (key, ref, slots)
  38. // - extract class and style into $attrs (to be merged onto child
  39. // component root)
  40. // - for the rest:
  41. // - if has declared props: put declared ones in `props`, the rest in `attrs`
  42. // - else: everything goes in `props`.
  43. export function resolveProps(
  44. instance: ComponentInstance,
  45. rawProps: any,
  46. _options: ComponentPropsOptions | void
  47. ) {
  48. const hasDeclaredProps = _options != null
  49. const options = normalizePropsOptions(_options) as NormalizedPropsOptions
  50. if (!rawProps && !hasDeclaredProps) {
  51. return
  52. }
  53. const props: any = {}
  54. let attrs: any = void 0
  55. // update the instance propsProxy (passed to setup()) to trigger potential
  56. // changes
  57. const propsProxy = instance.propsProxy
  58. const setProp = propsProxy
  59. ? (key: string, val: any) => {
  60. props[key] = val
  61. propsProxy[key] = val
  62. }
  63. : (key: string, val: any) => {
  64. props[key] = val
  65. }
  66. // allow mutation of propsProxy (which is immutable by default)
  67. unlock()
  68. if (rawProps != null) {
  69. for (const key in rawProps) {
  70. // key, ref, slots are reserved
  71. if (key === 'key' || key === 'ref' || key === 'slots') {
  72. continue
  73. }
  74. // any non-declared data are put into a separate `attrs` object
  75. // for spreading
  76. if (hasDeclaredProps && !options.hasOwnProperty(key)) {
  77. ;(attrs || (attrs = {}))[key] = rawProps[key]
  78. } else {
  79. setProp(key, rawProps[key])
  80. }
  81. }
  82. }
  83. // set default values, cast booleans & run validators
  84. if (hasDeclaredProps) {
  85. for (const key in options) {
  86. let opt = options[key]
  87. if (opt == null) continue
  88. const isAbsent = !props.hasOwnProperty(key)
  89. const hasDefault = opt.hasOwnProperty('default')
  90. const currentValue = props[key]
  91. // default values
  92. if (hasDefault && currentValue === undefined) {
  93. const defaultValue = opt.default
  94. setProp(key, isFunction(defaultValue) ? defaultValue() : defaultValue)
  95. }
  96. // boolean casting
  97. if (opt[BooleanFlags.shouldCast]) {
  98. if (isAbsent && !hasDefault) {
  99. setProp(key, false)
  100. } else if (
  101. opt[BooleanFlags.shouldCastTrue] &&
  102. (currentValue === '' || currentValue === hyphenate(key))
  103. ) {
  104. setProp(key, true)
  105. }
  106. }
  107. // runtime validation
  108. if (__DEV__ && rawProps) {
  109. validateProp(key, unwrap(rawProps[key]), opt, isAbsent)
  110. }
  111. }
  112. } else {
  113. // if component has no declared props, $attrs === $props
  114. attrs = props
  115. }
  116. // lock immutable
  117. lock()
  118. instance.props = __DEV__ ? immutable(props) : props
  119. instance.attrs = options
  120. ? __DEV__
  121. ? immutable(attrs)
  122. : attrs
  123. : instance.props
  124. }
  125. const normalizationMap = new WeakMap()
  126. function normalizePropsOptions(
  127. raw: ComponentPropsOptions | void
  128. ): NormalizedPropsOptions | void {
  129. if (!raw) {
  130. return
  131. }
  132. if (normalizationMap.has(raw)) {
  133. return normalizationMap.get(raw)
  134. }
  135. const normalized: NormalizedPropsOptions = {}
  136. normalizationMap.set(raw, normalized)
  137. if (isArray(raw)) {
  138. for (let i = 0; i < raw.length; i++) {
  139. if (__DEV__ && !isString(raw[i])) {
  140. warn(`props must be strings when using array syntax.`, raw[i])
  141. }
  142. const normalizedKey = camelize(raw[i])
  143. if (!isReservedKey(normalizedKey)) {
  144. normalized[normalizedKey] = EMPTY_OBJ
  145. } else if (__DEV__) {
  146. warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`)
  147. }
  148. }
  149. } else {
  150. if (__DEV__ && !isObject(raw)) {
  151. warn(`invalid props options`, raw)
  152. }
  153. for (const key in raw) {
  154. const normalizedKey = camelize(key)
  155. if (!isReservedKey(normalizedKey)) {
  156. const opt = raw[key]
  157. const prop = (normalized[normalizedKey] =
  158. isArray(opt) || isFunction(opt) ? { type: opt } : opt)
  159. if (prop) {
  160. const booleanIndex = getTypeIndex(Boolean, prop.type)
  161. const stringIndex = getTypeIndex(String, prop.type)
  162. ;(prop as NormalizedProp)[BooleanFlags.shouldCast] = booleanIndex > -1
  163. ;(prop as NormalizedProp)[BooleanFlags.shouldCastTrue] =
  164. booleanIndex < stringIndex
  165. }
  166. } else if (__DEV__) {
  167. warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`)
  168. }
  169. }
  170. }
  171. return normalized
  172. }
  173. // use function string name to check type constructors
  174. // so that it works across vms / iframes.
  175. function getType(ctor: Prop<any>): string {
  176. const match = ctor && ctor.toString().match(/^\s*function (\w+)/)
  177. return match ? match[1] : ''
  178. }
  179. function isSameType(a: Prop<any>, b: Prop<any>): boolean {
  180. return getType(a) === getType(b)
  181. }
  182. function getTypeIndex(
  183. type: Prop<any>,
  184. expectedTypes: PropType<any> | void | null | true
  185. ): number {
  186. if (isArray(expectedTypes)) {
  187. for (let i = 0, len = expectedTypes.length; i < len; i++) {
  188. if (isSameType(expectedTypes[i], type)) {
  189. return i
  190. }
  191. }
  192. } else if (isObject(expectedTypes)) {
  193. return isSameType(expectedTypes, type) ? 0 : -1
  194. }
  195. return -1
  196. }
  197. type AssertionResult = {
  198. valid: boolean
  199. expectedType: string
  200. }
  201. function validateProp(
  202. name: string,
  203. value: any,
  204. prop: PropOptions<any>,
  205. isAbsent: boolean
  206. ) {
  207. const { type, required, validator } = prop
  208. // required!
  209. if (required && isAbsent) {
  210. warn('Missing required prop: "' + name + '"')
  211. return
  212. }
  213. // missing but optional
  214. if (value == null && !prop.required) {
  215. return
  216. }
  217. // type check
  218. if (type != null && type !== true) {
  219. let isValid = false
  220. const types = isArray(type) ? type : [type]
  221. const expectedTypes = []
  222. // value is valid as long as one of the specified types match
  223. for (let i = 0; i < types.length && !isValid; i++) {
  224. const { valid, expectedType } = assertType(value, types[i])
  225. expectedTypes.push(expectedType || '')
  226. isValid = valid
  227. }
  228. if (!isValid) {
  229. warn(getInvalidTypeMessage(name, value, expectedTypes))
  230. return
  231. }
  232. }
  233. // custom validator
  234. if (validator && !validator(value)) {
  235. warn('Invalid prop: custom validator check failed for prop "' + name + '".')
  236. }
  237. }
  238. const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
  239. function assertType(value: any, type: Prop<any>): AssertionResult {
  240. let valid
  241. const expectedType = getType(type)
  242. if (simpleCheckRE.test(expectedType)) {
  243. const t = typeof value
  244. valid = t === expectedType.toLowerCase()
  245. // for primitive wrapper objects
  246. if (!valid && t === 'object') {
  247. valid = value instanceof type
  248. }
  249. } else if (expectedType === 'Object') {
  250. valid = toRawType(value) === 'Object'
  251. } else if (expectedType === 'Array') {
  252. valid = isArray(value)
  253. } else {
  254. valid = value instanceof type
  255. }
  256. return {
  257. valid,
  258. expectedType
  259. }
  260. }
  261. function getInvalidTypeMessage(
  262. name: string,
  263. value: any,
  264. expectedTypes: string[]
  265. ): string {
  266. let message =
  267. `Invalid prop: type check failed for prop "${name}".` +
  268. ` Expected ${expectedTypes.map(capitalize).join(', ')}`
  269. const expectedType = expectedTypes[0]
  270. const receivedType = toRawType(value)
  271. const expectedValue = styleValue(value, expectedType)
  272. const receivedValue = styleValue(value, receivedType)
  273. // check if we need to specify expected value
  274. if (
  275. expectedTypes.length === 1 &&
  276. isExplicable(expectedType) &&
  277. !isBoolean(expectedType, receivedType)
  278. ) {
  279. message += ` with value ${expectedValue}`
  280. }
  281. message += `, got ${receivedType} `
  282. // check if we need to specify received value
  283. if (isExplicable(receivedType)) {
  284. message += `with value ${receivedValue}.`
  285. }
  286. return message
  287. }
  288. function styleValue(value: any, type: string): string {
  289. if (type === 'String') {
  290. return `"${value}"`
  291. } else if (type === 'Number') {
  292. return `${Number(value)}`
  293. } else {
  294. return `${value}`
  295. }
  296. }
  297. function toRawType(value: any): string {
  298. return Object.prototype.toString.call(value).slice(8, -1)
  299. }
  300. function isExplicable(type: string): boolean {
  301. const explicitTypes = ['string', 'number', 'boolean']
  302. return explicitTypes.some(elem => type.toLowerCase() === elem)
  303. }
  304. function isBoolean(...args: string[]): boolean {
  305. return args.some(elem => elem.toLowerCase() === 'boolean')
  306. }