import {
type Data,
EMPTY_ARR,
EMPTY_OBJ,
camelize,
extend,
hasOwn,
hyphenate,
isArray,
isFunction,
} from '@vue/shared'
import { baseWatch, shallowReactive } from '@vue/reactivity'
import { warn } from './warning'
import {
type Component,
type ComponentInternalInstance,
setCurrentInstance,
} from './component'
import { patchAttrs } from './componentAttrs'
import { createVaporPreScheduler } from './scheduler'
export type ComponentPropsOptions
=
| ComponentObjectPropsOptions
| string[]
export type ComponentObjectPropsOptions
= {
[K in keyof P]: Prop
| null
}
export type Prop = PropOptions | PropType
type DefaultFactory = (props: Data) => T | null | undefined
export interface PropOptions {
type?: PropType | true | null
required?: boolean
default?: D | DefaultFactory | null | undefined | object
validator?(value: unknown, props: Data): boolean
/**
* @internal
*/
skipFactory?: boolean
}
export type PropType = PropConstructor | PropConstructor[]
type PropConstructor =
| { new (...args: any[]): T & {} }
| { (): T }
| PropMethod
type PropMethod = [T] extends [
((...args: any) => any) | undefined,
] // if is function with args, allowing non-required functions
? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
: never
enum BooleanFlags {
shouldCast,
shouldCastTrue,
}
type NormalizedProp =
| null
| (PropOptions & {
[BooleanFlags.shouldCast]?: boolean
[BooleanFlags.shouldCastTrue]?: boolean
})
export type NormalizedProps = Record
export type NormalizedPropsOptions =
| [props: NormalizedProps, needCastKeys: string[]]
| []
type StaticProps = Record unknown>
type DynamicProps = () => Data
export type NormalizedRawProps = Array
export type RawProps = NormalizedRawProps | StaticProps | null
export function initProps(
instance: ComponentInternalInstance,
rawProps: RawProps,
isStateful: boolean,
) {
const props: Data = {}
const attrs = (instance.attrs = shallowReactive({}))
if (!rawProps) rawProps = []
else if (!isArray(rawProps)) rawProps = [rawProps]
instance.rawProps = rawProps
const [options] = instance.propsOptions
const hasDynamicProps = rawProps.some(isFunction)
if (options) {
if (hasDynamicProps) {
for (const key in options) {
const getter = () =>
getDynamicPropValue(rawProps as NormalizedRawProps, key)
registerProp(instance, props, key, getter, true)
}
} else {
for (const key in options) {
const rawKey = rawProps[0] && getRawKey(rawProps[0] as StaticProps, key)
if (rawKey) {
registerProp(
instance,
props,
key,
(rawProps[0] as StaticProps)[rawKey],
)
} else {
registerProp(instance, props, key, undefined, false, true)
}
}
}
}
// validation
if (__DEV__) {
validateProps(rawProps, props, options || {})
}
if (hasDynamicProps) {
baseWatch(() => patchAttrs(instance), undefined, {
scheduler: createVaporPreScheduler(instance),
})
} else {
patchAttrs(instance)
}
if (isStateful) {
instance.props = /* isSSR ? props : */ shallowReactive(props)
} else {
// functional w/ optional props, props === attrs
instance.props = instance.propsOptions === EMPTY_ARR ? attrs : props
}
}
function registerProp(
instance: ComponentInternalInstance,
props: Data,
rawKey: string,
getter?: (() => unknown) | (() => DynamicPropResult),
isDynamic?: boolean,
isAbsent?: boolean,
) {
const key = camelize(rawKey)
if (key in props) return
const [options, needCastKeys] = instance.propsOptions
const needCast = needCastKeys && needCastKeys.includes(key)
const withCast = (value: unknown, absent?: boolean) =>
resolvePropValue(options!, props, key, value, instance, absent)
if (isAbsent) {
props[key] = needCast ? withCast(undefined, true) : undefined
} else {
const get: () => unknown = isDynamic
? needCast
? () => withCast(...(getter!() as DynamicPropResult))
: () => (getter!() as DynamicPropResult)[0]
: needCast
? () => withCast(getter!())
: getter!
Object.defineProperty(props, key, {
get,
enumerable: true,
})
}
}
function getRawKey(obj: Data, key: string) {
return Object.keys(obj).find(k => camelize(k) === key)
}
type DynamicPropResult = [value: unknown, absent: boolean]
function getDynamicPropValue(
rawProps: NormalizedRawProps,
key: string,
): DynamicPropResult {
for (const props of Array.from(rawProps).reverse()) {
if (isFunction(props)) {
const resolved = props()
const rawKey = getRawKey(resolved, key)
if (rawKey) return [resolved[rawKey], false]
} else {
const rawKey = getRawKey(props, key)
if (rawKey) return [props[rawKey](), false]
}
}
return [undefined, true]
}
function resolvePropValue(
options: NormalizedProps,
props: Data,
key: string,
value: unknown,
instance: ComponentInternalInstance,
isAbsent?: boolean,
) {
const opt = options[key]
if (opt != null) {
const hasDefault = hasOwn(opt, 'default')
// default values
if (hasDefault && value === undefined) {
const defaultValue = opt.default
if (
opt.type !== Function &&
!opt.skipFactory &&
isFunction(defaultValue)
) {
// TODO: caching?
// const { propsDefaults } = instance
// if (key in propsDefaults) {
// value = propsDefaults[key]
// } else {
const reset = setCurrentInstance(instance)
value = defaultValue.call(null, props)
reset()
// }
} else {
value = defaultValue
}
}
// boolean casting
if (opt[BooleanFlags.shouldCast]) {
if (isAbsent && !hasDefault) {
value = false
} else if (
opt[BooleanFlags.shouldCastTrue] &&
(value === '' || value === hyphenate(key))
) {
value = true
}
}
}
return value
}
export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
// TODO: cahching?
const raw = comp.props
const normalized: NormalizedProps | undefined = {}
const needCastKeys: NormalizedPropsOptions[1] = []
if (!raw) {
return EMPTY_ARR as []
}
if (isArray(raw)) {
for (let i = 0; i < raw.length; i++) {
const normalizedKey = camelize(raw[i])
if (validatePropName(normalizedKey)) {
normalized[normalizedKey] = EMPTY_OBJ
}
}
} else {
for (const key in raw) {
const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) {
const opt = raw[key]
const prop: NormalizedProp = (normalized[normalizedKey] =
isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
if (prop) {
const booleanIndex = getTypeIndex(Boolean, prop.type)
const stringIndex = getTypeIndex(String, prop.type)
prop[BooleanFlags.shouldCast] = booleanIndex > -1
prop[BooleanFlags.shouldCastTrue] =
stringIndex < 0 || booleanIndex < stringIndex
// if the prop needs boolean casting or default value
if (booleanIndex > -1 || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
}
}
}
}
}
const res: NormalizedPropsOptions = [normalized, needCastKeys]
return res
}
function validatePropName(key: string) {
if (key[0] !== '$') {
return true
} else if (__DEV__) {
warn(`Invalid prop name: "${key}" is a reserved property.`)
}
return false
}
function getType(ctor: Prop): string {
const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/)
return match ? match[2] : ctor === null ? 'null' : ''
}
function isSameType(a: Prop, b: Prop): boolean {
return getType(a) === getType(b)
}
function getTypeIndex(
type: Prop,
expectedTypes: PropType | void | null | true,
): number {
if (isArray(expectedTypes)) {
return expectedTypes.findIndex(t => isSameType(t, type))
} else if (isFunction(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1
}
return -1
}
/**
* dev only
*/
function validateProps(
rawProps: NormalizedRawProps,
props: Data,
options: NormalizedProps,
) {
const presentKeys: string[] = []
for (const props of rawProps) {
presentKeys.push(...Object.keys(isFunction(props) ? props() : props))
}
for (const key in options) {
const opt = options[key]
if (opt != null)
validateProp(
key,
props[key],
opt,
props,
!presentKeys.some(k => camelize(k) === key),
)
}
}
/**
* dev only
*/
function validateProp(
name: string,
value: unknown,
option: PropOptions,
props: Data,
isAbsent: boolean,
) {
const { required, validator } = option
// required!
if (required && isAbsent) {
warn('Missing required prop: "' + name + '"')
return
}
// missing but optional
if (value == null && !required) {
return
}
// NOTE: type check is not supported in vapor
// // type check
// if (type != null && type !== true) {
// let isValid = false
// const types = isArray(type) ? type : [type]
// const expectedTypes = []
// // value is valid as long as one of the specified types match
// for (let i = 0; i < types.length && !isValid; i++) {
// const { valid, expectedType } = assertType(value, types[i])
// expectedTypes.push(expectedType || '')
// isValid = valid
// }
// if (!isValid) {
// warn(getInvalidTypeMessage(name, value, expectedTypes))
// return
// }
// }
// custom validator
if (validator && !validator(value, props)) {
warn('Invalid prop: custom validator check failed for prop "' + name + '".')
}
}