diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts index 885e80090a1..3a16840b4f3 100644 --- a/packages/runtime-core/__tests__/componentProps.spec.ts +++ b/packages/runtime-core/__tests__/componentProps.spec.ts @@ -282,6 +282,56 @@ describe('component props', () => { expect(root.innerHTML).toBe('
2
') }) + describe('validator', () => { + test('validator should be called with two arguments', async () => { + const mockFn = vi.fn((...args: any[]) => true) + const Comp = defineComponent({ + props: { + foo: { + type: Number, + validator: (value, props) => mockFn(value, props) + }, + bar: { + type: Number + } + }, + template: `
` + }) + + // Note this one is using the main Vue render so it can compile template + // on the fly + const root = document.createElement('div') + domRender(h(Comp, { foo: 1, bar: 2 }), root) + expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 }) + }) + + test('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, props) => !!(props.bar = 1) + }, + bar: { + type: Number, + validator: value => mockFn(value) + } + }, + template: `
` + }) + + // Note this one is using the main Vue render so it can compile template + // on the fly + const root = document.createElement('div') + domRender(h(Comp, { foo: 1, bar: 2 }), root) + expect( + `Set operation on key "bar" failed: target is readonly.` + ).toHaveBeenWarnedLast() + expect(mockFn).toHaveBeenCalledWith(2) + }) + }) + test('warn props mutation', () => { let instance: ComponentInternalInstance let setupProps: any diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index bbe88bf7b82..4b7be8a8e73 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -2,7 +2,8 @@ import { toRaw, shallowReactive, trigger, - TriggerOpTypes + TriggerOpTypes, + shallowReadonly } from '@vue/reactivity' import { EMPTY_OBJ, @@ -57,7 +58,7 @@ export interface PropOptions { type?: PropType | true | null required?: boolean default?: D | DefaultFactory | null | undefined | object - validator?(value: unknown): boolean + validator?(value: unknown, props: Data): boolean /** * @internal */ @@ -634,6 +635,7 @@ function validateProps( key, resolvedValues[key], opt, + __DEV__ ? shallowReadonly(resolvedValues) : resolvedValues, !hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key)) ) } @@ -646,6 +648,7 @@ function validateProp( name: string, value: unknown, prop: PropOptions, + props: Data, isAbsent: boolean ) { const { type, required, validator, skipCheck } = prop @@ -675,7 +678,7 @@ function validateProp( } } // custom validator - if (validator && !validator(value)) { + if (validator && !validator(value, props)) { warn('Invalid prop: custom validator check failed for prop "' + name + '".') } }