Skip to content

Commit

Permalink
fix: ensure transformed values in field-level schemas are used on submit
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Leckey <leckey.ryan@gmail.com>
  • Loading branch information
mehcode authored and logaretm committed Jun 2, 2024
1 parent 0b219b8 commit e29c760
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 82 deletions.
4 changes: 2 additions & 2 deletions packages/rules/src/toTypedSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { keysOf } from '../../vee-validate/src/utils';
import { TypedSchema, RawFormSchema, validateObject, TypedSchemaError, validate } from 'vee-validate';
import { TypedSchema, RawFormSchema, validateObject, TypedSchemaError, validate, GenericObject } from 'vee-validate';
import { Optional, isObject } from '../../shared';

export function toTypedSchema<TOutput = any, TInput extends Optional<TOutput> = Optional<TOutput>>(
Expand All @@ -21,7 +21,7 @@ export function toTypedSchema<TOutput = any, TInput extends Optional<TOutput> =
};
}

const result = await validateObject<TInput, TOutput>(rawSchema, values);
const result = await validateObject(rawSchema, values as GenericObject | undefined);

return {
errors: keysOf(result.errors).map(path => {
Expand Down
92 changes: 53 additions & 39 deletions packages/vee-validate/src/types/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import { FieldValidationMetaInfo } from '../../../shared';
import { Path, PathValue } from './paths';
import { PartialDeep } from 'type-fest';

export interface ValidationResult {
export interface ValidationResult<TValue = unknown> {
errors: string[];
valid: boolean;
value?: TValue;
}

export type FlattenAndMapPathsValidationResult<TInput extends GenericObject, TOutput extends GenericObject> = {
[K in Path<TInput>]: ValidationResult<TOutput[K]>;
};

export interface TypedSchemaError {
path?: string;
errors: string[];
Expand Down Expand Up @@ -81,17 +86,17 @@ export interface ValidationOptions {
warn: boolean;
}

export type FieldValidator = (opts?: Partial<ValidationOptions>) => Promise<ValidationResult>;
export type FieldValidator<TOutput> = (opts?: Partial<ValidationOptions>) => Promise<ValidationResult<TOutput>>;

export interface PathStateConfig {
export interface PathStateConfig<TOutput> {
bails: boolean;
label: MaybeRefOrGetter<string | undefined>;
type: InputType;
validate: FieldValidator;
validate: FieldValidator<TOutput>;
schema?: MaybeRefOrGetter<TypedSchema | undefined>;
}

export interface PathState<TValue = unknown> {
export interface PathState<TInput = unknown, TOutput = TInput> {
id: number | number[];
path: string;
touched: boolean;
Expand All @@ -100,8 +105,8 @@ export interface PathState<TValue = unknown> {
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;
Expand All @@ -112,7 +117,7 @@ export interface PathState<TValue = unknown> {
pendingUnmount: Record<string, boolean>;
pendingReset: boolean;
};
validate?: FieldValidator;
validate?: FieldValidator<TOutput>;
}

export interface FieldEntry<TValue = unknown> {
Expand All @@ -139,29 +144,29 @@ export interface PrivateFieldArrayContext<TValue = unknown> extends FieldArrayCo
path: MaybeRefOrGetter<string>;
}

export interface PrivateFieldContext<TValue = unknown> {
export interface PrivateFieldContext<TInput = unknown, TOutput = TInput> {
id: number;
name: MaybeRef<string>;
value: Ref<TValue>;
meta: FieldMeta<TValue>;
value: Ref<TInput>;
meta: FieldMeta<TInput>;
errors: Ref<string[]>;
errorMessage: Ref<string | undefined>;
label?: MaybeRefOrGetter<string | undefined>;
type?: string;
bails?: boolean;
keepValueOnUnmount?: MaybeRefOrGetter<boolean | undefined>;
checkedValue?: MaybeRefOrGetter<TValue>;
uncheckedValue?: MaybeRefOrGetter<TValue>;
checkedValue?: MaybeRefOrGetter<TInput>;
uncheckedValue?: MaybeRefOrGetter<TInput>;
checked?: Ref<boolean>;
resetField(state?: Partial<FieldState<TValue>>): void;
resetField(state?: Partial<FieldState<TInput>>): void;
handleReset(): void;
validate: FieldValidator;
validate: FieldValidator<TOutput>;
handleChange(e: Event | unknown, shouldValidate?: boolean): void;
handleBlur(e?: Event, shouldValidate?: boolean): void;
setState(state: Partial<FieldState<TValue>>): void;
setState(state: Partial<FieldState<TInput>>): 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<TValue = unknown> = Omit<PrivateFieldContext<TValue>, 'id' | 'instances'>;
Expand Down Expand Up @@ -197,43 +202,47 @@ export interface FormActions<TValues extends GenericObject, TOutput = TValues> {
resetField(field: Path<TValues>, state?: Partial<FieldState>): void;
}

export interface FormValidationResult<TValues, TOutput = TValues> {
export interface FormValidationResult<TInput extends GenericObject, TOutput extends GenericObject = TInput> {
valid: boolean;
results: Partial<Record<Path<TValues>, ValidationResult>>;
errors: Partial<Record<Path<TValues>, string>>;
values?: TOutput;
results: Partial<FlattenAndMapPathsValidationResult<TInput, TOutput>>;
errors: Partial<Record<Path<TInput>, string>>;
values?: Partial<TOutput>;
}

export interface SubmissionContext<TValues extends GenericObject = GenericObject> extends FormActions<TValues> {
export interface SubmissionContext<TInput extends GenericObject = GenericObject> extends FormActions<TInput> {
evt?: Event;
controlledValues: Partial<TValues>;
controlledValues: Partial<TInput>;
}

export type SubmissionHandler<TValues extends GenericObject = GenericObject, TOutput = TValues, TReturn = unknown> = (
export type SubmissionHandler<TInput extends GenericObject = GenericObject, TOutput = TInput, TReturn = unknown> = (
values: TOutput,
ctx: SubmissionContext<TValues>,
ctx: SubmissionContext<TInput>,
) => TReturn;

export interface InvalidSubmissionContext<TValues extends GenericObject = GenericObject> {
values: TValues;
export interface InvalidSubmissionContext<
TInput extends GenericObject = GenericObject,
TOutput extends GenericObject = TInput,
> {
values: TInput;
evt?: Event;
errors: Partial<Record<Path<TValues>, string>>;
results: Partial<Record<Path<TValues>, ValidationResult>>;
errors: Partial<Record<Path<TInput>, string>>;
results: FormValidationResult<TInput, TOutput>['results'];
}

export type InvalidSubmissionHandler<TValues extends GenericObject = GenericObject> = (
ctx: InvalidSubmissionContext<TValues>,
) => void;
export type InvalidSubmissionHandler<
TInput extends GenericObject = GenericObject,
TOutput extends GenericObject = TInput,
> = (ctx: InvalidSubmissionContext<TInput, TOutput>) => void;

export type RawFormSchema<TValues> = Record<Path<TValues>, string | GenericValidateFunction | GenericObject>;

export type FieldPathLookup<TValues extends GenericObject = GenericObject> = Partial<
Record<Path<TValues>, PrivateFieldContext | PrivateFieldContext[]>
>;

type HandleSubmitFactory<TValues extends GenericObject, TOutput = TValues> = <TReturn = unknown>(
type HandleSubmitFactory<TValues extends GenericObject, TOutput extends GenericObject = TValues> = <TReturn = unknown>(
cb: SubmissionHandler<TValues, TOutput, TReturn>,
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>,
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues, TOutput>,
) => (e?: Event) => Promise<TReturn | undefined>;

export type PublicPathState<TValue = unknown> = Omit<
Expand Down Expand Up @@ -310,8 +319,10 @@ export interface BaseInputBinds<TValue = unknown> {
onInput: (e: Event) => void;
}

export interface PrivateFormContext<TValues extends GenericObject = GenericObject, TOutput = TValues>
extends FormActions<TValues> {
export interface PrivateFormContext<
TValues extends GenericObject = GenericObject,
TOutput extends GenericObject = TValues,
> extends FormActions<TValues> {
formId: number;
values: TValues;
initialValues: Ref<Partial<TValues>>;
Expand All @@ -327,14 +338,17 @@ export interface PrivateFormContext<TValues extends GenericObject = GenericObjec
keepValuesOnUnmount: MaybeRef<boolean>;
validateSchema?: (mode: SchemaValidationMode) => Promise<FormValidationResult<TValues, TOutput>>;
validate(opts?: Partial<ValidationOptions>): Promise<FormValidationResult<TValues, TOutput>>;
validateField(field: Path<TValues>, opts?: Partial<ValidationOptions>): Promise<ValidationResult>;
validateField<TPath extends Path<TValues>>(
field: TPath,
opts?: Partial<ValidationOptions>,
): Promise<ValidationResult<TOutput[TPath]>>;
stageInitialValue(path: string, value: unknown, updateOriginal?: boolean): void;
unsetInitialValue(path: string): void;
handleSubmit: HandleSubmitFactory<TValues, TOutput> & { withControlled: HandleSubmitFactory<TValues, TOutput> };
setFieldInitialValue(path: string, value: unknown, updateOriginal?: boolean): void;
createPathState<TPath extends Path<TValues>>(
path: MaybeRef<TPath>,
config?: Partial<PathStateConfig>,
config?: Partial<PathStateConfig<TOutput[TPath]>>,
): PathState<PathValue<TValues, TPath>>;
getPathState<TPath extends Path<TValues>>(path: TPath): PathState<PathValue<TValues, TPath>> | undefined;
getAllPathStates(): PathState[];
Expand Down Expand Up @@ -387,7 +401,7 @@ export interface PrivateFormContext<TValues extends GenericObject = GenericObjec
): Ref<BaseInputBinds<TValue> & TExtras>;
}

export interface FormContext<TValues extends GenericObject = GenericObject, TOutput = TValues>
export interface FormContext<TValues extends GenericObject = GenericObject, TOutput extends GenericObject = TValues>
extends Omit<
PrivateFormContext<TValues, TOutput>,
| 'formId'
Expand Down
8 changes: 4 additions & 4 deletions packages/vee-validate/src/useFieldState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ export interface FieldStateComposable<TValue = unknown> {
setState(state: Partial<StateSetterInit<TValue>>): void;
}

export interface StateInit<TValue = unknown> {
modelValue: MaybeRef<TValue>;
export interface StateInit<TInput = unknown, TOutput = TInput> {
modelValue: MaybeRef<TInput>;
form?: PrivateFormContext;
bails: boolean;
label?: MaybeRefOrGetter<string | undefined>;
type?: InputType;
validate?: FieldValidator;
schema?: MaybeRefOrGetter<TypedSchema<TValue> | undefined>;
validate?: FieldValidator<TOutput>;
schema?: MaybeRefOrGetter<TypedSchema<TInput> | undefined>;
}

let ID_COUNTER = 0;
Expand Down
45 changes: 31 additions & 14 deletions packages/vee-validate/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ValidationResult,
FormState,
FormValidationResult,
FlattenAndMapPathsValidationResult,
PrivateFormContext,
FormContext,
FormErrors,
Expand Down Expand Up @@ -109,7 +110,7 @@ function resolveInitialValues<TValues extends GenericObject = GenericObject>(opt

export function useForm<
TValues extends GenericObject = GenericObject,
TOutput = TValues,
TOutput extends GenericObject = TValues,
TSchema extends FormSchema<TValues> | TypedSchema<TValues, TOutput> =
| FormSchema<TValues>
| TypedSchema<TValues, TOutput>,
Expand Down Expand Up @@ -266,10 +267,10 @@ export function useForm<

const schema = opts?.validationSchema;

function createPathState<TValue>(
path: MaybeRefOrGetter<Path<TValues>>,
config?: Partial<PathStateConfig>,
): PathState<TValue> {
function createPathState<TPath extends Path<TValues>>(
path: MaybeRefOrGetter<TPath>,
config?: Partial<PathStateConfig<TOutput[TPath]>>,
): PathState<TValues[TPath], TOutput[TPath]> {
const initialValue = computed(() => getFromPath(initialValues.value, toValue(path)));
const pathStateExists = pathStateLookup.value[toValue(path)];
const isCheckboxOrRadio = config?.type === 'checkbox' || config?.type === 'radio';
Expand All @@ -285,7 +286,7 @@ export function useForm<
pathStateExists.fieldsCount++;
pathStateExists.__flags.pendingUnmount[id] = false;

return pathStateExists as PathState<TValue>;
return pathStateExists as PathState<TValues[TPath], TOutput[TPath]>;
}

const currentValue = computed(() => getFromPath(formValues, toValue(path)));
Expand Down Expand Up @@ -336,7 +337,7 @@ export function useForm<
dirty: computed(() => {
return !isEqual(unref(currentValue), unref(initialValue));
}),
}) as PathState<TValue>;
}) as PathState<TValues[TPath], TOutput[TPath]>;

pathStates.value.push(state);
pathStateLookup.value[pathValue] = state;
Expand Down Expand Up @@ -510,7 +511,7 @@ export function useForm<
function makeSubmissionFactory(onlyControlled: boolean) {
return function submitHandlerFactory<TReturn = unknown>(
fn?: SubmissionHandler<TValues, TOutput, TReturn>,
onValidationError?: InvalidSubmissionHandler<TValues>,
onValidationError?: InvalidSubmissionHandler<TValues, TOutput>,
) {
return function submissionHandler(e: unknown) {
if (e instanceof Event) {
Expand All @@ -529,9 +530,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, {
Expand Down Expand Up @@ -859,29 +861,37 @@ 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,
};
});
}),
);

isValidating.value = false;

const results: Partial<FlattenAndSetPathsType<TValues, ValidationResult>> = {};
const results: Partial<FlattenAndMapPathsValidationResult<TValues, TOutput>> = {};
const errors: Partial<FlattenAndSetPathsType<TValues, string>> = {};
const values: Partial<TOutput> = {};

for (const validation of validations) {
results[validation.key as Path<TValues>] = {
valid: validation.valid,
errors: validation.errors,
};

if (validation.value) {
setInPath(values, validation.key, validation.value);
}

if (validation.errors.length) {
errors[validation.key as Path<TValues>] = validation.errors[0];
}
Expand All @@ -891,10 +901,14 @@ export function useForm<
valid: validations.every(r => r.valid),
results,
errors,
values,
};
}

async function validateField(path: Path<TValues>, opts?: Partial<ValidationOptions>): Promise<ValidationResult> {
async function validateField<TPath extends Path<TValues>>(
path: TPath,
opts?: Partial<ValidationOptions>,
): Promise<ValidationResult<TOutput[TPath]>> {
const state = findPathState(path);
if (state && opts?.mode !== 'silent') {
state.validated = true;
Expand Down Expand Up @@ -1291,7 +1305,10 @@ function useFormInitialValues<TValues extends GenericObject>(
};
}

function mergeValidationResults(a: ValidationResult, b?: ValidationResult): ValidationResult {
function mergeValidationResults<TValue extends GenericObject>(
a: ValidationResult<TValue>,
b?: ValidationResult<TValue>,
): ValidationResult<TValue> {
if (!b) {
return a;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/vee-validate/src/useValidateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { injectWithSelf, warn } from './utils';
/**
* Validates a single field
*/
export function useValidateField(path?: MaybeRefOrGetter<string>) {
export function useValidateField<TOutput>(path?: MaybeRefOrGetter<string>) {
const form = injectWithSelf(FormContextKey);
const field = path ? undefined : inject(FieldContextKey);

return function validateField(): Promise<ValidationResult> {
return function validateField(): Promise<ValidationResult<TOutput>> {
if (field) {
return field.validate();
return field.validate() as Promise<ValidationResult<TOutput>>;
}

if (form && path) {
Expand Down
Loading

0 comments on commit e29c760

Please sign in to comment.