Skip to content

Validation

Form validation is one of the most important features of any form library. The main goal of a client-side validation library isn’t protection or security, but to provide a better user experience.

By providing more immediate feedback on the client side, the user can fix their mistakes before submitting the form. This has a couple of benefits:

  • Reduces the likelihood of a form being submitted with invalid data, allowing your server to process requests that are more likely to succeed.
  • Provides a better user experience by giving immediate feedback on the client side.

Formwerk bakes validation into any of the component composables that you use. This means that you don’t need to worry about implementing validation logic yourself.

Formwerk makes use of a couple of different validation systems:

  • HTML Constraint Validation API: Done via the various validation attributes that you can add to your form fields like required, minlength, maxlength, min, max, type, etc.
  • Standard Schema Validation: A schema-based validation that uses JavaScript implemented by multiple providers like Zod, Valibot, and Arktype.

Both systems can be used together, and you can even mix between them.

HTML Constraint Validation API

The HTML constraint API at its core is pretty simple. It’s a set of attributes that you can add to your form fields, and they enforce rules on the field’s value.

Each field has some attributes that can be set; some of these attributes are specific to the type of the field. Each field guide shows you which attributes are available for that field, so we won’t cover them in detail here.

However, you need to keep in mind some caveats around this:

  • Rules like maxLength for text fields or min/max for number fields are “preventative”. This means they don’t allow values that violate the rule or they prevent violation. Unlike other attributes like minLength and required, which are “suggestive”, in other words, the user can violate them and see an error message. In either case, both prevent submission.
  • Some validation rules are applied implicitly based on the field’s type. For text fields, setting a [type="email"] will automatically apply the browser’s email validation. Similarly, other types like [type="url"] will apply URL validation. These rules are also “suggestive”, as in the user can violate them and see an error message.
  • Validation messages are always set in the user’s language. If the user’s locale is German, for example, the message will be in German regardless of the language of your application.

Given these caveats and the limited capabilities for advanced cases, many apps today prefer to use a schema-based validation library like Zod or Yup, which is also supported by Formwerk.

Disabling HTML5 Validation

In some cases, you may need to disable HTML validation messages. One common reason is language mismatch between the browser and the website.

You can disable HTML5 validation by setting disableHtmlValidation to true. This option exists as a prop on fields, forms, form groups, or as a global configuration with the configure function.

import { configure } from '@formwerk/core';
configure({
disableHtmlValidation: true,
});

Standard Schema Validation

Formwerk supports the Standard Schema Spec and leverages it to provide a uniform schema validation with full type safety for all libraries that support it.

At the moment, the following providers implement the Standard Schema Spec and are supported by Formwerk:

Here is an example that uses Valibot:

import * as v from 'valibot';
import { useForm } from '@formwerk/core';
const schema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const { values, handleSubmit } = useForm({
schema,
});

Form Types

As mentioned in the form guide, standard schemas automatically offer type inference for the form’s current values and the submit values. This is incredibly useful to avoid having to cast and re-check values when submitting them just because TypeScript isn’t aware of the runtime validation.

When using a Standard Schema, you don’t have to do anything special to get these benefits. Types are automatically inferred from the given schema.

import { useForm } from '@formwerk/core';
import * as v from 'valibot';
const schema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const { values, handleSubmit } = useForm({
schema,
});
values; // { email?: string | undefined; password?: string | undefined }
const onSubmit = handleSubmit((data) => {
data.toObject(); // { email: string; password: string }
});

The same logic applies to form groups; however, form groups do not have output types.

import { useFormGroup } from '@formwerk/core';
import * as v from 'valibot';
const schema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const { getValues } = useFormGroup({
schema,
});
getValues(); // { email?: string | undefined; password?: string | undefined }

Unified Validation API

As you have probably noticed in the field guides, Formwerk unifies standard schema validation with the HTML constraint validation API.

Under the hood, if using a Standard Schema, Formwerk takes the errors and sets them on the field’s validationMessage property. That means you can make use of pseudo-classes like :invalid and :valid in CSS to style your fields regardless of the validation system you use. We made use of this a few times in the styled examples.

Validation Triggers and Error Display

Formwerk always displays the error messages regardless of whether the user has interacted with the field or not. This is done to avoid confusion for you as the developer and puts you in control.

So instead of “When to validate?”, you can think of it as “When to display errors?” which is less complicated and less prone to errors.

Validation Events

Formwerk by default validates on the following events:

  • blur: When a field loses focus.
  • change: When a field changes its value.
  • submit: When a form is submitted.
  • click: For some types of fields like checkbox or radio.
  • invalid: An event that fires if the field becomes invalid as a result of HTML constraint validation.

At this moment, you can’t change the default validation events.

Error Display

Now that you know you will always have errors available, you can decide when to display them.

All Formwerk components expose a few properties that can help you with error display:

  • errors: An array of all error messages if the field is invalid.
  • errorMessage: The error message if the field is invalid. Always the first element of the errors array.
  • isTouched: A boolean indicating if the field has been interacted with (blurred).
  • isValid: A boolean indicating if the field is valid.
  • isDirty: A boolean indicating if the field’s value has changed.
  • displayError: A function that returns the error message if the field has been touched and is invalid.

Given these properties, you can mix between them to produce the desired behavior.

Here’s an example that shows errors only when the field is touched:

<script setup lang="ts">
import { type TextFieldProps, useTextField } from '@formwerk/core';
const props = defineProps<TextFieldProps>();
const {
inputProps,
labelProps,
errorMessage,
errorMessageProps,
isTouched,
descriptionProps,
} = useTextField(props);
</script>
<template>
<div>
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" :style="{ display: 'block' }" />
<div v-if="isTouched && errorMessage" v-bind="errorMessageProps">
{{ errorMessage }}
</div>
</div>
</template>
<script setup lang="ts">
import TextField from './TextField.vue';
</script>
<template>
<TextField label="Email" type="email" required />
</template>

Notice that even though the error message is present initially, it is still not displayed. This is because the field is not yet touched.

You can shorten this logic by using the displayError function.

<script setup lang="ts">
import { type TextFieldProps, useTextField } from '@formwerk/core';
const props = defineProps<TextFieldProps>();
const {
inputProps,
labelProps,
displayError,
errorMessageProps,
descriptionProps,
} = useTextField(props);
</script>
<template>
<div>
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" :style="{ display: 'block' }" />
<div v-bind="errorMessageProps">
{{ displayError() }}
</div>
</div>
</template>
<script setup lang="ts">
import TextField from './TextField.vue';
</script>
<template>
<TextField label="Email" type="email" required />
</template>