Skip to content

Commit 12250a8

Browse files
authored
feat(runtime-vapor): component props (#40)
1 parent ecf7da9 commit 12250a8

File tree

6 files changed

+458
-22
lines changed

6 files changed

+458
-22
lines changed

packages/runtime-vapor/src/component.ts

+41-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
import { type Ref, EffectScope, ref } from '@vue/reactivity'
2-
import type { Block } from './render'
3-
import type { DirectiveBinding } from './directive'
1+
import { EffectScope, Ref, ref } from '@vue/reactivity'
2+
3+
import { EMPTY_OBJ } from '@vue/shared'
4+
import { Block } from './render'
5+
import { type DirectiveBinding } from './directive'
6+
import {
7+
type ComponentPropsOptions,
8+
type NormalizedPropsOptions,
9+
normalizePropsOptions,
10+
} from './componentProps'
11+
412
import type { Data } from '@vue/shared'
513

14+
export type Component = FunctionalComponent | ObjectComponent
15+
616
export type SetupFn = (props: any, ctx: any) => Block | Data
717
export type FunctionalComponent = SetupFn & {
18+
props: ComponentPropsOptions
819
render(ctx: any): Block
920
}
1021
export interface ObjectComponent {
22+
props: ComponentPropsOptions
1123
setup: SetupFn
1224
render(ctx: any): Block
1325
}
@@ -17,13 +29,22 @@ export interface ComponentInternalInstance {
1729
container: ParentNode
1830
block: Block | null
1931
scope: EffectScope
20-
2132
component: FunctionalComponent | ObjectComponent
22-
get isMounted(): boolean
23-
isMountedRef: Ref<boolean>
33+
propsOptions: NormalizedPropsOptions
34+
35+
// TODO: type
36+
proxy: Data | null
37+
38+
// state
39+
props: Data
40+
setupState: Data
2441

2542
/** directives */
2643
dirs: Map<Node, DirectiveBinding[]>
44+
45+
// lifecycle
46+
get isMounted(): boolean
47+
isMountedRef: Ref<boolean>
2748
// TODO: registory of provides, appContext, lifecycles, ...
2849
}
2950

@@ -51,14 +72,25 @@ export const createComponentInstance = (
5172
block: null,
5273
container: null!, // set on mount
5374
scope: new EffectScope(true /* detached */)!,
54-
5575
component,
76+
77+
// resolved props and emits options
78+
propsOptions: normalizePropsOptions(component),
79+
// emitsOptions: normalizeEmitsOptions(type, appContext), // TODO:
80+
81+
proxy: null,
82+
83+
// state
84+
props: EMPTY_OBJ,
85+
setupState: EMPTY_OBJ,
86+
87+
dirs: new Map(),
88+
89+
// lifecycle
5690
get isMounted() {
5791
return isMountedRef.value
5892
},
5993
isMountedRef,
60-
61-
dirs: new Map(),
6294
// TODO: registory of provides, appContext, lifecycles, ...
6395
}
6496
return instance
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
// NOTE: runtime-core/src/componentProps.ts
2+
3+
import {
4+
Data,
5+
EMPTY_ARR,
6+
EMPTY_OBJ,
7+
camelize,
8+
extend,
9+
hasOwn,
10+
hyphenate,
11+
isArray,
12+
isFunction,
13+
isReservedProp,
14+
} from '@vue/shared'
15+
import { shallowReactive, toRaw } from '@vue/reactivity'
16+
import { type ComponentInternalInstance, type Component } from './component'
17+
18+
export type ComponentPropsOptions<P = Data> =
19+
| ComponentObjectPropsOptions<P>
20+
| string[]
21+
22+
export type ComponentObjectPropsOptions<P = Data> = {
23+
[K in keyof P]: Prop<P[K]> | null
24+
}
25+
26+
export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
27+
28+
type DefaultFactory<T> = (props: Data) => T | null | undefined
29+
30+
export interface PropOptions<T = any, D = T> {
31+
type?: PropType<T> | true | null
32+
required?: boolean
33+
default?: D | DefaultFactory<D> | null | undefined | object
34+
validator?(value: unknown): boolean
35+
/**
36+
* @internal
37+
*/
38+
skipFactory?: boolean
39+
}
40+
41+
export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
42+
43+
type PropConstructor<T = any> =
44+
| { new (...args: any[]): T & {} }
45+
| { (): T }
46+
| PropMethod<T>
47+
48+
type PropMethod<T, TConstructor = any> = [T] extends [
49+
((...args: any) => any) | undefined,
50+
] // if is function with args, allowing non-required functions
51+
? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
52+
: never
53+
54+
enum BooleanFlags {
55+
shouldCast,
56+
shouldCastTrue,
57+
}
58+
59+
type NormalizedProp =
60+
| null
61+
| (PropOptions & {
62+
[BooleanFlags.shouldCast]?: boolean
63+
[BooleanFlags.shouldCastTrue]?: boolean
64+
})
65+
66+
export type NormalizedProps = Record<string, NormalizedProp>
67+
export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
68+
69+
export function initProps(
70+
instance: ComponentInternalInstance,
71+
rawProps: Data | null,
72+
) {
73+
const props: Data = {}
74+
75+
const [options, needCastKeys] = instance.propsOptions
76+
let rawCastValues: Data | undefined
77+
if (rawProps) {
78+
for (let key in rawProps) {
79+
// key, ref are reserved and never passed down
80+
if (isReservedProp(key)) {
81+
continue
82+
}
83+
84+
const valueGetter = () => rawProps[key]
85+
let camelKey
86+
if (options && hasOwn(options, (camelKey = camelize(key)))) {
87+
if (!needCastKeys || !needCastKeys.includes(camelKey)) {
88+
// NOTE: must getter
89+
// props[camelKey] = value
90+
Object.defineProperty(props, camelKey, {
91+
get() {
92+
return valueGetter()
93+
},
94+
})
95+
} else {
96+
// NOTE: must getter
97+
// ;(rawCastValues || (rawCastValues = {}))[camelKey] = value
98+
rawCastValues || (rawCastValues = {})
99+
Object.defineProperty(rawCastValues, camelKey, {
100+
get() {
101+
return valueGetter()
102+
},
103+
})
104+
}
105+
} else {
106+
// TODO:
107+
}
108+
}
109+
}
110+
111+
if (needCastKeys) {
112+
const rawCurrentProps = toRaw(props)
113+
const castValues = rawCastValues || EMPTY_OBJ
114+
for (let i = 0; i < needCastKeys.length; i++) {
115+
const key = needCastKeys[i]
116+
117+
// NOTE: must getter
118+
// props[key] = resolvePropValue(
119+
// options!,
120+
// rawCurrentProps,
121+
// key,
122+
// castValues[key],
123+
// instance,
124+
// !hasOwn(castValues, key),
125+
// )
126+
Object.defineProperty(props, key, {
127+
get() {
128+
return resolvePropValue(
129+
options!,
130+
rawCurrentProps,
131+
key,
132+
castValues[key],
133+
instance,
134+
!hasOwn(castValues, key),
135+
)
136+
},
137+
})
138+
}
139+
}
140+
141+
instance.props = shallowReactive(props)
142+
}
143+
144+
function resolvePropValue(
145+
options: NormalizedProps,
146+
props: Data,
147+
key: string,
148+
value: unknown,
149+
instance: ComponentInternalInstance,
150+
isAbsent: boolean,
151+
) {
152+
const opt = options[key]
153+
if (opt != null) {
154+
const hasDefault = hasOwn(opt, 'default')
155+
// default values
156+
if (hasDefault && value === undefined) {
157+
const defaultValue = opt.default
158+
if (
159+
opt.type !== Function &&
160+
!opt.skipFactory &&
161+
isFunction(defaultValue)
162+
) {
163+
// TODO: caching?
164+
// const { propsDefaults } = instance
165+
// if (key in propsDefaults) {
166+
// value = propsDefaults[key]
167+
// } else {
168+
// setCurrentInstance(instance)
169+
// value = propsDefaults[key] = defaultValue.call(
170+
// __COMPAT__ &&
171+
// isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
172+
// ? createPropsDefaultThis(instance, props, key)
173+
// : null,
174+
// props,
175+
// )
176+
// unsetCurrentInstance()
177+
// }
178+
} else {
179+
value = defaultValue
180+
}
181+
}
182+
// boolean casting
183+
if (opt[BooleanFlags.shouldCast]) {
184+
if (isAbsent && !hasDefault) {
185+
value = false
186+
} else if (
187+
opt[BooleanFlags.shouldCastTrue] &&
188+
(value === '' || value === hyphenate(key))
189+
) {
190+
value = true
191+
}
192+
}
193+
}
194+
return value
195+
}
196+
197+
export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
198+
// TODO: cahching?
199+
200+
const raw = comp.props as any
201+
const normalized: NormalizedPropsOptions[0] = {}
202+
const needCastKeys: NormalizedPropsOptions[1] = []
203+
204+
if (!raw) {
205+
return EMPTY_ARR as any
206+
}
207+
208+
if (isArray(raw)) {
209+
for (let i = 0; i < raw.length; i++) {
210+
const normalizedKey = camelize(raw[i])
211+
if (validatePropName(normalizedKey)) {
212+
normalized[normalizedKey] = EMPTY_OBJ
213+
}
214+
}
215+
} else if (raw) {
216+
for (const key in raw) {
217+
const normalizedKey = camelize(key)
218+
if (validatePropName(normalizedKey)) {
219+
const opt = raw[key]
220+
const prop: NormalizedProp = (normalized[normalizedKey] =
221+
isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
222+
if (prop) {
223+
const booleanIndex = getTypeIndex(Boolean, prop.type)
224+
const stringIndex = getTypeIndex(String, prop.type)
225+
prop[BooleanFlags.shouldCast] = booleanIndex > -1
226+
prop[BooleanFlags.shouldCastTrue] =
227+
stringIndex < 0 || booleanIndex < stringIndex
228+
// if the prop needs boolean casting or default value
229+
if (booleanIndex > -1 || hasOwn(prop, 'default')) {
230+
needCastKeys.push(normalizedKey)
231+
}
232+
}
233+
}
234+
}
235+
}
236+
237+
const res: NormalizedPropsOptions = [normalized, needCastKeys]
238+
return res
239+
}
240+
241+
function validatePropName(key: string) {
242+
if (key[0] !== '$') {
243+
return true
244+
}
245+
return false
246+
}
247+
248+
function getType(ctor: Prop<any>): string {
249+
const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/)
250+
return match ? match[2] : ctor === null ? 'null' : ''
251+
}
252+
253+
function isSameType(a: Prop<any>, b: Prop<any>): boolean {
254+
return getType(a) === getType(b)
255+
}
256+
257+
function getTypeIndex(
258+
type: Prop<any>,
259+
expectedTypes: PropType<any> | void | null | true,
260+
): number {
261+
if (isArray(expectedTypes)) {
262+
return expectedTypes.findIndex((t) => isSameType(t, type))
263+
} else if (isFunction(expectedTypes)) {
264+
return isSameType(expectedTypes, type) ? 0 : -1
265+
}
266+
return -1
267+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { hasOwn } from '@vue/shared'
2+
import { type ComponentInternalInstance } from './component'
3+
4+
export interface ComponentRenderContext {
5+
[key: string]: any
6+
_: ComponentInternalInstance
7+
}
8+
9+
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
10+
get({ _: instance }: ComponentRenderContext, key: string) {
11+
let normalizedProps
12+
const { setupState, props } = instance
13+
if (hasOwn(setupState, key)) {
14+
return setupState[key]
15+
} else if (
16+
(normalizedProps = instance.propsOptions[0]) &&
17+
hasOwn(normalizedProps, key)
18+
) {
19+
return props![key]
20+
}
21+
},
22+
}

0 commit comments

Comments
 (0)