diff --git a/packages/vee-validate/src/types/forms.ts b/packages/vee-validate/src/types/forms.ts index 57b3f0d1e..7d410c4ea 100644 --- a/packages/vee-validate/src/types/forms.ts +++ b/packages/vee-validate/src/types/forms.ts @@ -4,9 +4,10 @@ import { FieldValidationMetaInfo } from '../../../shared'; import { Path, PathValue } from './paths'; import { PartialDeep } from 'type-fest'; -export interface ValidationResult { +export interface ValidationResult { errors: string[]; valid: boolean; + value?: TValue; } export interface TypedSchemaError { @@ -77,17 +78,17 @@ export interface ValidationOptions { warn: boolean; } -export type FieldValidator = (opts?: Partial) => Promise; +export type FieldValidator = (opts?: Partial) => Promise>; -export interface PathStateConfig { +export interface PathStateConfig { bails: boolean; label: MaybeRefOrGetter; type: InputType; - validate: FieldValidator; + validate: FieldValidator; schema?: MaybeRefOrGetter; } -export interface PathState { +export interface PathState { id: number | number[]; path: string; touched: boolean; @@ -96,8 +97,8 @@ export interface PathState { required: boolean; validated: boolean; pending: boolean; - initialValue: TValue | undefined; - value: TValue | undefined; + initialValue: TInput | undefined; + value: TInput | undefined; errors: string[]; bails: boolean; label: string | undefined; @@ -108,7 +109,7 @@ export interface PathState { pendingUnmount: Record; pendingReset: boolean; }; - validate?: FieldValidator; + validate?: FieldValidator; } export interface FieldEntry { @@ -135,29 +136,29 @@ export interface PrivateFieldArrayContext extends FieldArrayCo path: MaybeRefOrGetter; } -export interface PrivateFieldContext { +export interface PrivateFieldContext { id: number; name: MaybeRef; - value: Ref; - meta: FieldMeta; + value: Ref; + meta: FieldMeta; errors: Ref; errorMessage: Ref; label?: MaybeRefOrGetter; type?: string; bails?: boolean; keepValueOnUnmount?: MaybeRefOrGetter; - checkedValue?: MaybeRefOrGetter; - uncheckedValue?: MaybeRefOrGetter; + checkedValue?: MaybeRefOrGetter; + uncheckedValue?: MaybeRefOrGetter; checked?: Ref; - resetField(state?: Partial>): void; + resetField(state?: Partial>): void; handleReset(): void; - validate: FieldValidator; + validate: FieldValidator; handleChange(e: Event | unknown, shouldValidate?: boolean): void; handleBlur(e?: Event, shouldValidate?: boolean): void; - setState(state: Partial>): void; + setState(state: Partial>): void; setTouched(isTouched: boolean): void; setErrors(message: string | string[]): void; - setValue(value: TValue, shouldValidate?: boolean): void; + setValue(value: TInput, shouldValidate?: boolean): void; } export type FieldContext = Omit, 'id' | 'instances'>; @@ -195,9 +196,9 @@ export interface FormActions { export interface FormValidationResult { valid: boolean; - results: Partial, ValidationResult>>; + results: Partial, ValidationResult>>; errors: Partial, string>>; - values?: TOutput; + values?: Partial; } export interface SubmissionContext extends FormActions { @@ -214,7 +215,7 @@ export interface InvalidSubmissionContext, string>>; - results: Partial, ValidationResult>>; + results: Partial, ValidationResult>>; } export type InvalidSubmissionHandler = ( @@ -306,8 +307,10 @@ export interface BaseInputBinds { onInput: (e: Event) => void; } -export interface PrivateFormContext - extends FormActions { +export interface PrivateFormContext< + TValues extends GenericObject = GenericObject, + TOutput extends GenericObject = TValues, +> extends FormActions { formId: number; values: TValues; initialValues: Ref>; @@ -323,14 +326,17 @@ export interface PrivateFormContext; validateSchema?: (mode: SchemaValidationMode) => Promise>; validate(opts?: Partial): Promise>; - validateField(field: Path, opts?: Partial): Promise; + validateField>( + field: TPath, + opts?: Partial, + ): Promise>; stageInitialValue(path: string, value: unknown, updateOriginal?: boolean): void; unsetInitialValue(path: string): void; handleSubmit: HandleSubmitFactory & { withControlled: HandleSubmitFactory }; setFieldInitialValue(path: string, value: unknown, updateOriginal?: boolean): void; createPathState>( path: MaybeRef, - config?: Partial, + config?: Partial>, ): PathState>; getPathState>(path: TPath): PathState> | undefined; getAllPathStates(): PathState[]; @@ -383,7 +389,7 @@ export interface PrivateFormContext & TExtras>; } -export interface FormContext +export interface FormContext extends Omit< PrivateFormContext, | 'formId' diff --git a/packages/vee-validate/src/useFieldState.ts b/packages/vee-validate/src/useFieldState.ts index bf9dac061..b31de2745 100644 --- a/packages/vee-validate/src/useFieldState.ts +++ b/packages/vee-validate/src/useFieldState.ts @@ -18,14 +18,14 @@ export interface FieldStateComposable { setState(state: Partial>): void; } -export interface StateInit { - modelValue: MaybeRef; +export interface StateInit { + modelValue: MaybeRef; form?: PrivateFormContext; bails: boolean; label?: MaybeRefOrGetter; type?: InputType; - validate?: FieldValidator; - schema?: MaybeRefOrGetter | undefined>; + validate?: FieldValidator; + schema?: MaybeRefOrGetter | undefined>; } let ID_COUNTER = 0; diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts index a6265b7bf..7d2239579 100644 --- a/packages/vee-validate/src/useForm.ts +++ b/packages/vee-validate/src/useForm.ts @@ -109,7 +109,7 @@ function resolveInitialValues(opt export function useForm< TValues extends GenericObject = GenericObject, - TOutput = TValues, + TOutput extends GenericObject = TValues, TSchema extends FormSchema | TypedSchema = | FormSchema | TypedSchema, @@ -266,10 +266,10 @@ export function useForm< const schema = opts?.validationSchema; - function createPathState( - path: MaybeRefOrGetter>, - config?: Partial, - ): PathState { + function createPathState>( + path: MaybeRefOrGetter, + config?: Partial>, + ): PathState { const initialValue = computed(() => getFromPath(initialValues.value, toValue(path))); const pathStateExists = pathStateLookup.value[toValue(path)]; const isCheckboxOrRadio = config?.type === 'checkbox' || config?.type === 'radio'; @@ -285,7 +285,7 @@ export function useForm< pathStateExists.fieldsCount++; pathStateExists.__flags.pendingUnmount[id] = false; - return pathStateExists as PathState; + return pathStateExists as PathState; } const currentValue = computed(() => getFromPath(formValues, toValue(path))); @@ -336,7 +336,7 @@ export function useForm< dirty: computed(() => { return !isEqual(unref(currentValue), unref(initialValue)); }), - }) as PathState; + }) as PathState; pathStates.value.push(state); pathStateLookup.value[pathValue] = state; @@ -529,9 +529,10 @@ export function useForm< if (result.valid && typeof fn === 'function') { const controlled = deepCopy(controlledValues.value); - let submittedValues = (onlyControlled ? controlled : values) as unknown as TOutput; + const submittedValues = (onlyControlled ? controlled : values) as unknown as TOutput; + if (result.values) { - submittedValues = result.values; + Object.assign(submittedValues, result.values); } return fn(submittedValues, { @@ -859,14 +860,16 @@ export function useForm< key: state.path, valid: true, errors: [], + value: undefined, }); } - return state.validate(opts).then((result: ValidationResult) => { + return state.validate(opts).then(result => { return { key: state.path, valid: result.valid, errors: result.errors, + value: result.value, }; }); }), @@ -874,14 +877,20 @@ export function useForm< isValidating.value = false; - const results: Partial> = {}; + const results: Partial>> = {}; const errors: Partial> = {}; + const values: Partial = {}; + for (const validation of validations) { results[validation.key as Path] = { valid: validation.valid, errors: validation.errors, }; + if (validation.value) { + setInPath(values, validation.key, validation.value); + } + if (validation.errors.length) { errors[validation.key as Path] = validation.errors[0]; } @@ -891,10 +900,14 @@ export function useForm< valid: validations.every(r => r.valid), results, errors, + values, }; } - async function validateField(path: Path, opts?: Partial): Promise { + async function validateField>( + path: TPath, + opts?: Partial, + ): Promise> { const state = findPathState(path); if (state && opts?.mode !== 'silent') { state.validated = true; @@ -1281,7 +1294,7 @@ function useFormInitialValues( }; } -function mergeValidationResults(a: ValidationResult, b?: ValidationResult): ValidationResult { +function mergeValidationResults(a: ValidationResult, b?: ValidationResult): ValidationResult { if (!b) { return a; } diff --git a/packages/vee-validate/src/useValidateField.ts b/packages/vee-validate/src/useValidateField.ts index f6a79992c..7763516e4 100644 --- a/packages/vee-validate/src/useValidateField.ts +++ b/packages/vee-validate/src/useValidateField.ts @@ -6,13 +6,13 @@ import { injectWithSelf, warn } from './utils'; /** * Validates a single field */ -export function useValidateField(path?: MaybeRefOrGetter) { +export function useValidateField(path?: MaybeRefOrGetter) { const form = injectWithSelf(FormContextKey); const field = path ? undefined : inject(FieldContextKey); - return function validateField(): Promise { + return function validateField(): Promise> { if (field) { - return field.validate(); + return field.validate() as Promise>; } if (form && path) { diff --git a/packages/vee-validate/src/validate.ts b/packages/vee-validate/src/validate.ts index aa7315751..71df5ff89 100644 --- a/packages/vee-validate/src/validate.ts +++ b/packages/vee-validate/src/validate.ts @@ -17,13 +17,13 @@ import { isCallable, FieldValidationMetaInfo } from '../../shared'; /** * Used internally */ -interface FieldValidationContext { +interface FieldValidationContext { name: string; label?: string; rules: - | GenericValidateFunction - | GenericValidateFunction[] - | TypedSchema + | GenericValidateFunction + | GenericValidateFunction[] + | TypedSchema | string | Record; bails: boolean; @@ -40,18 +40,18 @@ interface ValidationOptions { /** * Validates a value against the rules. */ -export async function validate( - value: TValue, +export async function validate( + value: TInput, rules: | string | Record - | GenericValidateFunction - | GenericValidateFunction[] - | TypedSchema, + | GenericValidateFunction + | GenericValidateFunction[] + | TypedSchema, options: ValidationOptions = {}, -): Promise { +): Promise> { const shouldBail = options?.bails; - const field: FieldValidationContext = { + const field: FieldValidationContext = { name: options?.name || '{field}', rules, label: options?.label, @@ -60,18 +60,20 @@ export async function validate( }; const result = await _validate(field, value); - const errors = result.errors; return { - errors, - valid: !errors.length, + ...result, + valid: !result.errors.length, }; } /** * Starts the validation process. */ -async function _validate(field: FieldValidationContext, value: TValue) { +async function _validate( + field: FieldValidationContext, + value: TInput, +): Promise, 'valid'>> { if (isTypedSchema(field.rules) || isYupValidator(field.rules)) { return validateFieldWithTypedSchema(value, field.rules); } @@ -217,6 +219,7 @@ async function validateFieldWithTypedSchema(value: unknown, schema: TypedSchema } return { + value: result.value, errors: messages, }; } @@ -302,7 +305,7 @@ export async function validateTypedSchema( const typedSchema = isTypedSchema(schema) ? schema : yupToTypedSchema(schema); const validationResult = await typedSchema.parse(deepCopy(values)); - const results: Partial, ValidationResult>> = {}; + const results: Partial, ValidationResult>> = {}; const errors: Partial, string>> = {}; for (const error of validationResult.errors) { const messages = error.errors; @@ -349,7 +352,7 @@ export async function validateObjectSchema( let isAllValid = true; const validationResults = await Promise.all(validations); - const results: Partial, ValidationResult>> = {}; + const results: Partial, ValidationResult>> = {}; const errors: Partial, string>> = {}; for (const result of validationResults) { results[result.path] = { diff --git a/packages/vee-validate/tests/useForm.spec.ts b/packages/vee-validate/tests/useForm.spec.ts index e99cfdf7a..43e016e88 100644 --- a/packages/vee-validate/tests/useForm.spec.ts +++ b/packages/vee-validate/tests/useForm.spec.ts @@ -240,6 +240,7 @@ describe('useForm()', () => { errors: [REQUIRED_MESSAGE], }, }, + values: {}, }); }); diff --git a/packages/yup/tests/yup.spec.ts b/packages/yup/tests/yup.spec.ts index 856125985..bad724978 100644 --- a/packages/yup/tests/yup.spec.ts +++ b/packages/yup/tests/yup.spec.ts @@ -545,3 +545,44 @@ test('reports required state for field-level schemas without a form context', as }), ); }); + +test('uses transformed value as submitted value', async () => { + const onSubmitSpy = vi.fn(); + let onSubmit!: () => void; + + const wrapper = mountWithHoc({ + setup() { + const { handleSubmit } = useForm<{ + req: string; + }>(); + + const { value } = useField('test', toTypedSchema(yup.string().transform(val => `modified: ${val}`))); + + // submit now + onSubmit = handleSubmit(onSubmitSpy); + + return { + value, + }; + }, + template: ` +
+ +
+ `, + }); + + const input = wrapper.$el.querySelector('input'); + + setValue(input, '12345678'); + await flushPromises(); + onSubmit(); + await flushPromises(); + await expect(onSubmitSpy).toHaveBeenCalledTimes(1); + await expect(onSubmitSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + test: 'modified: 12345678', + }), + expect.anything(), + ); +}); diff --git a/packages/zod/tests/zod.spec.ts b/packages/zod/tests/zod.spec.ts index a25342dda..b6d23888d 100644 --- a/packages/zod/tests/zod.spec.ts +++ b/packages/zod/tests/zod.spec.ts @@ -660,3 +660,45 @@ test('reports required state for field-level schemas without a form context', as }), ); }); + +test('uses transformed value as submitted value', async () => { + const onSubmitSpy = vi.fn(); + let onSubmit!: () => void; + + const wrapper = mountWithHoc({ + setup() { + const { handleSubmit } = useForm<{ + test: string; + }>(); + + const testRules = toTypedSchema(z.string().transform(value => `modified: ${value}`)); + const { value } = useField('test', testRules); + + // submit now + onSubmit = handleSubmit(onSubmitSpy); + + return { + value, + }; + }, + template: ` +
+ +
+ `, + }); + + const input = wrapper.$el.querySelector('input'); + + setValue(input, '12345678'); + await flushPromises(); + onSubmit(); + await flushPromises(); + await expect(onSubmitSpy).toHaveBeenCalledTimes(1); + await expect(onSubmitSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + test: 'modified: 12345678', + }), + expect.anything(), + ); +});