Using providers for validation is very handy but it introduces few usage problems of its own, for example how would you tell the current state of an entire form. Let's say you want to disable a button as long as the form is invalid, how would you do that?
The ValidationObserver is a convenient component that also uses the scoped slots feature to communicate the current state of your inputs as a whole.
Here is a small example, with our refactored components:
<ValidationObserver v-slot="{ invalid }">
<form>
<TextFieldWithValidation rules="required" v-model="first" />
<TextField WithValidation rules="required" v-model="second" />
<button :disabled="invalid">Submit</button>
</form>
</ValidationObserver>
The scoped slot is passed an object containing a flags object representing the merged state of all providers registered under the observer. It contains the following properties:
Name | Type | Description |
---|---|---|
dirty | boolean |
True if at least one field is dirty. |
pristine | boolean |
True if all fields are pristine (not dirty). |
valid | boolean |
True if all fields are valid. |
invalid | boolean |
True if at least one field is invalid. |
pending | boolean |
True if at least one field's validation is in progress. |
touched | boolean |
True if at least one field has been touched (blurred). |
untouched | boolean |
True if all fields haven't been touched (blurred). |
errors | { [x: string]: string[] } |
An object containing reference to each field errors, each field is keyed by its vid prop. |
validate | ({ silent: boolean }) => { then: () => Promise<any> } |
A method that triggers validation for all providers. Can be chained using then to run a method after validation. Mutates child providers state unless silent is true. |
passes | (cb: Function) => Promise<void> |
Calls validation like validate and mutates provider's state, accepts a callback to be run only if the validation is successful |
reset | () => void |
A method that resets validation state for all providers. |
Like providers, observers render a span
by default. You can customize the rendered tag using the tag
prop, for example a form
tag might be more useful.
<!-- Render a form -->
<ValidationObserver tag="form">
<!-- Fields -->
</ValidationObserver>
You can expand upon this by adding your form listeners like submit
on the observer directly:
<!-- Render a form -->
<ValidationObserver tag="form" @submit.prevent="onSubmit">
<!-- Fields -->
</ValidationObserver>
Sometimes it is unsuitable for a Observer component in principle to render anything extra, because of limitations in the Vue rendering output we cannot have multiple root nodes which limits the design choice to move away from renderless at the moment, in Vue 3.x it this may change with fragments.
A slim
prop can be used to force the component to be renderless, by default it is set to false
. The highlighted lines are the only rendered elements in the final output:
<!-- Only the form inside the observer will be rendered -->
<ValidationObserver slim>
<form>
<!-- Fields -->
</form>
</ValidationObserver>
Note that only the first child will be rendered when slim
is used, any other nodes will be dropped as you cannot have multiple root nodes in a renderless component. Be mindful of that when using the slim
prop.
<!-- Only form is rendered. -->
<ValidationObserver slim>
<form></form>
<div></div>
</ValidationObserver>
Validating before submit is very easy way, using the public methods and a simple ref we can validate all providers before submitting the form.
<template>
<ValidationObserver ref="observer" v-slot="{ invalid }" tag="form" @submit.prevent="submit()">
<TextFieldWithValidation rules="required" v-model="first" />
<TextFieldWithValidation rules="required" v-model="second" />
<button :disabled="invalid">Submit</button>
</ValidationObserver>
</template>
<script>
export default {
methods: {
async submit() {
const isValid = await this.$refs.observer.validate();
if (!isValid) {
// ABORT!!
}
// 🐿 ship it
}
}
};
</script>
:::tip $refs
Note that $refs
are not available until the first render, so make sure to add proper checks for that.
:::
You can trigger validation from the template using passes
method which is convenient for running a handler if the form is valid:
<template>
<ValidationObserver v-slot="{ invalid, passes }">
<form @submit.prevent="passes(submit)">
<TextFieldWithValidation rules="required" v-model="first" />
<TextFieldWithValidation rules="required" v-model="second" />
<button :disabled="invalid">Submit</button>
</form>
</ValidationObserver>
</template>
<script>
export default {
methods: {
submit() {
// No need to worry about form state
// as this is only runs when the form is valid
// 🐿 ship it
}
}
};
</script>
You can see observers in action here
While you can add errors manually to ValidationProvider
with the setErrors
method, it can be annoying to do so for multiple fields at once.
The ValidationObserver
exposes a setErrors
method that can set errors for its children providers.
<template>
<div>
<ValidationObserver ref="observer">
<ValidationProvider vid="field1" v-slot="{ errors }">
<input type="text" v-model="field1" />
<span id="error1">{{ errors[0] }}</span>
</ValidationProvider>
<ValidationProvider vid="field2" v-slot="{ errors }">
<input type="text" v-model="field2" />
<span id="error2">{{ errors[0] }}</span>
</ValidationProvider>
</ValidationObserver>
</div>
</template>
<script>
// Somewhere in a method, set the errors for each field.
this.$refs.observer.setErrors({
field1: ['wrong'],
field2: ['whoops']
});
</script>
Notice that the observer setErrors
takes a different argument than the ValidationProvider
method, as it accepts an object containing error arrays keyed by the vid
of each provider.
Refer to the live example for more information.
Like the validate
method, we could also reset our form after submitting the values to the server. There are a few things to keep in mind:
- The observer does not reset the values on your inputs, you have to do that yourself.
- It only resets the validation state, being the error messages and flags.
- Vue renders updates asynchronously.
<template>
<ValidationObserver ref="observer" tag="form" @submit.prevent="submit()" v-slot="{ invalid }">
<TextFieldWithValidation rules="required" v-model="first" />
<TextFieldWithValidation rules="required" v-model="second" />
<button :disabled="invalid">Submit</button>
</ValidationObserver>
</template>
<script>
export default {
methods: {
async submit() {
const isValid = await this.$refs.observer.validate();
if (!isValid) {
// ABORT!!
}
// 🐿 ship it
// sending to API...
// ...
// reset the values ...
this.first = '';
this.second = '';
// You should call it on the next frame
requestAnimationFrame(() => {
this.$refs.observer.reset();
});
}
}
};
</script>
Notice the usage of requestAnimationFrame, this is because Vue does not update the values immediately after you set them. So you need to reset the state after the UI has been stabilized, otherwise some inputs may report an invalid state.
You can use the ValidationObserver to group your fields using multiple observers and refs.
<template>
<div>
<ValidationObserver tag="form" ref="obs1" v-slot="{ invalid }">
<!-- Fields -->
</ValidationObserver>
<ValidationObserver tag="form" ref="obs2" v-slot="{ invalid }">
<!-- Fields -->
</ValidationObserver>
</div>
</template>
<script>
// Somewhere in a method ...
// validate the first observer.
this.$refs.obs1.validate();
// validate the second observer.
this.$refs.obs2.validate();
</script>
Building upon the previous example, observers can be nested to create nested forms for advanced use-cases. The outmost observer is able to trigger validation/resets on child observers and providers. Its state is also synced with the child observers and providers alike.
<ValidationObserver ref="op" v-slot="observer">
<ValidationObserver ref="oc">
<ValidationProvider rules="required" v-slot="provider">
<input type="text" v-model="value">
<span>{{ provider.errors[0] }}</span>
</ValidationProvider>
<!-- This is synced with the state of all children providers/observers -->
<pre>
{{ observer }}
</pre>
</ValidationObserver>
</ValidationObserver>
You can control the name of child observers by providing a vid
prop to it which will make it easier to access them.
Sometimes when building something like a multi-step form, you would need to use v-if
on your providers to toggle the visibility of your steps. However, when the provider is hidden and shown again, it does not keep its state.
You can use the persist
prop to allow the provider to remember its state across mounting/destroyed lifecycles, but there are a couple of caveats:
- Your Provider must be inside an observer component.
- Your Provider must have a
vid
property set.
<ValidationObserver>
<div v-if="!isHidden">
<ValidationProvider
rules="required|min:3|max:6"
vid="myfield"
:persist="true"
v-slot="{ errors }"
>
<input type="text" v-model="value">
{{ errors[0] }}
</ValidationProvider>
</div>
</ValidationObserver>
<button @click="isHidden = !isHidden">Toggle</button>
When querying the observer state, any errors will still be preserved even if the provider is not visible anymore.
Below is the reference of the ValidationObserver public API.
Prop | Type | Default Value | Description |
---|---|---|---|
tag | string |
span |
The default tag to render. |
slim | boolean |
false |
If true, it will make the observer renderless, only rendering the HTML inside its slot. |
disabled | boolean |
false |
If true, the observer will be ignored when validate is called by a parent observer. |
vid | string |
random string | if provided it will be used as the observer's id when being registered as a child of a parent observer. |
Those are the only methods meant for public usage, other methods that may exist on the ValidationObserver are strictly internal.
Method | Args | Return Value | Description |
---|---|---|---|
validate | { silent: boolean } |
Promise<boolean> |
Validates all the child providers/observers and mutates their state unless silent is true. |
reset | void |
void |
Resets validation state for all child providers/observers. |
The validation observer does not emit any events at this time.