Repository: logaretm/vee-validate
Pull request #4397 (2023-07-30 15:42:57)

feat: added composition setter functions
🔎 __Overview__


Adds the following functions:

- `useSetFieldValue`: sets a field's value.
- `useSetFieldTouched`: sets a field's touched state.
- `useSetFieldError`: sets a field's error message.
- `useSetFormValues`: sets form values.
- `useSetFormTouched`: sets multiple or all fields touched state.
- `useSetFormErrors`: sets form error messages.

✔ __Issues affected__

closes  #4365


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) Report
> Merging [#4397](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) (1c772b5) into [main](https://app.codecov.io/gh/logaretm/vee-validate/commit/91bff18fc3cfa6afdd717a2e61883273b4c996c3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) (91bff18) will **increase** coverage by `0.04%`.
> Report is 1 commits behind head on main.
> The diff coverage is `100.00%`.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad).

```diff
@@            Coverage Diff             @@
##             main    #4397      +/-   ##
==========================================
+ Coverage   98.09%   98.14%   +0.04%     
==========================================
  Files          73       79       +6     
  Lines        5926     6081     +155     
  Branches     1134     1161      +27     
==========================================
+ Hits         5813     5968     +155     
  Misses        113      113              
```


| [Files Changed](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) | Coverage Δ | |
|---|---|---|
| [packages/vee-validate/src/types/forms.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy90eXBlcy9mb3Jtcy50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/vee-validate/src/useForm.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy91c2VGb3JtLnRz) | `99.47% <100.00%> (+<0.01%)` | :arrow_up: |
| [packages/vee-validate/src/useSetFieldError.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy91c2VTZXRGaWVsZEVycm9yLnRz) | `100.00% <100.00%> (ø)` | |
| [packages/vee-validate/src/useSetFieldTouched.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy91c2VTZXRGaWVsZFRvdWNoZWQudHM=) | `100.00% <100.00%> (ø)` | |
| [packages/vee-validate/src/useSetFieldValue.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy91c2VTZXRGaWVsZFZhbHVlLnRz) | `100.00% <100.00%> (ø)` | |
| [packages/vee-validate/src/useSetFormErrors.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy91c2VTZXRGb3JtRXJyb3JzLnRz) | `100.00% <100.00%> (ø)` | |
| [packages/vee-validate/src/useSetFormTouched.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy91c2VTZXRGb3JtVG91Y2hlZC50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/vee-validate/src/useSetFormValues.ts](https://app.codecov.io/gh/logaretm/vee-validate/pull/4397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad#diff-cGFja2FnZXMvdmVlLXZhbGlkYXRlL3NyYy91c2VTZXRGb3JtVmFsdWVzLnRz) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad)


Patch:
diff --git a/.changeset/spicy-balloons-dream.md b/.changeset/spicy-balloons-dream.md
new file mode 100644
index 00000000..5847fbe6
--- /dev/null
+++ b/.changeset/spicy-balloons-dream.md
@@ -0,0 +1,5 @@
+---
+'vee-validate': minor
+---
+
+feat: added composition setter functions
diff --git a/packages/vee-validate/src/index.ts b/packages/vee-validate/src/index.ts
index d97413ec..61caffa4 100644
--- a/packages/vee-validate/src/index.ts
+++ b/packages/vee-validate/src/index.ts
@@ -54,4 +54,10 @@ export { useFormValues } from './useFormValues';
 export { useFormErrors } from './useFormErrors';
 export { useFieldError } from './useFieldError';
 export { useSubmitForm } from './useSubmitForm';
+export { useSetFieldError } from './useSetFieldError';
+export { useSetFieldTouched } from './useSetFieldTouched';
+export { useSetFieldValue } from './useSetFieldValue';
+export { useSetFormErrors } from './useSetFormErrors';
+export { useSetFormTouched } from './useSetFormTouched';
+export { useSetFormValues } from './useSetFormValues';
 export { FormContextKey, FieldContextKey, IS_ABSENT } from './symbols';
diff --git a/packages/vee-validate/src/types/forms.ts b/packages/vee-validate/src/types/forms.ts
index 2c7de32e..3bebe58b 100644
--- a/packages/vee-validate/src/types/forms.ts
+++ b/packages/vee-validate/src/types/forms.ts
@@ -143,7 +143,7 @@ export interface PrivateFieldContext<TValue = unknown> {
   setState(state: Partial<FieldState<TValue>>): void;
   setTouched(isTouched: boolean): void;
   setErrors(message: string | string[]): void;
-  setValue(value: TValue): void;
+  setValue(value: TValue, shouldValidate?: boolean): void;
 }
 
 export type FieldContext<TValue = unknown> = Omit<PrivateFieldContext<TValue>, 'id' | 'instances'>;
@@ -170,7 +170,7 @@ export interface FormActions<TValues extends GenericObject, TOutput = TValues> {
   setErrors(fields: Partial<FlattenAndSetPathsType<TValues, string | string[] | undefined>>): void;
   setValues(fields: PartialDeep<TValues>, shouldValidate?: boolean): void;
   setFieldTouched(field: Path<TValues>, isTouched: boolean): void;
-  setTouched(fields: Partial<Record<Path<TValues>, boolean>>): void;
+  setTouched(fields: Partial<Record<Path<TValues>, boolean>> | boolean): void;
   resetForm(state?: Partial<FormState<TValues>>): void;
   resetField(field: Path<TValues>, state?: Partial<FieldState>): void;
 }
diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts
index 9374be77..8125c7c6 100644
--- a/packages/vee-validate/src/useForm.ts
+++ b/packages/vee-validate/src/useForm.ts
@@ -685,7 +685,15 @@ export function useForm<
   /**
    * Sets the touched meta state on multiple fields
    */
-  function setTouched(fields: Partial<FlattenAndSetPathsType<TValues, boolean>>) {
+  function setTouched(fields: Partial<FlattenAndSetPathsType<TValues, boolean>> | boolean) {
+    if (typeof fields === 'boolean') {
+      mutateAllPathState(state => {
+        state.touched = fields;
+      });
+
+      return;
+    }
+
     keysOf(fields).forEach(field => {
       setFieldTouched(field, !!fields[field]);
     });
diff --git a/packages/vee-validate/src/useSetFieldError.ts b/packages/vee-validate/src/useSetFieldError.ts
new file mode 100644
index 00000000..e94f9120
--- /dev/null
+++ b/packages/vee-validate/src/useSetFieldError.ts
@@ -0,0 +1,26 @@
+import { inject, MaybeRefOrGetter, toValue } from 'vue';
+import { FieldContextKey, FormContextKey } from './symbols';
+import { injectWithSelf, warn } from './utils';
+
+/**
+ * Sets a field's error message
+ */
+export function useSetFieldError(path?: MaybeRefOrGetter<string>) {
+  const form = injectWithSelf(FormContextKey);
+  // We don't want to use self injected context as it doesn't make sense
+  const field = path ? undefined : inject(FieldContextKey);
+
+  return function setFieldError(message: string | string[] | undefined) {
+    if (path && form) {
+      form.setFieldError(toValue(path), message);
+    }
+
+    if (field) {
+      field.setErrors(message || []);
+    }
+
+    if (__DEV__) {
+      warn(`Could not set error for unknown field with path "${toValue(path)}"`);
+    }
+  };
+}
diff --git a/packages/vee-validate/src/useSetFieldTouched.ts b/packages/vee-validate/src/useSetFieldTouched.ts
new file mode 100644
index 00000000..1b6c7096
--- /dev/null
+++ b/packages/vee-validate/src/useSetFieldTouched.ts
@@ -0,0 +1,26 @@
+import { inject, MaybeRefOrGetter, toValue } from 'vue';
+import { FieldContextKey, FormContextKey } from './symbols';
+import { injectWithSelf, warn } from './utils';
+
+/**
+ * Sets a field's touched meta state
+ */
+export function useSetFieldTouched(path?: MaybeRefOrGetter<string>) {
+  const form = injectWithSelf(FormContextKey);
+  // We don't want to use self injected context as it doesn't make sense
+  const field = path ? undefined : inject(FieldContextKey);
+
+  return function setFieldTouched(touched: boolean) {
+    if (path && form) {
+      form.setFieldTouched(toValue(path), touched);
+    }
+
+    if (field) {
+      field.setTouched(touched);
+    }
+
+    if (__DEV__) {
+      warn(`Could not set touched meta for unknown field with path "${toValue(path)}"`);
+    }
+  };
+}
diff --git a/packages/vee-validate/src/useSetFieldValue.ts b/packages/vee-validate/src/useSetFieldValue.ts
new file mode 100644
index 00000000..c998a11c
--- /dev/null
+++ b/packages/vee-validate/src/useSetFieldValue.ts
@@ -0,0 +1,26 @@
+import { inject, MaybeRefOrGetter, toValue } from 'vue';
+import { FieldContextKey, FormContextKey } from './symbols';
+import { injectWithSelf, warn } from './utils';
+
+/**
+ * Sets a field's value
+ */
+export function useSetFieldValue<TValue = unknown>(path?: MaybeRefOrGetter<string>) {
+  const form = injectWithSelf(FormContextKey);
+  // We don't want to use self injected context as it doesn't make sense
+  const field = path ? undefined : inject(FieldContextKey);
+
+  return function setFieldValue(value: TValue, shouldValidate = true) {
+    if (path && form) {
+      form.setFieldValue(toValue(path), value, shouldValidate);
+    }
+
+    if (field) {
+      field.setValue(value, shouldValidate);
+    }
+
+    if (__DEV__) {
+      warn(`Could not set value for unknown field with path "${toValue(path)}"`);
+    }
+  };
+}
diff --git a/packages/vee-validate/src/useSetFormErrors.ts b/packages/vee-validate/src/useSetFormErrors.ts
new file mode 100644
index 00000000..1b0c6fc0
--- /dev/null
+++ b/packages/vee-validate/src/useSetFormErrors.ts
@@ -0,0 +1,23 @@
+import { FormContextKey } from './symbols';
+import { injectWithSelf, warn } from './utils';
+
+/**
+ * Sets multiple fields errors
+ */
+export function useSetFormErrors() {
+  const form = injectWithSelf(FormContextKey);
+
+  function setFormErrors(fields: Record<string, string | string[] | undefined>) {
+    if (form) {
+      form.setErrors(fields);
+    }
+
+    if (__DEV__) {
+      warn(
+        `Could not set errors because a form was not detected, did you forget to use "useForm" in a parent component?`,
+      );
+    }
+  }
+
+  return setFormErrors;
+}
diff --git a/packages/vee-validate/src/useSetFormTouched.ts b/packages/vee-validate/src/useSetFormTouched.ts
new file mode 100644
index 00000000..e8a305ca
--- /dev/null
+++ b/packages/vee-validate/src/useSetFormTouched.ts
@@ -0,0 +1,23 @@
+import { FormContextKey } from './symbols';
+import { injectWithSelf, warn } from './utils';
+
+/**
+ * Sets multiple fields touched or all fields in the form
+ */
+export function useSetFormTouched() {
+  const form = injectWithSelf(FormContextKey);
+
+  function setFormTouched(fields: Record<string, boolean> | boolean) {
+    if (form) {
+      form.setTouched(fields);
+    }
+
+    if (__DEV__) {
+      warn(
+        `Could not set touched state because a form was not detected, did you forget to use "useForm" in a parent component?`,
+      );
+    }
+  }
+
+  return setFormTouched;
+}
diff --git a/packages/vee-validate/src/useSetFormValues.ts b/packages/vee-validate/src/useSetFormValues.ts
new file mode 100644
index 00000000..6a668784
--- /dev/null
+++ b/packages/vee-validate/src/useSetFormValues.ts
@@ -0,0 +1,23 @@
+import { FormContextKey } from './symbols';
+import { injectWithSelf, warn } from './utils';
+
+/**
+ * Sets multiple fields values
+ */
+export function useSetFormValues() {
+  const form = injectWithSelf(FormContextKey);
+
+  function setFormValues(fields: Record<string, unknown>) {
+    if (form) {
+      form.setValues(fields);
+    }
+
+    if (__DEV__) {
+      warn(
+        `Could not set form values because a form was not detected, did you forget to use "useForm" in a parent component?`,
+      );
+    }
+  }
+
+  return setFormValues;
+}


Test patch:
diff --git a/packages/vee-validate/tests/useSetFieldError.spec.ts b/packages/vee-validate/tests/useSetFieldError.spec.ts
new file mode 100644
index 00000000..7b1284e4
--- /dev/null
+++ b/packages/vee-validate/tests/useSetFieldError.spec.ts
@@ -0,0 +1,92 @@
+import { useField, useSetFieldError, useForm } from '@/vee-validate';
+import { mountWithHoc, flushPromises } from './helpers';
+import { defineComponent } from 'vue';
+
+describe('useSetFieldError()', () => {
+  test('sets a single field error message', async () => {
+    let setError!: ReturnType<typeof useSetFieldError>;
+
+    mountWithHoc({
+      setup() {
+        useForm();
+        const { value, errorMessage } = useField('test');
+        setError = useSetFieldError('test');
+
+        return {
+          value,
+          errorMessage,
+        };
+      },
+      template: `
+      <input name="field" v-model="value" />
+      <span>{{ errorMessage }}</span>
+    `,
+    });
+    await flushPromises();
+    const msg = 'Field is required';
+
+    const error = document.querySelector('span');
+    setError(msg);
+    await flushPromises();
+    expect(error?.textContent).toBe(msg);
+  });
+
+  test('sets a single field error message in a child component without specifying a path', async () => {
+    let setError!: ReturnType<typeof useSetFieldError>;
+    const CustomSetErrorComponent = defineComponent({
+      template: '<button></button>',
+      setup() {
+        setError = useSetFieldError();
+
+        return {};
+      },
+    });
+    mountWithHoc({
+      components: {
+        CustomSetErrorComponent,
+      },
+      setup() {
+        useForm();
+        const { errorMessage } = useField('test');
+
+        return {
+          errorMessage,
+        };
+      },
+      template: `
+      <span>{{ errorMessage }}</span>
+      <CustomSetErrorComponent />
+    `,
+    });
+
+    await flushPromises();
+    const msg = 'Field is required';
+    const error = document.querySelector('span');
+    setError(msg);
+    await flushPromises();
+    expect(error?.textContent).toBe(msg);
+  });
+
+  test('warns if field not found', async () => {
+    let setError!: ReturnType<typeof useSetFieldError>;
+    const spy = vi.spyOn(console, 'warn').mockImplementation(() => {
+      // NOOP
+    });
+
+    mountWithHoc({
+      setup() {
+        useForm();
+        setError = useSetFieldError('something');
+
+        return {};
+      },
+      template: `<div></div>`,
+    });
+
+    await flushPromises();
+    setError('ERROR');
+    await flushPromises();
+    expect(spy).toHaveBeenCalled();
+    spy.mockRestore();
+  });
+});
diff --git a/packages/vee-validate/tests/useSetFieldTouched.spec.ts b/packages/vee-validate/tests/useSetFieldTouched.spec.ts
new file mode 100644
index 00000000..df4af77a
--- /dev/null
+++ b/packages/vee-validate/tests/useSetFieldTouched.spec.ts
@@ -0,0 +1,90 @@
+import { useField, useForm, useSetFieldTouched } from '@/vee-validate';
+import { mountWithHoc, flushPromises } from './helpers';
+import { defineComponent } from 'vue';
+
+describe('useSetFieldTouched()', () => {
+  test('sets a single field touched status', async () => {
+    let setTouched!: ReturnType<typeof useSetFieldTouched>;
+    mountWithHoc({
+      setup() {
+        useForm();
+        const { meta } = useField('test');
+        setTouched = useSetFieldTouched('test');
+
+        return {
+          meta,
+        };
+      },
+      template: `
+      <span>{{ meta.touched.toString() }}</span>
+    `,
+    });
+    await flushPromises();
+
+    const touched = document.querySelector('span');
+    expect(touched?.textContent).toBe('false');
+    setTouched(true);
+    await flushPromises();
+    expect(touched?.textContent).toBe('true');
+  });
+
+  test('sets a single field isTouched status in child components without path prop', async () => {
+    let setTouched!: ReturnType<typeof useSetFieldTouched>;
+    const TouchedButton = defineComponent({
+      template: '<button></button>',
+      setup() {
+        setTouched = useSetFieldTouched();
+
+        return {};
+      },
+    });
+    mountWithHoc({
+      components: {
+        TouchedButton,
+      },
+      setup() {
+        useForm();
+        const { meta } = useField('test');
+
+        return {
+          meta,
+        };
+      },
+      template: `
+      <span>{{ meta.touched.toString() }}</span>
+      <TouchedButton />
+    `,
+    });
+
+    await flushPromises();
+
+    const touched = document.querySelector('span');
+    expect(touched?.textContent).toBe('false');
+    setTouched(true);
+    await flushPromises();
+    expect(touched?.textContent).toBe('true');
+  });
+
+  test('warns if field not found', async () => {
+    let setTouched!: ReturnType<typeof useSetFieldTouched>;
+    const spy = vi.spyOn(console, 'warn').mockImplementation(() => {
+      // NOOP
+    });
+
+    mountWithHoc({
+      setup() {
+        useForm();
+        setTouched = useSetFieldTouched('something');
+
+        return {};
+      },
+      template: `<div></div>`,
+    });
+
+    await flushPromises();
+    setTouched(true);
+    await flushPromises();
+    expect(spy).toHaveBeenCalled();
+    spy.mockRestore();
+  });
+});
diff --git a/packages/vee-validate/tests/useSetFieldValue.spec.ts b/packages/vee-validate/tests/useSetFieldValue.spec.ts
new file mode 100644
index 00000000..c1390e9b
--- /dev/null
+++ b/packages/vee-validate/tests/useSetFieldValue.spec.ts
@@ -0,0 +1,91 @@
+import { useField, useForm, useSetFieldValue } from '@/vee-validate';
+import { mountWithHoc, flushPromises } from './helpers';
+import { defineComponent } from 'vue';
+
+describe('useSetFieldTouched()', () => {
+  test('sets a single field value', async () => {
+    let setValue!: ReturnType<typeof useSetFieldValue>;
+    mountWithHoc({
+      setup() {
+        useForm();
+        const { value } = useField('test');
+        setValue = useSetFieldValue('test');
+
+        return {
+          value,
+        };
+      },
+      template: `
+      <span>{{ value }}</span>
+    `,
+    });
+    await flushPromises();
+
+    const valueSpan = document.querySelector('span');
+    expect(valueSpan?.textContent).toBe('');
+    setValue('test 123');
+    await flushPromises();
+    expect(valueSpan?.textContent).toBe('test 123');
+  });
+
+  test('sets a single field value in child components without path prop', async () => {
+    let setValue!: ReturnType<typeof useSetFieldValue>;
+    const ValueBtn = defineComponent({
+      template: '<button></button>',
+      setup() {
+        setValue = useSetFieldValue();
+
+        return {};
+      },
+    });
+    mountWithHoc({
+      components: {
+        ValueBtn,
+      },
+      setup() {
+        useForm();
+        const { value } = useField('test');
+
+        return {
+          value,
+        };
+      },
+      template: `
+      <span>{{ value }}</span>
+      <ValueBtn />
+    `,
+    });
+
+    await flushPromises();
+
+    const valueSpan = document.querySelector('span');
+    expect(valueSpan?.textContent).toBe('');
+    setValue('test 123');
+    await flushPromises();
+    expect(valueSpan?.textContent).toBe('test 123');
+  });
+
+  test('warns if field not found', async () => {
+    let setValue!: ReturnType<typeof useSetFieldValue>;
+
+    const spy = vi.spyOn(console, 'warn').mockImplementation(() => {
+      // NOOP
+    });
+
+    mountWithHoc({
+      setup() {
+        useForm();
+        setValue = useSetFieldValue('something');
+
+        return {};
+      },
+      template: `<div></div>`,
+    });
+
+    await flushPromises();
+    setValue('test 123');
+    await flushPromises();
+    expect(spy).toHaveBeenCalled();
+    spy.mockRestore();
+  });
+});
diff --git a/packages/vee-validate/tests/useSetFormErrors.spec.ts b/packages/vee-validate/tests/useSetFormErrors.spec.ts
new file mode 100644
index 00000000..4b485d0f
--- /dev/null
+++ b/packages/vee-validate/tests/useSetFormErrors.spec.ts
@@ -0,0 +1,52 @@
+import { useForm, useSetFormErrors } from '@/vee-validate';
+import { mountWithHoc, flushPromises } from './helpers';
+
+describe('useSetFormErrors()', () => {
+  const REQUIRED_MESSAGE = 'Field is required';
+
+  test('sets multiple fields errors', async () => {
+    let setErrors!: ReturnType<typeof useSetFormErrors>;
+    mountWithHoc({
+      setup() {
+        const { errors } = useForm();
+        setErrors = useSetFormErrors();
+
+        return {
+          errors,
+        };
+      },
+      template: `
+      <span>{{ errors.test }}</span>
+    `,
+    });
+
+    await flushPromises();
+    const error = document.querySelector('span');
+    setErrors({ test: REQUIRED_MESSAGE });
+    await flushPromises();
+    expect(error?.textContent).toBe(REQUIRED_MESSAGE);
+  });
+
+  test('warns if form is not found', async () => {
+    const spy = vi.spyOn(console, 'warn').mockImplementation(() => {
+      // NOOP
+    });
+    let setErrors!: ReturnType<typeof useSetFormErrors>;
+    mountWithHoc({
+      setup() {
+        setErrors = useSetFormErrors();
+
+        return {};
+      },
+      template: `
+      <span></span>
+    `,
+    });
+
+    await flushPromises();
+    setErrors({});
+    await flushPromises();
+    expect(spy).toHaveBeenCalled();
+    spy.mockRestore();
+  });
+});
diff --git a/packages/vee-validate/tests/useSetFormTouched.spec.ts b/packages/vee-validate/tests/useSetFormTouched.spec.ts
new file mode 100644
index 00000000..88f4afca
--- /dev/null
+++ b/packages/vee-validate/tests/useSetFormTouched.spec.ts
@@ -0,0 +1,85 @@
+import { useField, useForm, useSetFormTouched } from '@/vee-validate';
+import { mountWithHoc, flushPromises } from './helpers';
+
+describe('useSetFormTouched()', () => {
+  test('sets multiple fields touched state', async () => {
+    let setTouched!: ReturnType<typeof useSetFormTouched>;
+    mountWithHoc({
+      setup() {
+        useForm();
+        const { meta: test1 } = useField('test1');
+        const { meta: test2 } = useField('test2');
+        setTouched = useSetFormTouched();
+
+        return {
+          test1,
+          test2,
+        };
+      },
+      template: `
+      <span id="t1">{{ test1.touched }}</span>
+      <span id="t2">{{ test2.touched }}</span>
+    `,
+    });
+
+    await flushPromises();
+    const touched1 = document.querySelector('#t1');
+    const touched2 = document.querySelector('#t2');
+    setTouched({ test1: true, test2: false });
+    await flushPromises();
+    expect(touched1?.textContent).toBe('true');
+    expect(touched2?.textContent).toBe('false');
+  });
+
+  test('sets all fields touched state', async () => {
+    let setTouched!: ReturnType<typeof useSetFormTouched>;
+    mountWithHoc({
+      setup() {
+        useForm();
+        const { meta: test1 } = useField('test1');
+        const { meta: test2 } = useField('test2');
+        setTouched = useSetFormTouched();
+
+        return {
+          test1,
+          test2,
+        };
+      },
+      template: `
+      <span id="t1">{{ test1.touched }}</span>
+      <span id="t2">{{ test2.touched }}</span>
+    `,
+    });
+
+    await flushPromises();
+    const touched1 = document.querySelector('#t1');
+    const touched2 = document.querySelector('#t2');
+    setTouched(true);
+    await flushPromises();
+    expect(touched1?.textContent).toBe('true');
+    expect(touched2?.textContent).toBe('true');
+  });
+
+  test('warns if form is not found', async () => {
+    const spy = vi.spyOn(console, 'warn').mockImplementation(() => {
+      // NOOP
+    });
+    let setTouched!: ReturnType<typeof useSetFormTouched>;
+    mountWithHoc({
+      setup() {
+        setTouched = useSetFormTouched();
+
+        return {};
+      },
+      template: `
+      <span></span>
+    `,
+    });
+
+    await flushPromises();
+    setTouched({});
+    await flushPromises();
+    expect(spy).toHaveBeenCalled();
+    spy.mockRestore();
+  });
+});
diff --git a/packages/vee-validate/tests/useSetFormValues.spec.ts b/packages/vee-validate/tests/useSetFormValues.spec.ts
new file mode 100644
index 00000000..dc04985d
--- /dev/null
+++ b/packages/vee-validate/tests/useSetFormValues.spec.ts
@@ -0,0 +1,58 @@
+import { useField, useForm, useSetFormValues } from '@/vee-validate';
+import { mountWithHoc, flushPromises } from './helpers';
+
+describe('useSetFormValues()', () => {
+  test('sets form values', async () => {
+    let setValues!: ReturnType<typeof useSetFormValues>;
+    mountWithHoc({
+      setup() {
+        useForm();
+        const { value: test1 } = useField('test1');
+        const { value: test2 } = useField('test2');
+        setValues = useSetFormValues();
+
+        return {
+          test1,
+          test2,
+        };
+      },
+      template: `
+      <span id="t1">{{ test1 }}</span>
+      <span id="t2">{{ test2 }}</span>
+    `,
+    });
+
+    await flushPromises();
+    const value1 = document.querySelector('#t1');
+    const value2 = document.querySelector('#t2');
+
+    setValues({ test1: 'test1', test2: 'test2' });
+    await flushPromises();
+    expect(value1?.textContent).toBe('test1');
+    expect(value2?.textContent).toBe('test2');
+  });
+
+  test('warns if form is not found', async () => {
+    let setValues!: ReturnType<typeof useSetFormValues>;
+    const spy = vi.spyOn(console, 'warn').mockImplementation(() => {
+      // NOOP
+    });
+
+    mountWithHoc({
+      setup() {
+        setValues = useSetFormValues();
+
+        return {};
+      },
+      template: `
+      <span></span>
+    `,
+    });
+
+    await flushPromises();
+    setValues({});
+    await flushPromises();
+    expect(spy).toHaveBeenCalled();
+    spy.mockRestore();
+  });
+});