-
Notifications
You must be signed in to change notification settings - Fork 0
Validators
On this page
- Validators
- How to use the builtin validators
- The
alwaysValid
validator - Validating
boolean
values - Validating related fields
- Custom validation rules
Validators are functions that takes a value to validate and returns undefined
if there is no error and an
error message (string
) if there is an error.
You can use the builtin validators from simple-form
or your own or a combination.
The builtin validators are available with the useValidationRules
hook like.
const { required, maxLength } = useValidationRules();
The builtin validators are functions that returns a Validator<T>
which in itself is a function, so when you
use a validator in a form definition, you must invoke it.
const fd = useFormDefinition<FormFields>({
fields: {
name: {
validators: [required(), maxLength(50)]
},
age: {
validators: [required()]
}
}
});
Some validators takes some configuration parameters, but they all take an optional errorMessage
as the last parameter. If
you call them without the errorMessage
parameter, they use the error messages from simple-form
, but you have the
opportunity to override it with a custom message.
const fd = useFormDefinition<FormFields>({
fields: {
name: {
- validators: [required(), maxLength(50)]
+ validators: [required("Please tell us your name."), maxLength(50, "Oops no more than 50 characters.)]
},
age: {
- validators: [required()]
+ validators: [required("Enter your age.")]
}
}
});
simple-form
exposes an alwaysValid
validator which always returns undefined
meaning no error.
This can come in handy when constructing an array of validators dynamically.
Assume our component has a prop telling if age
is a required field.
type Props = {
ageRequired: boolean;
};
const PersonForm = (props: Props) => {
const { ageRequired } = props;
//... the rest of the component
}
The age
field is defined like this so far:
age: {
validators: [required(), min(3), max(125)]
},
We only want to include the required
validator if ageRequired
is true
.
age: {
- validators: [required(), min(3), max(125)]
+ validators: ageRequired ? [required(), min(3), max(125)] : [min(3), max(125)]
},
This works, but as you can see we repeat the min(3), max(125)
validators because we want this regardless of
ageRequired
. If we had more validators we could easily forget one in the two situations. Instead
we can use alwaysValid
which makes it easier to create the array.
age: {
- validators: [required(), min(3), max(125)]
+ validators: [ageRequired ? required() : alwaysValid, min(3), max(125)]
},
Now we only specify the other validators one time.
Note
alwaysValid
is not a function like the rest of the validators. This is because the other validators are functions so they can take a custom error message, but asalwaysValid
never returns an error, it can be just a constant.
A common use case is to have a checkbox for something like Please accept our terms. You want the user to check this checkbox before submitting the form.
This can be accomplished with the equal
validator.
const fd = useFormDefinition<FormFields>({
fields: {
name: {
validators: [required(), maxLength(50)]
},
age: {
validators: [min(3), max(125)]
},
acceptTerms: {
validators: [required("Please accept the terms."), equal(true, "Please accept the terms")]
}
}
});
<Box display="flex" flexDirection="column" alignItems="flex-start" gap={1}>
<FormText formManager={fm} fieldName="name" label="Name" />
<FormNumber formManager={fm} fieldName="age" label="Age" />
<FormCheckbox formManager={fm} fieldName="acceptTerms" label="Please accept our terms" />
<Button onClick={handleSubmit}>SUBMIT</Button>
</Box>
We must also use the required
validator because all other validators should not report an
error for a null
value.
It is easy to create validation rules that ensures some relation between two or more fields. Let's create an example where the user should enter a persons working hours.
type FormFields = {
fromHours: number;
toHours: number;
};
const defaultValidators = [required(), min(5), max(23)];
const fd = useFormDefinition<FormFields>({
fields: {
fromHours: {
validators: defaultValidators
},
toHours: {
validators: defaultValidators
}
}
});
<Box display="flex" flexDirection="column" alignItems="flex-start" gap={1}>
<FormNumber formManager={fm} fieldName="fromHours" label="From hours" />
<FormNumber formManager={fm} fieldName="toHours" label="To hours" />
<Button onClick={handleSubmit}>SUBMIT</Button>
</Box>
Both values should be between 5 and 23 and we also want fromHours
to be less than toHours
.
fm.onChange.fromHours = value => {
fm.setValidators("toHours", [...defaultValidators, hasValue(value) ? min(value + 1) : alwaysValid]);
};
fm.onChange.toHours = value => {
fm.setValidators("fromHours", [...defaultValidators, hasValue(value) ? max(value - 1) : alwaysValid]);
};
If the user enters 8 in fromHours
and then 7 in toHours
the validators reports these errors.
This may look a little intrusive. We can make it better by having custom error messages.
fm.onChange.fromHours = value => {
fm.setValidators("toHours", [...defaultValidators, hasValue(value) ? min(value + 1, "To hours must be greater than From hours.") : alwaysValid]);
};
fm.onChange.toHours = value => {
fm.setValidators("fromHours", [...defaultValidators, hasValue(value) ? max(value - 1, "From hours must be less than To hours.") : alwaysValid]);
};
This example also shows some other useful concepts. We have some default validation rules for each field and
we don't want to duplicate them in the useFormDefinition
call and in the fm.onChange
event handler.
To avoid this, we create a local variable defaultValidators
and use it in both cases of the ternary expression.
Also note that we set the min
validator twice for the toHours
field. The first time min(5)
ensures that
the user can never enter a value below 5 and the second ensures that the value is less than the value of
the toHours
field. When validation is performed, it occurs in the order the validators appears in the array
and when an error is found, validation for that field is terminated.
If the user enters 7 in toHours
the validators for fromHours
would be:
[required(), min(5), max(23), min(6)]
This could of course also be accomplished by only using
min
once and calculate the minimum valueMath.max(5, 7-1)
, but this wouldn't make it possible to reusedefaultValidators
.
If the validators from simple-form
aren't sufficient, you can easily create your own.
Your validator must be a Validator<T>
where T
is the type it is validating - this ensures you can't by mistake
apply it to a field of another type than T
.
The validator should return undefined
if there are no errors and an error message otherwise. The validator should not
return any errors if the value is null
(because this should be handled by the required
validator).
Say you only want to allow odd numbers for a field.
const odd: Validator<number> = value => (hasValue(value) && value % 2 !== 0 ? "Must be an odd number" : undefined);
You can then use it like this:
mustBeOdd: {
validators: [odd]
}
If you are exporting the validator and intent to use it in other places, you should create it like the builtin validators, taking a parameter for an optional custom error message.
const odd =
(errorMessage?: string): Validator<number> =>
value =>
hasValue(value) && value % 2 !== 0 ? errorMessage ?? "Must be an odd number" : undefined;
And then invoke it when you use it:
mustBeOdd: {
validators: [odd()]
}
Have you noticed that you don't have to tell the form components when the value is required other than using the
required
validator? How does simple-form
find out that among the array of validators (which are functions)
one of them is the required
validator? It does so by looking at the name of the function. This is the reason the
required
validator is implemented using function
syntax and not arrow syntax.
const required = <T>(errorMessage?: string): Validator<T> =>
// Must be written like this because the name of the function returned must start with "required".
function required(value) {
return hasValue(value) ? undefined : errorMessage ?? "Value is required";
};
You can create your own validators for required (for instance I use a special version for usage with Facebook's draft editor). The only requirement is that you implement it as above and that your function starts with "required" in the name.