Validating fields
formo supports field validations via Validators.
tsexport typeValidator <I ,O ,E > = (input :I ) =>Promise <Result <NonEmptyArray <E >,O >>;
tsexport typeValidator <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:
tsimport {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 ),});
tsimport {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:
tsfieldProps ("name").issues ;
tsfieldProps ("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:
tsvalidators .minLength (2, {message : "Name is too short",severity : 1 });
tsvalidators .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:
tsvalidators .inSequence (validators .minLength (2, "Too short"),validators .regex (/^[A-Z]/, "Must start with an uppercase letter"));
tsvalidators .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:
tsvalidators .inParallel (validators .minLength (2, "Too short"),validators .regex (/^[A-Z]/, "Must start with an uppercase letter"));
tsvalidators .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:
tsximport {useFormo ,validators ,success } from "@buildo/formo";ÂtypeFormValues = {profession ?: string;};Âexport constMyForm = () => {constinitialValues :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 >);};
tsximport {useFormo ,validators ,success } from "@buildo/formo";ÂtypeFormValues = {profession ?: string;};Âexport constMyForm = () => {constinitialValues :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
tsxconststartsWithUppercaseLetter = (errorMessage : string) =>validators .regex (/^[A-Z]/,errorMessage );
tsxconststartsWithUppercaseLetter = (errorMessage : string) =>validators .regex (/^[A-Z]/,errorMessage );
or create a completely custom one
tsxconst perfectNumberValidator = (errorMessage: string) => validators.validato r((n: number) => n === 42 ? success(n) : failure(errorMessage));
tsxconst perfectNumberValidator = (errorMessage: string) => validators.validato r((n: number) => n === 42 ? success(n) : failure(errorMessage));
and use them accordingly
tsxconst { 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), });
tsxconst { 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), });