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 committed May 25, 2024
1 parent cf1e1a6 commit 9ef7af0
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 62 deletions.
56 changes: 31 additions & 25 deletions packages/vee-validate/src/types/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { FieldValidationMetaInfo } from '../../../shared';
import { Path, PathValue } from './paths';
import { PartialDeep } from 'type-fest';

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

export interface TypedSchemaError {
Expand Down Expand Up @@ -77,17 +78,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 @@ -96,8 +97,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 @@ -108,7 +109,7 @@ export interface PathState<TValue = unknown> {
pendingUnmount: Record<string, boolean>;
pendingReset: boolean;
};
validate?: FieldValidator;
validate?: FieldValidator<TOutput>;
}

export interface FieldEntry<TValue = unknown> {
Expand All @@ -135,29 +136,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 @@ -195,9 +196,9 @@ export interface FormActions<TValues extends GenericObject, TOutput = TValues> {

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

export interface SubmissionContext<TValues extends GenericObject = GenericObject> extends FormActions<TValues> {
Expand All @@ -214,7 +215,7 @@ export interface InvalidSubmissionContext<TValues extends GenericObject = Generi
values: TValues;
evt?: Event;
errors: Partial<Record<Path<TValues>, string>>;
results: Partial<Record<Path<TValues>, ValidationResult>>;
results: Partial<Record<Path<TValues>, ValidationResult<never>>>;
}

export type InvalidSubmissionHandler<TValues extends GenericObject = GenericObject> = (
Expand Down Expand Up @@ -306,8 +307,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 @@ -323,14 +326,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 @@ -383,7 +389,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
39 changes: 26 additions & 13 deletions packages/vee-validate/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,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 +266,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 +285,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 +336,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 @@ -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, {
Expand Down Expand Up @@ -859,29 +860,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<FlattenAndSetPathsType<TValues, ValidationResult<never>>> = {};
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 +900,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 @@ -1281,7 +1294,7 @@ function useFormInitialValues<TValues extends GenericObject>(
};
}

function mergeValidationResults(a: ValidationResult, b?: ValidationResult): ValidationResult {
function mergeValidationResults(a: ValidationResult<never>, b?: ValidationResult<never>): ValidationResult<never> {
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 9ef7af0

Please sign in to comment.