Skip to content

Commit 2517319

Browse files
authored
feat: expose controlled values on useForm (#3924)
1 parent 75ba332 commit 2517319

File tree

6 files changed

+298
-76
lines changed

6 files changed

+298
-76
lines changed

docs/src/pages/api/use-form.mdx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ interface FormMeta<TValues extends Record<string, any>> {
171171
initialValues?: TValues;
172172
}
173173

174+
type InvalidSubmissionHandler<TValues extends GenericFormValues = GenericFormValues> = (
175+
ctx: InvalidSubmissionContext<TValues>
176+
) => void;
177+
178+
type HandleSubmitFactory<TValues extends GenericFormValues> = <TReturn = unknown>(
179+
cb: SubmissionHandler<TValues, TReturn>,
180+
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>
181+
) => (e?: Event) => Promise<TReturn | undefined>;
182+
174183
type useForm = (opts?: FormOptions) => {
175184
values: TValues; // current form values
176185
submitCount: Ref<number>; // the number of submission attempts
@@ -190,10 +199,7 @@ type useForm = (opts?: FormOptions) => {
190199
validateField(field: keyof TValues): Promise<ValidationResult>;
191200
useFieldModel<TPath extends keyof TValues>(path: MaybeRef<TPath>): Ref<TValues[TPath]>;
192201
useFieldModel<TPath extends keyof TValues>(paths: [...MaybeRef<TPath>[]]): MapValues<typeof paths, TValues>;
193-
handleSubmit<TReturn = unknown>(
194-
cb: SubmissionHandler<TValues, TReturn>,
195-
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>
196-
): (e?: Event) => Promise<TReturn | undefined>;
202+
handleSubmit: HandleSubmitFactory<TValues> & { withControlled: HandleSubmitFactory<TValues> };
197203
};
198204
```
199205

@@ -537,6 +543,26 @@ const onSubmit = handleSubmit((values, actions) => {
537543
});
538544
```
539545

546+
`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.
547+
548+
```vue
549+
<template>
550+
<form @submit="onSubmit"></form>
551+
</template>
552+
553+
<script setup>
554+
import { useForm } from 'vee-validate';
555+
556+
const { handleSubmit } = useForm();
557+
558+
const onSubmit = handleSubmit.withControlled(values => {
559+
// Send only controlled values to the API
560+
// Only fields declared with `useField` or `useFieldModel` will be printed
561+
alert(JSON.stringify(values, null, 2));
562+
});
563+
</script>
564+
```
565+
540566
<DocTip title="Virtual Forms">
541567

542568
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.

docs/src/pages/guide/composition-api/handling-forms.mdx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ const { handleSubmit, setFieldError, setErrors } = useForm();
391391
392392
const onSubmit = handleSubmit(async values => {
393393
// Send data to the API
394-
const response = await client.post('/users/');
394+
const response = await client.post('/users/', values);
395395
396396
// all good
397397
if (!response.errors) {
@@ -415,7 +415,7 @@ Alternatively you can use the `FormActions` passed as the second argument to the
415415
```js
416416
const onSubmit = handleSubmit(async (values, actions) => {
417417
// Send data to the API
418-
const response = await client.post('/users/');
418+
const response = await client.post('/users/', values);
419419
// ...
420420

421421
// set single field error
@@ -428,3 +428,56 @@ const onSubmit = handleSubmit(async (values, actions) => {
428428
actions.setErrors(response.errors);
429429
});
430430
```
431+
432+
## Controlled Values
433+
434+
The form values can be categorized into two categories:
435+
436+
- Controlled values: values that have a form input controlling them via `useField` or `<Field />` or via `useFieldModel` model binding.
437+
- Uncontrolled values: values that are inserted dynamically with `setFieldValue` or inserted initially with initial values.
438+
439+
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.
440+
441+
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:
442+
443+
```vue
444+
<template>
445+
<form @submit="onSubmit">
446+
<!-- some fields -->
447+
</form>
448+
</template>
449+
450+
<script setup>
451+
import { useForm } from 'vee-validate';
452+
453+
const { handleSubmit, controlledValues } = useForm();
454+
455+
const onSubmit = handleSubmit(async () => {
456+
// Send only controlled values to the API
457+
// Only fields declared with `useField` or `useFieldModel` will be sent
458+
const response = await client.post('/users/', controlledValues.value);
459+
});
460+
</script>
461+
```
462+
463+
Alternatively for less verbosity, you can create handlers with only the controlled values with `handleSubmit.withControlled` which has the same API as `handleSubmit`:
464+
465+
```vue
466+
<template>
467+
<form @submit="onSubmit">
468+
<!-- some fields -->
469+
</form>
470+
</template>
471+
472+
<script setup>
473+
import { useForm } from 'vee-validate';
474+
475+
const { handleSubmit } = useForm();
476+
477+
const onSubmit = handleSubmit.withControlled(async values => {
478+
// Send only controlled values to the API
479+
// Only fields declared with `useField` or `useFieldModel` will be sent
480+
const response = await client.post('/users/', values);
481+
});
482+
</script>
483+
```

packages/vee-validate/src/Form.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type FormSlotProps = UnwrapRef<
2222
| 'setFieldTouched'
2323
| 'setTouched'
2424
| 'resetForm'
25+
| 'controlledValues'
2526
>
2627
> & {
2728
handleSubmit: (evt: Event | SubmissionHandler, onSubmit?: SubmissionHandler) => Promise<unknown>;
@@ -80,6 +81,7 @@ const FormImpl = defineComponent({
8081
meta,
8182
isSubmitting,
8283
submitCount,
84+
controlledValues,
8385
validate,
8486
validateField,
8587
handleReset,
@@ -132,6 +134,7 @@ const FormImpl = defineComponent({
132134
values: values,
133135
isSubmitting: isSubmitting.value,
134136
submitCount: submitCount.value,
137+
controlledValues: controlledValues.value,
135138
validate,
136139
validateField,
137140
handleSubmit: handleScopedSlotSubmit,

packages/vee-validate/src/types.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export interface FormValidationResult<TValues> {
145145

146146
export interface SubmissionContext<TValues extends GenericFormValues = GenericFormValues> extends FormActions<TValues> {
147147
evt?: Event;
148+
controlledValues: Partial<TValues>;
148149
}
149150

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

181+
type HandleSubmitFactory<TValues extends GenericFormValues> = <TReturn = unknown>(
182+
cb: SubmissionHandler<TValues, TReturn>,
183+
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>
184+
) => (e?: Event) => Promise<TReturn | undefined>;
185+
180186
export interface PrivateFormContext<TValues extends Record<string, any> = Record<string, any>>
181187
extends FormActions<TValues> {
182188
formId: number;
183189
values: TValues;
190+
controlledValues: Ref<TValues>;
184191
fieldsByPath: Ref<FieldPathLookup>;
185192
fieldArrays: PrivateFieldArrayContext[];
186193
submitCount: Ref<number>;
@@ -198,10 +205,7 @@ export interface PrivateFormContext<TValues extends Record<string, any> = Record
198205
unsetInitialValue(path: string): void;
199206
register(field: PrivateFieldContext): void;
200207
unregister(field: PrivateFieldContext): void;
201-
handleSubmit<TReturn = unknown>(
202-
cb: SubmissionHandler<TValues, TReturn>,
203-
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>
204-
): (e?: Event) => Promise<TReturn | undefined>;
208+
handleSubmit: HandleSubmitFactory<TValues> & { withControlled: HandleSubmitFactory<TValues> };
205209
setFieldInitialValue(path: string, value: unknown): void;
206210
useFieldModel<TPath extends keyof TValues>(path: MaybeRef<TPath>): Ref<TValues[TPath]>;
207211
useFieldModel<TPath extends keyof TValues>(paths: [...MaybeRef<TPath>[]]): MapValues<typeof paths, TValues>;

packages/vee-validate/src/useForm.ts

Lines changed: 89 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
7171
): FormContext<TValues> {
7272
const formId = FORM_COUNTER++;
7373

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

161+
const controlledValues = computed(() => {
162+
return [...controlledModelPaths, ...keysOf(fieldsByPath.value)].reduce((acc, path) => {
163+
const value = getFromPath(formValues, path as string);
164+
setInPath(acc, path as string, value);
165+
166+
return acc;
167+
}, {} as TValues);
168+
});
169+
159170
const schema = opts?.validationSchema;
160171

161172
/**
@@ -221,10 +232,82 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
221232
}
222233
);
223234

235+
function makeSubmissionFactory(onlyControlled: boolean) {
236+
return function submitHandlerFactory<TReturn = unknown>(
237+
fn?: SubmissionHandler<TValues, TReturn>,
238+
onValidationError?: InvalidSubmissionHandler<TValues>
239+
) {
240+
return function submissionHandler(e: unknown) {
241+
if (e instanceof Event) {
242+
e.preventDefault();
243+
e.stopPropagation();
244+
}
245+
246+
// Touch all fields
247+
setTouched(
248+
keysOf(fieldsByPath.value).reduce((acc, field) => {
249+
acc[field] = true;
250+
251+
return acc;
252+
}, {} as Record<keyof TValues, boolean>)
253+
);
254+
255+
isSubmitting.value = true;
256+
submitCount.value++;
257+
return validate()
258+
.then(result => {
259+
const values = deepCopy(formValues);
260+
261+
if (result.valid && typeof fn === 'function') {
262+
const controlled = deepCopy(controlledValues.value);
263+
return fn(onlyControlled ? controlled : values, {
264+
evt: e as Event,
265+
controlledValues: controlled,
266+
setErrors,
267+
setFieldError,
268+
setTouched,
269+
setFieldTouched,
270+
setValues,
271+
setFieldValue,
272+
resetForm,
273+
});
274+
}
275+
276+
if (!result.valid && typeof onValidationError === 'function') {
277+
onValidationError({
278+
values,
279+
evt: e as Event,
280+
errors: result.errors,
281+
results: result.results,
282+
});
283+
}
284+
})
285+
.then(
286+
returnVal => {
287+
isSubmitting.value = false;
288+
289+
return returnVal;
290+
},
291+
err => {
292+
isSubmitting.value = false;
293+
294+
// re-throw the err so it doesn't go silent
295+
throw err;
296+
}
297+
);
298+
};
299+
};
300+
}
301+
302+
const handleSubmitImpl = makeSubmissionFactory(false);
303+
const handleSubmit: typeof handleSubmitImpl & { withControlled: typeof handleSubmitImpl } = handleSubmitImpl as any;
304+
handleSubmit.withControlled = makeSubmissionFactory(true);
305+
224306
const formCtx: PrivateFormContext<TValues> = {
225307
formId,
226308
fieldsByPath,
227309
values: formValues,
310+
controlledValues,
228311
errorBag,
229312
errors,
230313
schema,
@@ -368,6 +451,8 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
368451
}
369452
);
370453

454+
controlledModelPaths.add(unref(path) as string);
455+
371456
return value;
372457
}
373458

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

633-
function handleSubmit<TReturn = unknown>(
634-
fn?: SubmissionHandler<TValues, TReturn>,
635-
onValidationError?: InvalidSubmissionHandler<TValues>
636-
) {
637-
return function submissionHandler(e: unknown) {
638-
if (e instanceof Event) {
639-
e.preventDefault();
640-
e.stopPropagation();
641-
}
642-
643-
// Touch all fields
644-
setTouched(
645-
keysOf(fieldsByPath.value).reduce((acc, field) => {
646-
acc[field] = true;
647-
648-
return acc;
649-
}, {} as Record<keyof TValues, boolean>)
650-
);
651-
652-
isSubmitting.value = true;
653-
submitCount.value++;
654-
return validate()
655-
.then(result => {
656-
if (result.valid && typeof fn === 'function') {
657-
return fn(deepCopy(formValues), {
658-
evt: e as Event,
659-
setErrors,
660-
setFieldError,
661-
setTouched,
662-
setFieldTouched,
663-
setValues,
664-
setFieldValue,
665-
resetForm,
666-
});
667-
}
668-
669-
if (!result.valid && typeof onValidationError === 'function') {
670-
onValidationError({
671-
values: deepCopy(formValues),
672-
evt: e as Event,
673-
errors: result.errors,
674-
results: result.results,
675-
});
676-
}
677-
})
678-
.then(
679-
returnVal => {
680-
isSubmitting.value = false;
681-
682-
return returnVal;
683-
},
684-
err => {
685-
isSubmitting.value = false;
686-
687-
// re-throw the err so it doesn't go silent
688-
throw err;
689-
}
690-
);
691-
};
692-
}
693-
694-
function setFieldInitialValue(path: string, value: unknown) {
695-
setInPath(initialValues.value, path, deepCopy(value));
696-
}
697-
698718
function unsetInitialValue(path: string) {
699719
unsetPath(initialValues.value, path);
700720
}
@@ -710,6 +730,10 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
710730
}
711731
}
712732

733+
function setFieldInitialValue(path: string, value: unknown) {
734+
setInPath(initialValues.value, path, deepCopy(value));
735+
}
736+
713737
async function _validateSchema(): Promise<FormValidationResult<TValues>> {
714738
const schemaValue = unref(schema);
715739
if (!schemaValue) {

0 commit comments

Comments
 (0)