Validation
Each field in a form can have two types of validation. The first one is based on the field type and comes with the included field set. All it normally does is check if a required field is filled out. Here's an example of such a type validation for plain Input fields:
const isValid = (value, config) => {
if (config.isRequired && (value === "" || typeof value === "undefined")) return false;
return true;
};
More complicated field types can have more complicated default validations, for example a date range field would validate that the "from date" is before the "to date".
Additionally, you can validate on a per field basis, using the customValidation
or the regexValidation
property in a field config.
This could look soemthing like this:
{
id: "username",
label: "Username",
type: "text",
isRequired: true,
customValidation: ({ data, isValid }) => {
return isValid && data.replace(/[^a-z0-9-]/gi, "") === data;
},
errorRenderer: (error) => <div style={{ color: "#f30" }}><br />Please fill out this field. Only alphanumeric values and '-' is allowed.</div>,
}
... or with Regex:
{
id: "password",
label: "Password",
type: "password",
isRequired: true,
regexValidation: "^[0-9]{4}[A-Z]{1}$"
},
The property can either be a string as in the example or an actual Regex: regexValidation: /^[0-9]{4}[A-Z]{1}$/i
The above mentioned date range custom validation:
{
id: 'fromDate',
label: "From",
type: 'date',
isRequired: true
},
{
id: 'toDate',
label: "To",
type: 'date',
isRequired: true,
customValidation: ({ data, allData, fieldConfig, isValid, isDirty, triggeringEvent }) => {
if (allData.fromDate && data) {
const fromDate = new Date(allData.fromDate);
const toDate = new Date(data);
// Make sure fromDate isn't bigger than toDate:
if (fromDate > toDate) return false;
}
return isValid;
}
}
The isValid
variable in the customValidation
function tells you what the type based validation of the field returns, which
means you don't have to check for the isRequired
property again.
You get following variables in the customValidation
function:
- data: The data of this specific field
- allData: All the data of the form
- fieldConfig: The configuration for the field
- isValid: The default type based validation (a boolean)
- isDirty: In case you only want to validate dirty fields (a boolean)
- triggeringEvent: The event which triggered this validation, for example
change
,blur
oraction
or your custom event
For basic validations, return a boolean, true
for a valid field, false
for when something isn't.
For more complicated validations, those that check for multiple things which can be wrong, you can return an error code in the
form of a string like this: "INVALID_LEADING_CHARS"
. You can than in the error renderer use the error code to decide which
error to output:
{
id: "address",
label: "Address",
type: "text",
customValidation: ({ data, isValid }) => {
if (isValid && data && data.startsWith("0")) return "INVALID_LEADING_CHARS";
return isValid;
},
errorRenderer: ({ errorCode }) => {
if (errorCode === "INVALID_LEADING_CHARS") return <div className="error">Don't ...</div>;
if (errorCode === "SOMETHING_ELSE") return <div className="error">Don't ...</div>;
return <div className="error">General error message ...</div>;
}
}
Async Validations
Sometimes you can only know if a field is valid after checking an API asynchronously. That's why we
added async validations. The work the same way as custom validations, just with the async await pattern.
To avoid issues with unstable api response times, Stages handles any race conditions for you, no worries.
Here's an example from the demos where we simulate an async call with a setTimeout
:
{
id: "title",
label: "Title",
type: "text",
isRequired: true,
customValidation: async ({ data }) => {
await new Promise(resolve => setTimeout(resolve, 2000));
return data && data.length % 2 !== 1;
},
validateOn: ["blur", "action"]
}
It's a good idea to disable the submit actions while async validations are running:
<button
type="button"
onClick={() => {}}
disabled={actionProps.isDisabled}
>
Submit
</button>
And fields can optionally display the state of async validations with the isValidating
prop:
{error && !isValidating ? errorRenderer ? errorRenderer(error) : (
<Error text="Please fill out this field!" error={error} />
) : null}
{isValidating ? <div style={{ color: "#999" }}>Field is validating ...</div> : null}
Global Per Type Validation
Often you want the same validation for all fields with the same type, for example emails or phone numbers. This can be achived with global type validations on the Form component:
<Form
typeValidations={{
email: {
validation: ({ data, isValid }) => {
return isValid && data.indexOf('@') > -1 && data.indexOf('.') > -1;
},
renderer: ({ errorCode }) => (
<div style={{ color: "red" }}>Please enter a valid email address.</div>
)
},
tel: {
validation: ({ data, isValid }) => {
return isValid && data.indexOf('+') === 0 && data.length === 13;
},
renderer: ({ errorCode }) => (
<div style={{ color: "red" }}>Please enter a valid phone number.</div>
)
}
}}
...
/>
Now each time use use a field of type email, that global validation will be used, together with the globally defined error renderer.
Btw, you can still overwrite the global validation if you create a custom validation on a specific field.
Validation Events
Per default, validation happens on actions which have validation switched on, eq:
actionProps.handleActionClick(onSubmit, true);
But you can set it up however you want, using the validateOn
property on the Form component, which accepts an array of event types.
<Form
validateOn={["action", "blur"]}
...
/>
You have following options:
- action: This is the default, validation on form actions
- init: This will validate on initialization of the form
- blur: This probably is the second most used, validation on input blur (or click on elemnts that don't support blur)
- change: This is the most busy, validating on every change of the fields values
- throttledChange: This is a throttled version of the change event (default: 400ms). To change the default, use the
throttleWait
property on the Form component. - focus: This validates on focusing of the field, for example when you click into an input, or tap navigate to a select
- collectionAction: And this one validates on collection actions, like add or delete an entry
- yourCustomEventName: If you have defined a custom event on the Form, you can use it here
Additionally, you can set this behaviour on each field individually, like this:
{
id: "password",
label: "Password",
type: "password",
validateOn: ["change"]
}
Which can be very useful for things like the password field above, if you want dynamically show how save it is and if it's save enough.
And for really complex validation behaviour, you can use a callaback to decide when to validate:
{
id: "dynamicValidate",
label: "Valid on even length (2, 4, 6 ...)",
type: "text",
isRequired: true,
validateOn: ({ data, fieldIsDirty, fieldConfig }) => {
return data && data.length > 5 ? ["change", "blur", "action"] : ["blur", "action"];
},
customValidation: ({ data, allData, isValid }) => {
if (isValid && data.length % 2 === 1) return "UNEVEN";
return isValid;
}
}
Custom Events
In very complicated forms, if you need to validate for example on blur and change, but very specifically based on entered data, you can define a custom event.
This can look like this (a more specific change event):
<Form>
customEvents={{
'autocompleteChange': ({ data, fieldValue, dirtyFields, optionsLoaded, asyncData, errors, focusedField, triggeringEvent }) => {
if (data.country && triggeringEvent === "change") return true;
return false;
}
}}
validateOn={["action", "autocompleteChange"]}
...
</Form>
Basically return true for all states where this event should be called and than use the key in one of your validateOn arrays, either on the Form or an individual field.
Another example from the demo:
<Form
customEvents={{
'onBlurAndChangeIfLong': ({ fieldValue, triggeringEvent }) => {
if (!fieldValue && triggeringEvent === "blur") return true;
if (typeof fieldValue === "string" && fieldValue.length >= 5 && triggeringEvent === "change") return true;
if (typeof fieldValue === "string" && fieldValue.length < 5 && triggeringEvent === "blur") return true;
return false;
}
}}
validateOn={["action", "onBlurAndChangeIfLong"]}
...
</Form>