Skip to content

feat(runtime-vapor): component props validator #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 123 additions & 10 deletions packages/runtime-vapor/__tests__/componentProps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ describe('component props (vapor)', () => {
expect(props.bar).toEqual({ a: 1 })
expect(props.baz).toEqual(defaultBaz)
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: (caching is not supported)
expect(defaultFn).toHaveBeenCalledTimes(2)
expect(defaultFn).toHaveBeenCalledTimes(3)
expect(defaultBaz).toHaveBeenCalledTimes(0)

// #999: updates should not cause default factory of unchanged prop to be
Expand Down Expand Up @@ -358,25 +358,138 @@ describe('component props (vapor)', () => {
reset()
})

test.todo('validator', () => {
// TODO: impl validator
describe('validator', () => {
test('validator should be called with two arguments', () => {
let args: any
const mockFn = vi.fn((..._args: any[]) => {
args = _args
return true
})

const Comp = defineComponent({
props: {
foo: {
type: Number,
validator: (value: any, props: any) => mockFn(value, props),
},
bar: {
type: Number,
},
},
render() {
const t0 = template('<div/>')
const n0 = t0()
return n0
},
})

const props = {
get foo() {
return 1
},
get bar() {
return 2
},
}

render(Comp, props, host)
expect(mockFn).toHaveBeenCalled()
// NOTE: Vapor Component props defined by getter. So, `props` not Equal to `{ foo: 1, bar: 2 }`
// expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
expect(args.length).toBe(2)
expect(args[0]).toBe(1)
expect(args[1].foo).toEqual(1)
expect(args[1].bar).toEqual(2)
})

// TODO: impl setter and warnner
test.todo(
'validator should not be able to mutate other props',
async () => {
const mockFn = vi.fn((...args: any[]) => true)
const Comp = defineComponent({
props: {
foo: {
type: Number,
validator: (value: any, props: any) => !!(props.bar = 1),
},
bar: {
type: Number,
validator: (value: any) => mockFn(value),
},
},
render() {
const t0 = template('<div/>')
const n0 = t0()
return n0
},
})

render(
Comp,
{
get foo() {
return 1
},
get bar() {
return 2
},
},
host,
)
expect(
`Set operation on key "bar" failed: target is readonly.`,
).toHaveBeenWarnedLast()
expect(mockFn).toHaveBeenCalledWith(2)
},
)
})

test.todo('warn props mutation', () => {
// TODO: impl warn
})

test.todo('warn absent required props', () => {
// TODO: impl warn
test('warn absent required props', () => {
const Comp = defineComponent({
props: {
bool: { type: Boolean, required: true },
str: { type: String, required: true },
num: { type: Number, required: true },
},
setup() {
return () => null
},
})
render(Comp, {}, host)
expect(`Missing required prop: "bool"`).toHaveBeenWarned()
expect(`Missing required prop: "str"`).toHaveBeenWarned()
expect(`Missing required prop: "num"`).toHaveBeenWarned()
})

test.todo('warn on type mismatch', () => {
// TODO: impl warn
})
// NOTE: type check is not supported in vapor
// test('warn on type mismatch', () => {})

// #3495
test.todo('should not warn required props using kebab-case', async () => {
// TODO: impl warn
test('should not warn required props using kebab-case', async () => {
const Comp = defineComponent({
props: {
fooBar: { type: String, required: true },
},
setup() {
return () => null
},
})

render(
Comp,
{
get ['foo-bar']() {
return 'hello'
},
},
host,
)
expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned()
})

test('props type support BigInt', () => {
Expand Down
77 changes: 75 additions & 2 deletions packages/runtime-vapor/src/componentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
isFunction,
isReservedProp,
} from '@vue/shared'
import { shallowReactive, toRaw } from '@vue/reactivity'
import { shallowReactive, shallowReadonly, toRaw } from '@vue/reactivity'
import { warn } from './warning'
import {
type Component,
type ComponentInternalInstance,
Expand All @@ -35,7 +36,7 @@ export interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null
required?: boolean
default?: D | DefaultFactory<D> | null | undefined | object
validator?(value: unknown): boolean
validator?(value: unknown, props: Data): boolean
/**
* @internal
*/
Expand Down Expand Up @@ -142,6 +143,11 @@ export function initProps(
}
}

// validation
if (__DEV__) {
validateProps(rawProps || {}, props, instance)
}

instance.props = shallowReactive(props)
}

Expand Down Expand Up @@ -263,3 +269,70 @@ function getTypeIndex(
}
return -1
}

/**
* dev only
*/
function validateProps(
rawProps: Data,
props: Data,
instance: ComponentInternalInstance,
) {
const resolvedValues = toRaw(props)
const options = instance.propsOptions[0]
for (const key in options) {
let opt = options[key]
if (opt == null) continue
validateProp(
key,
resolvedValues[key],
opt,
__DEV__ ? shallowReadonly(resolvedValues) : resolvedValues,
!hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key)),
)
}
}

/**
* dev only
*/
function validateProp(
name: string,
value: unknown,
prop: PropOptions,
props: Data,
isAbsent: boolean,
) {
const { required, validator } = prop
// 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 + '".')
}
}