Skip to content

Commit

Permalink
feat: form and fields values setters (#2949)
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Oct 10, 2020
1 parent 1389437 commit cc2cb41
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 34 deletions.
24 changes: 14 additions & 10 deletions docs/content/api/form.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,19 @@ While not recommended, you can make the `Form` component a renderless component

The default slot gives you access to the following props:

| Scoped Prop | Type | Description |
| :----------- | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| errors | `Record<string, string>` | The first error message of each field, the object keys are the fields names |
| meta | `Record<string, boolean>` | An aggregate of the [FieldMeta](/api/field#fieldmeta) for the fields within the form |
| values | `Record<string, any>` | The current field values |
| isSubmitting | `boolean` | True while the submission handler for the form is being executed |
| validate | `Function` | Validates the form |
| handleSubmit | `(cb: Function) => Function` | Creates a submission handler that disables the native form submissions and executes the callback if the validation passes |
| handleReset | `Function` | Resets and form and executes any `onReset` listeners on the component |
| submitForm | `Function` | Validates the form and triggers the `submit` event on the form, useful for non-SPA applications |
| Scoped Prop | Type | Description |
| :------------ | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| errors | `Record<string, string>` | The first error message of each field, the object keys are the fields names |
| meta | `Record<string, boolean>` | An aggregate of the [FieldMeta](/api/field#fieldmeta) for the fields within the form |
| values | `Record<string, any>` | The current field values |
| isSubmitting | `boolean` | True while the submission handler for the form is being executed |
| validate | `Function` | Validates the form |
| handleSubmit | `(cb: Function) => Function` | Creates a submission handler that disables the native form submissions and executes the callback if the validation passes |
| handleReset | `Function` | Resets and form and executes any `onReset` listeners on the component |
| submitForm | `Function` | Validates the form and triggers the `submit` event on the form, useful for non-SPA applications |
| setFieldError | `Function` | Sets an error message on a field |
| setErrors | `Function` | Sets error message for the specified fields |
| setFieldValue | `Function` | Sets a field's value, triggers validation |
| setValues | `Function` | Sets the specified fields values, triggers validation on those fields |

Check the sample above for rendering with scoped slots
4 changes: 3 additions & 1 deletion docs/content/api/use-form.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ type useForm = (
handleReset: (e: Event) => void; // Resets all fields' errors and meta
handleSubmit: (cb: Function) => () => void; // Creates a submission handler that calls the cb only after successful validation with the form values
submitForm: (e: Event) => void; // Forces submission of a form after successful validation (calls e.target.submit())
setErrors: (errors: Record<string, string>) => void; // Sets error messages for fields
setErrors: (fields: Record<string, string>) => void; // Sets error messages for fields
setFieldError: (field: string, errorMessage: string) => void; // Sets an error message for a field
setFieldValue: (field: string, value: any) => void; // Sets a field value
setValues: (fields: Record<string, any>) => void; // Sets multiple fields values
};
```
97 changes: 96 additions & 1 deletion docs/content/guide/handling-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,14 +372,109 @@ You can use `validateOnMount` prop present on the `<Form />` component to force

The `initialValues` prop on both the `<Form />` component and `useForm()` function can reactive value, meaning you can change the initial values after your component was created/mounted which is very useful if you are populating form fields from external API.

Note that **only the pristine fields will be updated**. In other words, **only the fields that were not manipulated by the user will be updated**.
Note that **only the pristine fields will be updated**. In other words, **only the fields that were not manipulated by the user will be updated**. For information on how to set the values for all fields regardless of their dirty status check the following [Setting Form Values section](#setting-form-values)

<doc-tip title="Composition API">

If you are using the composition API with `setup` function, you could create the `initialValues` prop using both [**reactive()**](https://v3.vuejs.org/api/basic-reactivity.html#reactive) and [**ref()**](https://v3.vuejs.org/api/refs-api.html#ref). vee-validate handles both cases.

</doc-tip>

## Setting Form Values

You can set any field's value using either `setFieldValue` or `setValues`, both methods are exposed on the `<Form />` component scoped slot props, and in `useForm` return value, and as instance methods if so you can call them with template `$refs` and for an added convenience you can call them in the submit handler callback.

**Using scoped slot props**

```vue
<Form v-slot="{ setFieldValue, setValues }">
<Field name="email" as="input">
<ErrorMessage name="email" />
<Field name="password" as="input">
<ErrorMessage name="password" />
<button type="button" @click="setFieldValue('email', 'test')">Set Field Value</button>
<button type="button" @click="setValues({ email: 'test', password: 'test12' })">
Set Multiple Values
</button>
</Form>
```

**Using submit callback**

```vue
<template>
<Form @submit="onSubmit">
<Field name="email" as="input">
<ErrorMessage name="email" />
<Field name="password" as="input">
<ErrorMessage name="password" />
<button>Submit</button>
</Form>
</template>
<script>
export default {
// ...
methods :{
onSubmit(values, { form }) {
// Submit the values...
// set single field value
form.setFieldValue('email', 'ummm@example.com');
// set multiple values
form.setValues({
email: 'ummm@example.com',
password: 'P@$$w0Rd',
});
}
}
};
</script>
```

**Using template `$refs`**

```vue
<template>
<Form @submit="onSubmit" ref="myForm">
<Field name="email" as="input">
<ErrorMessage name="email" />
<Field name="password" as="input">
<ErrorMessage name="password" />
<button>Submit</button>
</Form>
</template>
<script>
export default {
// ...
methods :{
onSubmit(values) {
// Submit the values...
// set single field value
this.$refs.myForm.setFieldValue('email', 'ummm@example.com');
// set multiple values
this.$refs.myForm.setValues({
email: 'ummm@example.com',
password: 'P@$$w0Rd',
});
}
}
};
</script>
```

Note that setting any field's value using this way will trigger validation

## Setting Errors Manually

Quite often you will find yourself unable to replicate some validation rules on the client-side due to natural limitations. For example a `unique` email validation is complex to implement on the client-side, which is why the `<Form />` component and `useForm()` function allow you to set errors manually.
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const Form = defineComponent({
submitForm,
setErrors,
setFieldError,
setFieldValue,
setValues,
} = useForm({
validationSchema: props.validationSchema,
initialValues,
Expand All @@ -58,6 +60,8 @@ export const Form = defineComponent({
if (!this.setErrors) {
this.setFieldError = setFieldError;
this.setErrors = setErrors;
this.setFieldValue = setFieldValue;
this.setValues = setValues;
}

const children = normalizeChildren(ctx, {
Expand All @@ -71,6 +75,8 @@ export const Form = defineComponent({
submitForm,
setErrors,
setFieldError,
setFieldValue,
setValues,
});

if (!props.as) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export interface FormController {
validateSchema?: (shouldMutate?: boolean) => Promise<Record<string, ValidationResult>>;
setFieldValue: (path: string, value: any) => void;
setFieldError: (field: string, message: string) => void;
setErrors: (errors: Record<string, string>) => void;
setErrors: (fields: Record<string, string>) => void;
setValues: (fields: Record<string, any>) => void;
}

type SubmissionContext = { evt: SubmitEvent; form: FormController };
Expand Down
59 changes: 38 additions & 21 deletions packages/core/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,40 @@ export function useForm(opts?: FormOptions) {
});
}

/**
* Sets a single field value
*/
function setFieldValue(path: string, value: any) {
const field = fieldsById.value[path];

// 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);
const newVal = Array.isArray(oldVal) ? [...oldVal] : [];
const idx = newVal.indexOf(value);
idx >= 0 ? newVal.splice(idx, 1) : newVal.push(value);
setInPath(formValues, path, newVal);
return;
}

let newValue = value;
// Single Checkbox
if (field?.type === 'checkbox') {
newValue = getFromPath(formValues, path) === value ? undefined : value;
}

setInPath(formValues, path, newValue);
}

/**
* Sets multiple fields values
*/
function setValues(fields: Record<string, any>) {
Object.keys(fields).forEach(field => {
setFieldValue(field, fields[field]);
});
}

// a private ref for all form values
const formValues = reactive<Record<string, any>>({});
const controller: FormController = {
Expand Down Expand Up @@ -130,27 +164,8 @@ export function useForm(opts?: FormOptions) {
return validateYupSchema(controller, shouldMutate);
}
: undefined,
setFieldValue(path: string, value: any) {
const field = fieldsById.value[path];

// 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);
const newVal = Array.isArray(oldVal) ? [...oldVal] : [];
const idx = newVal.indexOf(value);
idx >= 0 ? newVal.splice(idx, 1) : newVal.push(value);
setInPath(formValues, path, newVal);
return;
}

let newValue = value;
// Single Checkbox
if (field?.type === 'checkbox') {
newValue = getFromPath(formValues, path) === value ? undefined : value;
}

setInPath(formValues, path, newValue);
},
setFieldValue,
setValues,
setErrors,
setFieldError,
};
Expand Down Expand Up @@ -291,6 +306,8 @@ export function useForm(opts?: FormOptions) {
submitForm,
setFieldError,
setErrors,
setFieldValue,
setValues,
};
}

Expand Down
39 changes: 39 additions & 0 deletions packages/core/tests/Form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1216,4 +1216,43 @@ describe('<Form />', () => {
await flushPromises();
expect(error.textContent).toBe('WRONG');
});

test('sets individual field value with setFieldValue()', async () => {
const wrapper = mountWithHoc({
template: `
<VForm ref="form">
<Field id="email" name="email" as="input" />
</VForm>
`,
});

await flushPromises();
const value = 'example@gmail.com';
const email = wrapper.$el.querySelector('#email');
(wrapper.$refs as any)?.form.setFieldValue('email', value);
await flushPromises();
expect(email.value).toBe(value);
});

test('sets multiple fields values with setValues()', async () => {
const wrapper = mountWithHoc({
template: `
<VForm ref="form">
<Field id="email" name="email" as="input" />
<Field id="password" name="password" as="input" />
</VForm>
`,
});

await flushPromises();
const values = {
email: 'example@gmail.com',
password: '12345',
};
const inputs = wrapper.$el.querySelectorAll('input');
(wrapper.$refs as any)?.form.setValues(values);
await flushPromises();
expect(inputs[0].value).toBe(values.email);
expect(inputs[1].value).toBe(values.password);
});
});

0 comments on commit cc2cb41

Please sign in to comment.