Validating fields

formo supports field validations via Validators.

ts
export type Validator<I, O, E> = (
input: I
) => Promise<Result<NonEmptyArray<E>, O>>;
ts
export type Validator<I, O, E> = (
input: I
) => Promise<Result<NonEmptyArray<E>, O>>;

A Validator is a function that takes the field's value as input I and returns a Result of either errors NonEmptyArray<E> or a valid output O.

Note that Validators may trasform the field's value other than just validating it.

Simple Validation#

formo provides a number of Validators for common use cases via a validators utility. For example, to make sure a text field is at least 2 characters long:

ts
import { useFormo, validators, success } from "@buildo/formo";
 
const { fieldProps } = useFormo(
{
initialValues: {
name: "",
},
fieldValidators: () => ({
name: validators.minLength(2, "Name is too short"),
}),
},
{
onSubmit: async (values) => success(values),
}
);
ts
import { useFormo, validators, success } from "@buildo/formo";
 
const { fieldProps } = useFormo(
{
initialValues: {
name: "",
},
fieldValidators: () => ({
name: validators.minLength(2, "Name is too short"),
}),
},
{
onSubmit: async (values) => success(values),
}
);

Possible validation errors can be accessed via the issues field:

ts
fieldProps("name").issues;
ts
fieldProps("name").issues;
tip

The type of issues depends on the validator error type E.

For instance, if we were to use a validator as follows:

ts
validators.minLength(2, { message: "Name is too short", severity: 1 });
ts
validators.minLength(2, { message: "Name is too short", severity: 1 });

then the type of issues would be

NonEmptyArray<{ message: string, severity: number }> | undefined

Multiple validations on a field#

Some fields may require multiple validations. We can combine validations using the inSequence and inParallel combinators.

As the name suggests, inSequence runs validations one after the other and the field's issues will contain the first validation that failed:

ts
validators.inSequence(
validators.minLength(2, "Too short"),
validators.regex(/^[A-Z]/, "Must start with an uppercase letter")
);
ts
validators.inSequence(
validators.minLength(2, "Too short"),
validators.regex(/^[A-Z]/, "Must start with an uppercase letter")
);

Alternatively, we can run the same validations in parallel:

ts
validators.inParallel(
validators.minLength(2, "Too short"),
validators.regex(/^[A-Z]/, "Must start with an uppercase letter")
);
ts
validators.inParallel(
validators.minLength(2, "Too short"),
validators.regex(/^[A-Z]/, "Must start with an uppercase letter")
);

In this case, the field's issues will contain all the failed validations.

Transforming values#

If we take a look at how Validator is defined, we will notice that it has both an input and an output type. While it's common that they are the same, this also means that Validators can produce a value which is different than the one in input.

note

This capability means that we're technically "parsing" instead of "validating" the field values.

We sticked to "validation" to preserve familiarity with the term in the context of forms.

Here's an excellent blog post that explains the difference between parsing and validating.

One example of validation that transforms the value is the validators.defined validator:

tsx
import { useFormo, validators, success } from "@buildo/formo";
 
type FormValues = {
profession?: string;
};
 
export const MyForm = () => {
const initialValues: FormValues = {
profession: undefined,
};
 
const { fieldProps, handleSubmit } = useFormo(
{
initialValues,
fieldValidators: (_) => ({
profession: validators.defined("You must select a profession"),
}),
},
{
onSubmit: async (values) => success(values),
}
);
 
return (
<div>
<RadioGroup
label="Profession"
options={["Developer", "Designer", "Other"]}
{...fieldProps("profession")}
/>
<button onClick={handleSubmit}>Submit</button>
</div>
);
};
tsx
import { useFormo, validators, success } from "@buildo/formo";
 
type FormValues = {
profession?: string;
};
 
export const MyForm = () => {
const initialValues: FormValues = {
profession: undefined,
};
 
const { fieldProps, handleSubmit } = useFormo(
{
initialValues,
fieldValidators: (_) => ({
profession: validators.defined("You must select a profession"),
}),
},
{
onSubmit: async (values) => success(values),
}
);
 
return (
<div>
<RadioGroup
label="Profession"
options={["Developer", "Designer", "Other"]}
{...fieldProps("profession")}
/>
<button onClick={handleSubmit}>Submit</button>
</div>
);
};

As we discussed, onSubmit is only ever called after all field validations succeed, and this is reflected in the types.

In this example profession has type string, while the non-validated field is string | undefined.

This is a very powerful capability, because it allows you to preserve in the types some useful information you checked during validation.

info

Due to a known issue, a transforming validator value's type might result unknown in the onSubmit callback.

To avoid it, specify the the fieldValidators function argument and let the type inference do the work:

fieldValidators: (_) => ({ profession: validators.defined("You must select a profession") })
fieldValidators: (_) => ({ profession: validators.defined("You must select a profession") })

Otherwise, you can specify the type of each validator:

fieldValidators: () => ({ profession: validators.defined<string | undefined, string>("You must select a profession"), })
fieldValidators: () => ({ profession: validators.defined<string | undefined, string>("You must select a profession"), })

but it is not recommended due to verbosity and error-proness.

Defining custom validations#

formo comes with a set of common validators, but you can of course augment them by providing your own.

For instance, you could leverage existing validators

tsx
const startsWithUppercaseLetter = (errorMessage: string) => validators.regex(
/^[A-Z]/, errorMessage
);
tsx
const startsWithUppercaseLetter = (errorMessage: string) => validators.regex(
/^[A-Z]/, errorMessage
);

or create a completely custom one

tsx
const perfectNumberValidator = (errorMessage: string) => validators.validator(
(n: number) => n === 42 ? success(n) : failure(errorMessage)
);
tsx
const perfectNumberValidator = (errorMessage: string) => validators.validator(
(n: number) => n === 42 ? success(n) : failure(errorMessage)
);

and use them accordingly

tsx
const { fieldProps } = useFormo(
{
initialValues: {
name: "",
age: 0,
},
fieldValidators: () => ({
name: startsWithUppercaseLetter("Name must start with uppercase letter"),
age: perfectNumberValidator("Age must be 42"),
}),
},
{
onSubmit: async (values) => success(values),
}
);
tsx
const { fieldProps } = useFormo(
{
initialValues: {
name: "",
age: 0,
},
fieldValidators: () => ({
name: startsWithUppercaseLetter("Name must start with uppercase letter"),
age: perfectNumberValidator("Age must be 42"),
}),
},
{
onSubmit: async (values) => success(values),
}
);