diff --git a/docs/content/api/use-form.md b/docs/content/api/use-form.md
index 332f04dea..57c59f0c7 100644
--- a/docs/content/api/use-form.md
+++ b/docs/content/api/use-form.md
@@ -28,6 +28,8 @@ export default {
};
```
+The `useForm` composable has very powerful type information and can be fully typed, for more information check the [useForm typing tutorial](/tutorials/use-form-types)
+
## API Reference
The full signature of the `useForm` function looks like this:
@@ -156,23 +158,26 @@ values.value; // { email: 'something@gmail.com', .... }
-`setFieldError: (field: string, message: string) => void`
+`setFieldError: (field: string, message: string | undefined) => void`
-Sets a field's error message, useful for setting messages form an API or that are not available as a validation rule.
+Sets a field's error message, useful for setting messages form an API or that are not available as a validation rule. Setting the message to `undefined` or an empty string clears the errors and marks the field as valid.
```js
const { setFieldError } = useForm();
setFieldError('email', 'this email is already taken');
+
+// Mark field as valid
+setFieldError('email', undefined);
```
If you try to set an error for field doesn't exist, it will not affect the form's overall validity and will be ignored.
-`setErrors: (fields: Record) => void`
+`setErrors: (fields: Record) => void`
@@ -184,9 +189,16 @@ const { setErrors } = useForm();
setErrors({
email: 'this email is already taken',
password: 'someone already has this password 🤪',
+ firstName: undefined, // clears errors and marks the field as valid
});
```
+
+
+Any missing fields you didn't pass to `setErrors` will be unaffected and their state will not change
+
+
+
`setFieldValue: (field: string, value: any) => void`
diff --git a/docs/content/guide/use-form-types.md b/docs/content/guide/use-form-types.md
new file mode 100644
index 000000000..098a2e6f8
--- /dev/null
+++ b/docs/content/guide/use-form-types.md
@@ -0,0 +1,73 @@
+---
+title: useForm() Field Types
+description: Using field types with useForm
+order: 7
+---
+
+# useForm() Field Types
+
+The `useForm` function exposed by vee-validate has field typing capabilities if you need it, getting type information for your fields and their values can be very powerful when building complex forms.
+
+By unlocking the field type you automatically get more strict information for the various properties/methods exposed by `useForm` like `setErrors` and `setTouched`. For more information about these properties/methods go to the [`useForm()` API reference](/api/use-form).
+
+## Unlocking Field Types
+
+There are two ways you can get the advanced typing information for your fields, the first is to provide a generic type to `useForm`.
+
+```ts
+import { useForm } from 'vee-validate';
+
+interface LoginForm {
+ email: string;
+ password: string;
+}
+
+// in your setup
+const { errors } = useForm();
+```
+
+```ts
+import { useForm } from 'vee-validate';
+
+interface LoginForm {
+ email: string;
+ password: string;
+}
+
+// in your setup
+const { errors, setErrors, setFieldValue } = useForm();
+
+errors.value; // typed as { email?: string; password?: string }
+
+setErrors({
+ email: 'This field is invalid', // auto-complete for `email` and `password`
+});
+
+setFieldValue('email', 'example@gmail.com'); // auto-complete for the field name and its value type
+```
+
+For example if you were to do this in the previous example:
+
+```ts
+setFieldValue('age', 5); // ⛔️ TypeScript error
+setFieldValue('email', 5); // ⛔️ TypeScript error
+```
+
+It will error out because `age` is not defined in the `LoginForm` type you defined. The second line errors out because the `email` field is typed as a `string`.
+
+## With Initial Values
+
+You can also unlock the same capabilities for simpler fields by providing an `initialValues` property to `useForm`:
+
+```typescript
+import { useForm } from 'vee-validate';
+
+const { errors, setErrors, setFieldValue } = useForm({
+ initialValues: {
+ email: '',
+ password: '',
+ },
+});
+```
+
+`useForm` will automatically pick up the type of `initialValues` and use it for the field types.
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 8d79269e2..a6959ef90 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -30,24 +30,30 @@ export type SubmitEvent = Event & { target: HTMLFormElement };
export type GenericValidateFunction = (value: any) => boolean | string | Promise;
-export interface FormContext {
+export interface FormContext = Record> {
register(field: any): void;
unregister(field: any): void;
- values: Record;
- fields: ComputedRef>;
- schema?: Record> | ObjectSchema;
- validateSchema?: (shouldMutate?: boolean) => Promise>;
- setFieldValue: (path: string, value: any) => void;
- setFieldError: (field: string, message: string) => void;
- setErrors: (fields: Record) => void;
- setValues: (fields: Record) => void;
- setFieldTouched: (field: string, isTouched: boolean) => void;
- setTouched: (fields: Record) => void;
- setFieldDirty: (field: string, isDirty: boolean) => void;
- setDirty: (fields: Record) => void;
+ values: TValues;
+ fields: ComputedRef>;
+ schema?: Record> | ObjectSchema;
+ validateSchema?: (shouldMutate?: boolean) => Promise>;
+ setFieldValue(field: T, value: TValues[T]): void;
+ setFieldError: (field: keyof TValues, message: string | undefined) => void;
+ setErrors: (fields: Partial>) => void;
+ setValues(fields: Partial>): void;
+ setFieldTouched: (field: keyof TValues, isTouched: boolean) => void;
+ setTouched: (fields: Partial>) => void;
+ setFieldDirty: (field: keyof TValues, isDirty: boolean) => void;
+ setDirty: (fields: Partial>) => void;
reset: () => void;
}
-type SubmissionContext = { evt: SubmitEvent; form: FormContext };
+type SubmissionContext = Record> = {
+ evt: SubmitEvent;
+ form: FormContext;
+};
-export type SubmissionHandler = (values: Record, ctx: SubmissionContext) => any;
+export type SubmissionHandler = Record> = (
+ values: TValues,
+ ctx: SubmissionContext
+) => any;
diff --git a/packages/core/src/useForm.ts b/packages/core/src/useForm.ts
index 3dc006f0d..2208b4d80 100644
--- a/packages/core/src/useForm.ts
+++ b/packages/core/src/useForm.ts
@@ -1,4 +1,4 @@
-import { computed, ref, Ref, provide, reactive, onMounted, isRef, watch, ComputedRef, unref } from 'vue';
+import { computed, ref, Ref, provide, reactive, onMounted, isRef, watch, unref } from 'vue';
import type { ObjectSchema, ValidationError } from 'yup';
import type { useField } from './useField';
import {
@@ -13,15 +13,17 @@ import {
import { getFromPath, isYupValidator, keysOf, setInPath, unsetPath } from './utils';
import { FormErrorsSymbol, FormInitialValues, FormSymbol } from './symbols';
-interface FormOptions {
- validationSchema?: Record> | ObjectSchema;
- initialValues?: MaybeReactive>;
+interface FormOptions> {
+ validationSchema?:
+ | Record>
+ | ObjectSchema;
+ initialValues?: MaybeReactive;
validateOnMount?: boolean;
}
type FieldComposite = ReturnType;
-export function useForm(opts?: FormOptions) {
+export function useForm = Record>(opts?: FormOptions) {
// A flat array containing field references
const fields: Ref = ref([]);
@@ -29,7 +31,7 @@ export function useForm(opts?: FormOptions) {
const isSubmitting = ref(false);
// a field map object useful for faster access of fields
- const fieldsById = computed(() => {
+ const fieldsById = computed>(() => {
return fields.value.reduce((acc, field) => {
// if the field was not added before
if (!acc[field.name]) {
@@ -59,11 +61,11 @@ export function useForm(opts?: FormOptions) {
});
// a private ref for all form values
- const formValues = reactive>({});
+ const formValues = reactive({}) as TValues;
// an aggregation of field errors in a map object
- const errors = computed(() => {
- return activeFields.value.reduce((acc: Record, field) => {
+ const errors = computed<{ [P in keyof TValues]?: string }>(() => {
+ return activeFields.value.reduce((acc: Record, field) => {
// Check if its a grouped field (checkbox/radio)
let message: string | undefined;
if (Array.isArray(fieldsById.value[field.name])) {
@@ -74,7 +76,7 @@ export function useForm(opts?: FormOptions) {
}
if (message) {
- acc[field.name] = message;
+ acc[field.name as keyof TValues] = message;
}
return acc;
@@ -82,8 +84,8 @@ export function useForm(opts?: FormOptions) {
});
// same as form values but filtered disabled fields out
- const activeFormValues = computed(() => {
- return activeFields.value.reduce((formData: Record, field) => {
+ const activeFormValues = computed(() => {
+ return activeFields.value.reduce((formData: Record, field) => {
setInPath(formData, field.name, unref(field.value));
return formData;
@@ -91,7 +93,7 @@ export function useForm(opts?: FormOptions) {
});
// initial form values
- const { initialValues } = useFormInitialValues(fieldsById, formValues, opts?.initialValues);
+ const { initialValues } = useFormInitialValues(fieldsById, formValues, opts?.initialValues);
// form meta aggregations
const meta = useFormMeta(fields, initialValues);
@@ -99,7 +101,7 @@ export function useForm(opts?: FormOptions) {
/**
* Manually sets an error message on a specific field
*/
- function setFieldError(field: string, message: string) {
+ function setFieldError(field: keyof TValues, message: string | undefined) {
const fieldInstance = fieldsById.value[field];
if (!fieldInstance) {
return;
@@ -107,19 +109,19 @@ export function useForm(opts?: FormOptions) {
if (Array.isArray(fieldInstance)) {
fieldInstance.forEach(instance => {
- instance.setValidationState({ errors: [message] });
+ instance.setValidationState({ errors: message ? [message] : [] });
});
return;
}
- fieldInstance.setValidationState({ errors: [message] });
+ fieldInstance.setValidationState({ errors: message ? [message] : [] });
}
/**
* Sets errors for the fields specified in the object
*/
- function setErrors(fields: Record) {
- Object.keys(fields).forEach(field => {
+ function setErrors(fields: Partial>) {
+ keysOf(fields).forEach(field => {
setFieldError(field, fields[field]);
});
}
@@ -127,33 +129,33 @@ export function useForm(opts?: FormOptions) {
/**
* Sets a single field value
*/
- function setFieldValue(path: string, value: any) {
- const field = fieldsById.value[path];
+ function setFieldValue(field: T, value: TValues[T] | undefined) {
+ const fieldInstance = fieldsById.value[field] as any;
// Multiple checkboxes, and only one of them got updated
- if (Array.isArray(field) && field[0]?.type === 'checkbox' && !Array.isArray(value)) {
- const oldVal = getFromPath(formValues, path);
+ if (Array.isArray(fieldInstance) && fieldInstance[0]?.type === 'checkbox' && !Array.isArray(value)) {
+ const oldVal = getFromPath(formValues, field as string);
const newVal = Array.isArray(oldVal) ? [...oldVal] : [];
const idx = newVal.indexOf(value);
idx >= 0 ? newVal.splice(idx, 1) : newVal.push(value);
- setInPath(formValues, path, newVal);
+ setInPath(formValues, field as string, newVal);
return;
}
let newValue = value;
// Single Checkbox
- if (field?.type === 'checkbox') {
- newValue = getFromPath(formValues, path) === value ? undefined : value;
+ if (fieldInstance?.type === 'checkbox') {
+ newValue = getFromPath(formValues, field as string) === value ? undefined : value;
}
- setInPath(formValues, path, newValue);
+ setInPath(formValues, field as string, newValue);
}
/**
* Sets multiple fields values
*/
- function setValues(fields: Record) {
- Object.keys(fields).forEach(field => {
+ function setValues(fields: Partial) {
+ keysOf(fields).forEach(field => {
setFieldValue(field, fields[field]);
});
}
@@ -161,7 +163,7 @@ export function useForm(opts?: FormOptions) {
/**
* Sets the touched meta state on a field
*/
- function setFieldTouched(field: string, isTouched: boolean) {
+ function setFieldTouched(field: keyof TValues, isTouched: boolean) {
const fieldInstance = fieldsById.value[field];
if (!fieldInstance) {
return;
@@ -178,16 +180,16 @@ export function useForm(opts?: FormOptions) {
/**
* Sets the touched meta state on multiple fields
*/
- function setTouched(fields: Record) {
- Object.keys(fields).forEach(field => {
- setFieldTouched(field, fields[field]);
+ function setTouched(fields: Partial>) {
+ keysOf(fields).forEach(field => {
+ setFieldTouched(field, !!fields[field]);
});
}
/**
* Sets the dirty meta state on a field
*/
- function setFieldDirty(field: string, isDirty: boolean) {
+ function setFieldDirty(field: keyof TValues, isDirty: boolean) {
const fieldInstance = fieldsById.value[field];
if (!fieldInstance) {
return;
@@ -204,9 +206,9 @@ export function useForm(opts?: FormOptions) {
/**
* Sets the dirty meta state on multiple fields
*/
- function setDirty(fields: Record) {
- Object.keys(fields).forEach(field => {
- setFieldDirty(field, fields[field]);
+ function setDirty(fields: Partial>) {
+ keysOf(fields).forEach(field => {
+ setFieldDirty(field, !!fields[field]);
});
}
@@ -257,7 +259,7 @@ export function useForm(opts?: FormOptions) {
unsetPath(formValues, fieldName);
}
- const formCtx: FormContext = {
+ const formCtx: FormContext = {
register: registerField,
unregister: unregisterField,
fields: fieldsById,
@@ -295,7 +297,7 @@ export function useForm(opts?: FormOptions) {
return results.every(r => !r.errors.length);
};
- const handleSubmit = (fn?: SubmissionHandler) => {
+ const handleSubmit = (fn?: SubmissionHandler) => {
return function submissionHandler(e: unknown) {
if (e instanceof Event) {
e.preventDefault();
@@ -364,10 +366,7 @@ export function useForm(opts?: FormOptions) {
/**
* Manages form meta aggregation
*/
-function useFormMeta(
- fields: Ref,
- initialValues: MaybeReactive>
-): ComputedRef }> {
+function useFormMeta(fields: Ref, initialValues: MaybeReactive) {
const MERGE_STRATEGIES: Record, 'every' | 'some'> = {
valid: 'every',
dirty: 'some',
@@ -384,13 +383,16 @@ function useFormMeta(
}, {} as Record, boolean>);
return {
- initialValues: unref(initialValues),
+ initialValues: unref(initialValues) as TValues,
...flags,
};
});
}
-async function validateYupSchema(form: FormContext, shouldMutate = false): Promise> {
+async function validateYupSchema(
+ form: FormContext,
+ shouldMutate = false
+): Promise> {
const errors: any[] = await (form.schema as any)
.validate(form.values, { abortEarly: false })
.then(() => [])
@@ -413,7 +415,7 @@ async function validateYupSchema(form: FormContext, shouldMutate = false): Promi
}, {});
// Aggregates the validation result
- const aggregatedResult = Object.keys(fields).reduce((result: Record, fieldId) => {
+ const aggregatedResult = keysOf(fields).reduce((result: Record, fieldId) => {
const field = fields[fieldId];
const messages = (errorsByPath[fieldId] || { errors: [] }).errors;
const fieldResult = {
@@ -436,7 +438,7 @@ async function validateYupSchema(form: FormContext, shouldMutate = false): Promi
field.setValidationState(fieldResult);
return result;
- }, {});
+ }, {} as Record);
return aggregatedResult;
}
@@ -444,17 +446,17 @@ async function validateYupSchema(form: FormContext, shouldMutate = false): Promi
/**
* Manages the initial values prop
*/
-function useFormInitialValues(
- fields: Ref,
- formValues: Record,
- providedValues?: MaybeReactive>
+function useFormInitialValues(
+ fields: Ref>,
+ formValues: TValues,
+ providedValues?: MaybeReactive
) {
- const initialValues = computed>(() => {
+ const initialValues = computed(() => {
if (isRef(providedValues)) {
- return providedValues.value as Record;
+ return providedValues.value;
}
- return providedValues || {};
+ return providedValues || ({} as TValues);
});
// Watch initial values for changes, and update the pristine (non-dirty and non-touched fields)
@@ -464,15 +466,15 @@ function useFormInitialValues(
initialValues,
value => {
const isSafeToUpdate = (f: any) => f.meta.dirty || f.meta.touched;
- Object.keys(fields.value).forEach(fieldPath => {
+ keysOf(fields.value).forEach(fieldPath => {
const field = fields.value[fieldPath];
const isFieldDirty = Array.isArray(field) ? field.some(isSafeToUpdate) : isSafeToUpdate(field);
if (isFieldDirty) {
return;
}
- const newValue = getFromPath(value, fieldPath);
- setInPath(formValues, fieldPath, newValue);
+ const newValue = getFromPath(value, fieldPath as string);
+ setInPath(formValues, fieldPath as string, newValue);
});
},
{