Skip to content

Commit

Permalink
feat: expose controlled values on useForm (#3924)
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm authored Oct 9, 2022
1 parent 75ba332 commit 2517319
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 76 deletions.
34 changes: 30 additions & 4 deletions docs/src/pages/api/use-form.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ interface FormMeta<TValues extends Record<string, any>> {
initialValues?: TValues;
}

type InvalidSubmissionHandler<TValues extends GenericFormValues = GenericFormValues> = (
ctx: InvalidSubmissionContext<TValues>
) => void;

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

type useForm = (opts?: FormOptions) => {
values: TValues; // current form values
submitCount: Ref<number>; // the number of submission attempts
Expand All @@ -190,10 +199,7 @@ type useForm = (opts?: FormOptions) => {
validateField(field: keyof TValues): Promise<ValidationResult>;
useFieldModel<TPath extends keyof TValues>(path: MaybeRef<TPath>): Ref<TValues[TPath]>;
useFieldModel<TPath extends keyof TValues>(paths: [...MaybeRef<TPath>[]]): MapValues<typeof paths, TValues>;
handleSubmit<TReturn = unknown>(
cb: SubmissionHandler<TValues, TReturn>,
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>
): (e?: Event) => Promise<TReturn | undefined>;
handleSubmit: HandleSubmitFactory<TValues> & { withControlled: HandleSubmitFactory<TValues> };
};
```

Expand Down Expand Up @@ -537,6 +543,26 @@ const onSubmit = handleSubmit((values, actions) => {
});
```

`handleSubmit` contains a `withControlled` function that you can use to only submit fields controlled by `useField` or `useFieldModel`. Read the [guide](/guide/composition-api/handling-forms) for more information.

```vue
<template>
<form @submit="onSubmit"></form>
</template>
<script setup>
import { useForm } from 'vee-validate';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit.withControlled(values => {
// Send only controlled values to the API
// Only fields declared with `useField` or `useFieldModel` will be printed
alert(JSON.stringify(values, null, 2));
});
</script>
```

<DocTip title="Virtual Forms">

You can use `handleSubmit` to submit **virtual forms** that may use `form` elements or not. As you may have noticed the snippet above doesn't really care if you are using forms or not.
Expand Down
57 changes: 55 additions & 2 deletions docs/src/pages/guide/composition-api/handling-forms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ const { handleSubmit, setFieldError, setErrors } = useForm();
const onSubmit = handleSubmit(async values => {
// Send data to the API
const response = await client.post('/users/');
const response = await client.post('/users/', values);
// all good
if (!response.errors) {
Expand All @@ -415,7 +415,7 @@ Alternatively you can use the `FormActions` passed as the second argument to the
```js
const onSubmit = handleSubmit(async (values, actions) => {
// Send data to the API
const response = await client.post('/users/');
const response = await client.post('/users/', values);
// ...

// set single field error
Expand All @@ -428,3 +428,56 @@ const onSubmit = handleSubmit(async (values, actions) => {
actions.setErrors(response.errors);
});
```

## Controlled Values

The form values can be categorized into two categories:

- Controlled values: values that have a form input controlling them via `useField` or `<Field />` or via `useFieldModel` model binding.
- Uncontrolled values: values that are inserted dynamically with `setFieldValue` or inserted initially with initial values.

Sometimes you maybe only interested in controlled values. For example, your initial data contains noisy extra properties from your API and you wish to ignore them when submitting them back to your API.

When accessing `values` from `useForm` result or the submission handler you get all the values, both controlled and uncontrolled values. To get access to only the controlled values you can use `controlledValues` from the `useForm` result:

```vue
<template>
<form @submit="onSubmit">
<!-- some fields -->
</form>
</template>
<script setup>
import { useForm } from 'vee-validate';
const { handleSubmit, controlledValues } = useForm();
const onSubmit = handleSubmit(async () => {
// Send only controlled values to the API
// Only fields declared with `useField` or `useFieldModel` will be sent
const response = await client.post('/users/', controlledValues.value);
});
</script>
```

Alternatively for less verbosity, you can create handlers with only the controlled values with `handleSubmit.withControlled` which has the same API as `handleSubmit`:

```vue
<template>
<form @submit="onSubmit">
<!-- some fields -->
</form>
</template>
<script setup>
import { useForm } from 'vee-validate';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit.withControlled(async values => {
// Send only controlled values to the API
// Only fields declared with `useField` or `useFieldModel` will be sent
const response = await client.post('/users/', values);
});
</script>
```
3 changes: 3 additions & 0 deletions packages/vee-validate/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type FormSlotProps = UnwrapRef<
| 'setFieldTouched'
| 'setTouched'
| 'resetForm'
| 'controlledValues'
>
> & {
handleSubmit: (evt: Event | SubmissionHandler, onSubmit?: SubmissionHandler) => Promise<unknown>;
Expand Down Expand Up @@ -80,6 +81,7 @@ const FormImpl = defineComponent({
meta,
isSubmitting,
submitCount,
controlledValues,
validate,
validateField,
handleReset,
Expand Down Expand Up @@ -132,6 +134,7 @@ const FormImpl = defineComponent({
values: values,
isSubmitting: isSubmitting.value,
submitCount: submitCount.value,
controlledValues: controlledValues.value,
validate,
validateField,
handleSubmit: handleScopedSlotSubmit,
Expand Down
12 changes: 8 additions & 4 deletions packages/vee-validate/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export interface FormValidationResult<TValues> {

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

export type SubmissionHandler<TValues extends GenericFormValues = GenericFormValues, TReturn = unknown> = (
Expand Down Expand Up @@ -177,10 +178,16 @@ export type MapValues<T, TValues extends Record<string, any>> = {
: Ref<unknown>;
};

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

export interface PrivateFormContext<TValues extends Record<string, any> = Record<string, any>>
extends FormActions<TValues> {
formId: number;
values: TValues;
controlledValues: Ref<TValues>;
fieldsByPath: Ref<FieldPathLookup>;
fieldArrays: PrivateFieldArrayContext[];
submitCount: Ref<number>;
Expand All @@ -198,10 +205,7 @@ export interface PrivateFormContext<TValues extends Record<string, any> = Record
unsetInitialValue(path: string): void;
register(field: PrivateFieldContext): void;
unregister(field: PrivateFieldContext): void;
handleSubmit<TReturn = unknown>(
cb: SubmissionHandler<TValues, TReturn>,
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>
): (e?: Event) => Promise<TReturn | undefined>;
handleSubmit: HandleSubmitFactory<TValues> & { withControlled: HandleSubmitFactory<TValues> };
setFieldInitialValue(path: string, value: unknown): void;
useFieldModel<TPath extends keyof TValues>(path: MaybeRef<TPath>): Ref<TValues[TPath]>;
useFieldModel<TPath extends keyof TValues>(paths: [...MaybeRef<TPath>[]]): MapValues<typeof paths, TValues>;
Expand Down
154 changes: 89 additions & 65 deletions packages/vee-validate/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
): FormContext<TValues> {
const formId = FORM_COUNTER++;

const controlledModelPaths: Set<string> = new Set();

// Prevents fields from double resetting their values, which causes checkboxes to toggle their initial value
// TODO: This won't be needed if we centralize all the state inside the `form` for form inputs
let RESET_LOCK = false;
Expand Down Expand Up @@ -156,6 +158,15 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
// form meta aggregations
const meta = useFormMeta(fieldsByPath, formValues, originalInitialValues, errors);

const controlledValues = computed(() => {
return [...controlledModelPaths, ...keysOf(fieldsByPath.value)].reduce((acc, path) => {
const value = getFromPath(formValues, path as string);
setInPath(acc, path as string, value);

return acc;
}, {} as TValues);
});

const schema = opts?.validationSchema;

/**
Expand Down Expand Up @@ -221,10 +232,82 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
}
);

function makeSubmissionFactory(onlyControlled: boolean) {
return function submitHandlerFactory<TReturn = unknown>(
fn?: SubmissionHandler<TValues, TReturn>,
onValidationError?: InvalidSubmissionHandler<TValues>
) {
return function submissionHandler(e: unknown) {
if (e instanceof Event) {
e.preventDefault();
e.stopPropagation();
}

// Touch all fields
setTouched(
keysOf(fieldsByPath.value).reduce((acc, field) => {
acc[field] = true;

return acc;
}, {} as Record<keyof TValues, boolean>)
);

isSubmitting.value = true;
submitCount.value++;
return validate()
.then(result => {
const values = deepCopy(formValues);

if (result.valid && typeof fn === 'function') {
const controlled = deepCopy(controlledValues.value);
return fn(onlyControlled ? controlled : values, {
evt: e as Event,
controlledValues: controlled,
setErrors,
setFieldError,
setTouched,
setFieldTouched,
setValues,
setFieldValue,
resetForm,
});
}

if (!result.valid && typeof onValidationError === 'function') {
onValidationError({
values,
evt: e as Event,
errors: result.errors,
results: result.results,
});
}
})
.then(
returnVal => {
isSubmitting.value = false;

return returnVal;
},
err => {
isSubmitting.value = false;

// re-throw the err so it doesn't go silent
throw err;
}
);
};
};
}

const handleSubmitImpl = makeSubmissionFactory(false);
const handleSubmit: typeof handleSubmitImpl & { withControlled: typeof handleSubmitImpl } = handleSubmitImpl as any;
handleSubmit.withControlled = makeSubmissionFactory(true);

const formCtx: PrivateFormContext<TValues> = {
formId,
fieldsByPath,
values: formValues,
controlledValues,
errorBag,
errors,
schema,
Expand Down Expand Up @@ -368,6 +451,8 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
}
);

controlledModelPaths.add(unref(path) as string);

return value;
}

Expand Down Expand Up @@ -630,71 +715,6 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
return fieldInstance.validate();
}

function handleSubmit<TReturn = unknown>(
fn?: SubmissionHandler<TValues, TReturn>,
onValidationError?: InvalidSubmissionHandler<TValues>
) {
return function submissionHandler(e: unknown) {
if (e instanceof Event) {
e.preventDefault();
e.stopPropagation();
}

// Touch all fields
setTouched(
keysOf(fieldsByPath.value).reduce((acc, field) => {
acc[field] = true;

return acc;
}, {} as Record<keyof TValues, boolean>)
);

isSubmitting.value = true;
submitCount.value++;
return validate()
.then(result => {
if (result.valid && typeof fn === 'function') {
return fn(deepCopy(formValues), {
evt: e as Event,
setErrors,
setFieldError,
setTouched,
setFieldTouched,
setValues,
setFieldValue,
resetForm,
});
}

if (!result.valid && typeof onValidationError === 'function') {
onValidationError({
values: deepCopy(formValues),
evt: e as Event,
errors: result.errors,
results: result.results,
});
}
})
.then(
returnVal => {
isSubmitting.value = false;

return returnVal;
},
err => {
isSubmitting.value = false;

// re-throw the err so it doesn't go silent
throw err;
}
);
};
}

function setFieldInitialValue(path: string, value: unknown) {
setInPath(initialValues.value, path, deepCopy(value));
}

function unsetInitialValue(path: string) {
unsetPath(initialValues.value, path);
}
Expand All @@ -710,6 +730,10 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
}
}

function setFieldInitialValue(path: string, value: unknown) {
setInPath(initialValues.value, path, deepCopy(value));
}

async function _validateSchema(): Promise<FormValidationResult<TValues>> {
const schemaValue = unref(schema);
if (!schemaValue) {
Expand Down
Loading

0 comments on commit 2517319

Please sign in to comment.