wip: props resolving
This commit is contained in:
parent
5c069eeae7
commit
9dd133b1e9
@ -1,6 +1,7 @@
|
||||
import { VNode, normalizeVNode, VNodeChild } from './vnode'
|
||||
import { ReactiveEffect } from '@vue/observer'
|
||||
import { isFunction, EMPTY_OBJ } from '@vue/shared'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { resolveProps, ComponentPropsOptions } from './componentProps'
|
||||
|
||||
interface Value<T> {
|
||||
value: T
|
||||
@ -18,14 +19,28 @@ type ExtractPropTypes<PropOptions> = {
|
||||
: PropOptions[key] extends null | undefined ? any : PropOptions[key]
|
||||
}
|
||||
|
||||
interface ComponentPublicProperties<P, S> {
|
||||
$props: P
|
||||
export type Data = { [key: string]: any }
|
||||
|
||||
export interface ComponentPublicProperties<P = Data, S = Data> {
|
||||
$state: S
|
||||
$props: P
|
||||
$attrs: Data
|
||||
|
||||
// TODO
|
||||
$refs: Data
|
||||
$slots: Data
|
||||
}
|
||||
|
||||
interface RenderFunctionArg<B = Data, P = Data> {
|
||||
state: B
|
||||
props: P
|
||||
attrs: Data
|
||||
slots: Slots
|
||||
}
|
||||
|
||||
export interface ComponentOptions<
|
||||
RawProps = { [key: string]: Prop<any> },
|
||||
RawBindings = { [key: string]: any } | void,
|
||||
RawProps = ComponentPropsOptions,
|
||||
RawBindings = Data | void,
|
||||
Props = ExtractPropTypes<RawProps>,
|
||||
Bindings = UnwrapBindings<RawBindings>
|
||||
> {
|
||||
@ -33,13 +48,22 @@ export interface ComponentOptions<
|
||||
setup?: (props: Props) => RawBindings
|
||||
render?: <B extends Bindings>(
|
||||
this: ComponentPublicProperties<Props, B>,
|
||||
ctx: {
|
||||
state: B
|
||||
props: Props
|
||||
}
|
||||
ctx: RenderFunctionArg<B, Props>
|
||||
) => VNodeChild
|
||||
}
|
||||
|
||||
export interface FunctionalComponent<P = {}> {
|
||||
(ctx: RenderFunctionArg): any
|
||||
props?: ComponentPropsOptions<P>
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
export type Slot = (...args: any[]) => VNode[]
|
||||
|
||||
export type Slots = Readonly<{
|
||||
[name: string]: Slot
|
||||
}>
|
||||
|
||||
// no-op, for type inference only
|
||||
export function createComponent<
|
||||
RawProps,
|
||||
@ -55,19 +79,25 @@ export function createComponent<
|
||||
return options as any
|
||||
}
|
||||
|
||||
export interface ComponentHandle {
|
||||
type: Function | ComponentOptions
|
||||
export type ComponentHandle = {
|
||||
type: FunctionalComponent | ComponentOptions
|
||||
vnode: VNode | null
|
||||
next: VNode | null
|
||||
subTree: VNode | null
|
||||
update: ReactiveEffect
|
||||
}
|
||||
} & ComponentPublicProperties
|
||||
|
||||
export function renderComponentRoot(handle: ComponentHandle): VNode {
|
||||
const { type, vnode } = handle
|
||||
// TODO actually resolve props
|
||||
const { 0: props, 1: attrs } = resolveProps(
|
||||
(vnode as VNode).props,
|
||||
type.props
|
||||
)
|
||||
const renderArg = {
|
||||
props: (vnode as VNode).props || EMPTY_OBJ
|
||||
state: handle.$state,
|
||||
slots: handle.$slots,
|
||||
props,
|
||||
attrs
|
||||
}
|
||||
if (isFunction(type)) {
|
||||
return normalizeVNode(type(renderArg))
|
||||
|
326
packages/runtime-core/src/componentProps.ts
Normal file
326
packages/runtime-core/src/componentProps.ts
Normal file
@ -0,0 +1,326 @@
|
||||
import { immutable, unwrap } from '@vue/observer'
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
camelize,
|
||||
hyphenate,
|
||||
capitalize,
|
||||
isString,
|
||||
isFunction,
|
||||
isArray,
|
||||
isObject
|
||||
} from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { Data, ComponentHandle } from './component'
|
||||
|
||||
export type ComponentPropsOptions<P = Data> = {
|
||||
[K in keyof P]: PropValidator<P[K]>
|
||||
}
|
||||
|
||||
export type Prop<T> = { (): T } | { new (...args: any[]): T & object }
|
||||
|
||||
export type PropType<T> = Prop<T> | Prop<T>[]
|
||||
|
||||
export type PropValidator<T> = PropOptions<T> | PropType<T>
|
||||
|
||||
export interface PropOptions<T = any> {
|
||||
type?: PropType<T> | true | null
|
||||
required?: boolean
|
||||
default?: T | null | undefined | (() => T | null | undefined)
|
||||
validator?(value: T): boolean
|
||||
}
|
||||
|
||||
const enum BooleanFlags {
|
||||
shouldCast = '1',
|
||||
shouldCastTrue = '2'
|
||||
}
|
||||
|
||||
type NormalizedProp = PropOptions & {
|
||||
[BooleanFlags.shouldCast]?: boolean
|
||||
[BooleanFlags.shouldCastTrue]?: boolean
|
||||
}
|
||||
|
||||
type NormalizedPropsOptions = Record<string, NormalizedProp>
|
||||
|
||||
const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$'
|
||||
|
||||
export function initializeProps(
|
||||
instance: ComponentHandle,
|
||||
options: NormalizedPropsOptions | undefined,
|
||||
data: Data | null
|
||||
) {
|
||||
const { 0: props, 1: attrs } = resolveProps(data, options)
|
||||
instance.$props = __DEV__ ? immutable(props) : props
|
||||
instance.$attrs = options
|
||||
? __DEV__
|
||||
? immutable(attrs)
|
||||
: attrs
|
||||
: instance.$props
|
||||
}
|
||||
|
||||
// resolve raw VNode data.
|
||||
// - filter out reserved keys (key, ref, slots)
|
||||
// - extract class and style into $attrs (to be merged onto child
|
||||
// component root)
|
||||
// - for the rest:
|
||||
// - if has declared props: put declared ones in `props`, the rest in `attrs`
|
||||
// - else: everything goes in `props`.
|
||||
|
||||
const EMPTY_PROPS = [EMPTY_OBJ, EMPTY_OBJ] as [Data, Data]
|
||||
|
||||
export function resolveProps(
|
||||
rawData: any,
|
||||
_options: ComponentPropsOptions | void
|
||||
): [Data, Data] {
|
||||
const hasDeclaredProps = _options != null
|
||||
const options = normalizePropsOptions(_options) as NormalizedPropsOptions
|
||||
if (!rawData && !hasDeclaredProps) {
|
||||
return EMPTY_PROPS
|
||||
}
|
||||
const props: any = {}
|
||||
let attrs: any = void 0
|
||||
if (rawData != null) {
|
||||
for (const key in rawData) {
|
||||
// key, ref, slots are reserved
|
||||
if (key === 'key' || key === 'ref' || key === 'slots') {
|
||||
continue
|
||||
}
|
||||
// any non-declared data are put into a separate `attrs` object
|
||||
// for spreading
|
||||
if (hasDeclaredProps && !options.hasOwnProperty(key)) {
|
||||
;(attrs || (attrs = {}))[key] = rawData[key]
|
||||
} else {
|
||||
props[key] = rawData[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
// set default values, cast booleans & run validators
|
||||
if (hasDeclaredProps) {
|
||||
for (const key in options) {
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
const isAbsent = !props.hasOwnProperty(key)
|
||||
const hasDefault = opt.hasOwnProperty('default')
|
||||
const currentValue = props[key]
|
||||
// default values
|
||||
if (hasDefault && currentValue === undefined) {
|
||||
const defaultValue = opt.default
|
||||
props[key] = isFunction(defaultValue) ? defaultValue() : defaultValue
|
||||
}
|
||||
// boolean casting
|
||||
if (opt[BooleanFlags.shouldCast]) {
|
||||
if (isAbsent && !hasDefault) {
|
||||
props[key] = false
|
||||
} else if (
|
||||
opt[BooleanFlags.shouldCastTrue] &&
|
||||
(currentValue === '' || currentValue === hyphenate(key))
|
||||
) {
|
||||
props[key] = true
|
||||
}
|
||||
}
|
||||
// runtime validation
|
||||
if (__DEV__ && rawData) {
|
||||
validateProp(key, unwrap(rawData[key]), opt, isAbsent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if component has no declared props, $attrs === $props
|
||||
attrs = props
|
||||
}
|
||||
return [props, attrs]
|
||||
}
|
||||
|
||||
const normalizationMap = new WeakMap()
|
||||
|
||||
function normalizePropsOptions(
|
||||
raw: ComponentPropsOptions | void
|
||||
): NormalizedPropsOptions | void {
|
||||
if (!raw) {
|
||||
return
|
||||
}
|
||||
if (normalizationMap.has(raw)) {
|
||||
return normalizationMap.get(raw)
|
||||
}
|
||||
const normalized: NormalizedPropsOptions = {}
|
||||
normalizationMap.set(raw, normalized)
|
||||
if (isArray(raw)) {
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
if (__DEV__ && !isString(raw[i])) {
|
||||
warn(`props must be strings when using array syntax.`, raw[i])
|
||||
}
|
||||
const normalizedKey = camelize(raw[i])
|
||||
if (!isReservedKey(normalizedKey)) {
|
||||
normalized[normalizedKey] = EMPTY_OBJ
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (__DEV__ && !isObject(raw)) {
|
||||
warn(`invalid props options`, raw)
|
||||
}
|
||||
for (const key in raw) {
|
||||
const normalizedKey = camelize(key)
|
||||
if (!isReservedKey(normalizedKey)) {
|
||||
const opt = raw[key]
|
||||
const prop = (normalized[normalizedKey] =
|
||||
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
|
||||
if (prop) {
|
||||
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
||||
const stringIndex = getTypeIndex(String, prop.type)
|
||||
;(prop as NormalizedProp)[BooleanFlags.shouldCast] = booleanIndex > -1
|
||||
;(prop as NormalizedProp)[BooleanFlags.shouldCastTrue] =
|
||||
booleanIndex < stringIndex
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// use function string name to check type constructors
|
||||
// so that it works across vms / iframes.
|
||||
function getType(ctor: Prop<any>): string {
|
||||
const match = ctor && ctor.toString().match(/^\s*function (\w+)/)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
function isSameType(a: Prop<any>, b: Prop<any>): boolean {
|
||||
return getType(a) === getType(b)
|
||||
}
|
||||
|
||||
function getTypeIndex(
|
||||
type: Prop<any>,
|
||||
expectedTypes: PropType<any> | void | null | true
|
||||
): number {
|
||||
if (isArray(expectedTypes)) {
|
||||
for (let i = 0, len = expectedTypes.length; i < len; i++) {
|
||||
if (isSameType(expectedTypes[i], type)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
} else if (isObject(expectedTypes)) {
|
||||
return isSameType(expectedTypes, type) ? 0 : -1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type AssertionResult = {
|
||||
valid: boolean
|
||||
expectedType: string
|
||||
}
|
||||
|
||||
function validateProp(
|
||||
name: string,
|
||||
value: any,
|
||||
prop: PropOptions<any>,
|
||||
isAbsent: boolean
|
||||
) {
|
||||
const { type, required, validator } = prop
|
||||
// required!
|
||||
if (required && isAbsent) {
|
||||
warn('Missing required prop: "' + name + '"')
|
||||
return
|
||||
}
|
||||
// missing but optional
|
||||
if (value == null && !prop.required) {
|
||||
return
|
||||
}
|
||||
// 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)) {
|
||||
warn('Invalid prop: custom validator check failed for prop "' + name + '".')
|
||||
}
|
||||
}
|
||||
|
||||
const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
|
||||
|
||||
function assertType(value: any, type: Prop<any>): AssertionResult {
|
||||
let valid
|
||||
const expectedType = getType(type)
|
||||
if (simpleCheckRE.test(expectedType)) {
|
||||
const t = typeof value
|
||||
valid = t === expectedType.toLowerCase()
|
||||
// for primitive wrapper objects
|
||||
if (!valid && t === 'object') {
|
||||
valid = value instanceof type
|
||||
}
|
||||
} else if (expectedType === 'Object') {
|
||||
valid = toRawType(value) === 'Object'
|
||||
} else if (expectedType === 'Array') {
|
||||
valid = isArray(value)
|
||||
} else {
|
||||
valid = value instanceof type
|
||||
}
|
||||
return {
|
||||
valid,
|
||||
expectedType
|
||||
}
|
||||
}
|
||||
|
||||
function getInvalidTypeMessage(
|
||||
name: string,
|
||||
value: any,
|
||||
expectedTypes: string[]
|
||||
): string {
|
||||
let message =
|
||||
`Invalid prop: type check failed for prop "${name}".` +
|
||||
` Expected ${expectedTypes.map(capitalize).join(', ')}`
|
||||
const expectedType = expectedTypes[0]
|
||||
const receivedType = toRawType(value)
|
||||
const expectedValue = styleValue(value, expectedType)
|
||||
const receivedValue = styleValue(value, receivedType)
|
||||
// check if we need to specify expected value
|
||||
if (
|
||||
expectedTypes.length === 1 &&
|
||||
isExplicable(expectedType) &&
|
||||
!isBoolean(expectedType, receivedType)
|
||||
) {
|
||||
message += ` with value ${expectedValue}`
|
||||
}
|
||||
message += `, got ${receivedType} `
|
||||
// check if we need to specify received value
|
||||
if (isExplicable(receivedType)) {
|
||||
message += `with value ${receivedValue}.`
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
function styleValue(value: any, type: string): string {
|
||||
if (type === 'String') {
|
||||
return `"${value}"`
|
||||
} else if (type === 'Number') {
|
||||
return `${Number(value)}`
|
||||
} else {
|
||||
return `${value}`
|
||||
}
|
||||
}
|
||||
|
||||
function toRawType(value: any): string {
|
||||
return Object.prototype.toString.call(value).slice(8, -1)
|
||||
}
|
||||
|
||||
function isExplicable(type: string): boolean {
|
||||
const explicitTypes = ['string', 'number', 'boolean']
|
||||
return explicitTypes.some(elem => type.toLowerCase() === elem)
|
||||
}
|
||||
|
||||
function isBoolean(...args: string[]): boolean {
|
||||
return args.some(elem => elem.toLowerCase() === 'boolean')
|
||||
}
|
@ -349,11 +349,16 @@ export function createRenderer(options: RendererOptions) {
|
||||
anchor?: HostNode
|
||||
) {
|
||||
const instance: ComponentHandle = (vnode.component = {
|
||||
type: vnode.type as Function,
|
||||
type: vnode.type as any,
|
||||
vnode: null,
|
||||
next: null,
|
||||
subTree: null,
|
||||
update: null as any
|
||||
update: null as any,
|
||||
$attrs: EMPTY_OBJ,
|
||||
$props: EMPTY_OBJ,
|
||||
$refs: EMPTY_OBJ,
|
||||
$slots: EMPTY_OBJ,
|
||||
$state: EMPTY_OBJ
|
||||
})
|
||||
|
||||
// TODO call setup, handle bindings and render context
|
||||
|
@ -7,6 +7,15 @@ export {
|
||||
Text,
|
||||
Empty
|
||||
} from './vnode'
|
||||
|
||||
export {
|
||||
ComponentOptions,
|
||||
FunctionalComponent,
|
||||
Slots,
|
||||
Slot,
|
||||
createComponent
|
||||
} from './component'
|
||||
|
||||
export { createRenderer, RendererOptions } from './createRenderer'
|
||||
export * from '@vue/observer'
|
||||
export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
|
||||
export * from '@vue/observer'
|
||||
|
3
packages/runtime-core/src/warning.ts
Normal file
3
packages/runtime-core/src/warning.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function warn(...args: any[]) {
|
||||
// TODO
|
||||
}
|
Loading…
Reference in New Issue
Block a user