Skip to main content

Validation

User input is validated in four separate scenarios:

  • When the input value changes (if the validateOnChange prop is true)
  • When the input is blurred (if the validateOnBlur prop is true)
  • When the submitForm function is run
  • Imperatively, by using the validateField and validateForm functions

If the user input is invalid, the form submission is stopped and the user defined handleSubmit function is not run.

There are three ways to implement validation into forms, each with their own pros and cons.

Error types

Errors are described within the system by the FormError type, this allows fine control over the position and content of the error, as well as giving us the ability to group related errors. Grouping errors is useful when multiple fields are related, will always error at the same time and showing the same (or an extremely similar) error underneath all of the related fields is redundant.

In the simplest case, the implementer will only need to define the error as a string, and the system will normalise this error into the required shape. A standard default for type (inline) will be applied when this happens.

type FormError = {
key?: string;
type?: 'inline' | 'global';
title?: string;
message: string;
} | null;

Validation library

The @rexlabs/form library is integrated with the @rexlabs/validator library, which allows the implementer to send a validation configuration object to the form's validate prop. This is the preferred method of validating fields as it's possible to extract the required validation from the configuration and automatically default a fields optional prop if the required validation is not found.

@rexlabs/validator is a small wrapper around the laravel style object validation library, validatorjs.

Basics

type ValidateProp = {
messages?: { [key: string]: string };
definitions: { [key: string]: Definition };
};

The validate prop takes a configuration object with two keys, messages and definitions.

Custom messages

The messages object is a dictionary of custom error messages for any validation rule defined by validatorjs. These will be applied globally across all fields that implement the same rule. The custom message can use the field name as part of the message by using the :attribute template string.

const messages = {
required:
'The galactic republic will be upset if you forgo the :attribute field!'
};
info

To create a custom message for a particular rule for a specific field the message has to be part of the validation Definition

Rule definitions

type ValidationRule = {
key?: string;
type?: 'inline' | 'global';
title?: string;
message?: string;
rule: string | ((values: any, helpers: any) => string | null);
};

type Definition = {
name?: string;
rules: string | (ValidationRule | string)[];
};

The definitions sub-object should have a corresponding Definition object for each field that requires validation. For nested fields these keys should still be a single string but separating each nested property with a period. Wildcard characters (denoted by *) can be used to make sure field arrays have the same validation.

Each definition can have a name that will be used instead of the Field name, in cases where the field name itself isn't very user-friendly.

The rules can either be a simple rule string (or list of rule strings) defined by validatorjs, or it can be a ValidationRule definition that allows for more customisation of the error. These rules are always evaluated in the order that they appear in the array.

Validation rule

The key is a way to group like global errors, fields that return errors with the same key along with the type of global won't show their own errors inline, and instead the first error found will be displayed as a global error.

The type determines where the error will be displayed, inline is shown underneath the input, global is shown above the form in an error banner.

The title is specific to global errors and determined the title of the error banner.

The message is a custom message to display for this specific validation rule against this field. Like the global messages described above, it also replaces the :attribute template with the field name.

The rule is either a rule string defined by validatorjs, or a function that returns a string describing the error, or null when the validation passes.

Example

import passwordValidator from 'password-validator';

var schema = new passwordValidator();

schema
.is()
.min(8)
.is()
.max(100)
.has()
.uppercase()
.has()
.lowercase()
.has()
.symbols(2)
.has()
.digits(2)
.has()
.not()
.spaces();

const validate = {
definitions: {
firstName: {
// Lowercase because the default message has
// the :attribute in the middle of the sentence
name: 'first name',
rules: [
{
rule: 'required',
message: 'The :attribute field is like, super required'
}
]
},
lastName: {
name: 'last name',
rules: 'required'
},
email: {
rules: ['required', 'email']
},
password: {
rules: [
'required',
{
rule: (values) => {
const { password } = values;
if (!schema.validate(password)) {
return 'This password is not valid';
}
return null;
}
},
{
key: 'password',
type: 'global',
title: 'Password mismatch',
rule: 'same:confirmPassword'
}
]
},
confirmPassword: {
name: 'password confirmation',
rules: [
'required',
{
key: 'password',
type: 'global',
title: 'Password mismatch',
rule: 'same:password'
}
]
},
// Field arrays with only one input in each section
'phoneNumbers.*': {
rules: ['required', 'numeric']
},
// Field arrays with nested fields can also be validated
'address.*.street': {
rules: 'required'
},
'address.*.suburb': {
rules: 'required'
}
}
};

Custom validation function

The validate function of the ReactForms component also accepts a function as it's value, which expects an object to be returned that has the same shape as the values, with each value being of the type string or FormError for more control.

caution

This API is much more manual and error prone, it should only be used very sparingly for things that can't be achieved using the validation configuration.

type FormHelpers<T, R> = {
setName: (name: string) => void;
setFieldArrayName: (name: string) => void;
setFieldValue: (name: string, value: any) => void;
setValues: (values: Partial<T>) => void;
setFieldTouched: (name: string, touched: boolean) => void;
setTouched: (touched: BooleanObject) => void;
setFieldError: (name: string, error: string | FormError) => void;
setServerError: (error: string | FormError) => void;
setErrors: (errors: FormErrorObject) => void;
setMeta: (meta: any) => void;
resetField: (name: string, value?: any) => void;
resetForm: (values?: Partial<T>) => void;
validateField: (name: string, value: any) => void;
validateForm: () => void;
submitForm: (args?: {
shouldReset?: boolean;
onValidationFail?: ValidationFailArg;
}) => Promise<R> | R | boolean | void;
};

type ValidatorReturn = {
[name: string]: string | FormError | FormErrorObject | FormErrorObject[];
};

type ValidationFunction<T, R> = (
values: any,
helpers: FormHelpers<T, R>
) => ValidatorReturn;

Example

import React from 'react';

import { ReactForms, Form, Field } from '@rexlabs/form'
import { TextInput } from '@rexlabs/text-input';

const validate = (values) => {
const { firstName, lastName } = values;
let errors = {};

if (!firstName) {
errors = { ...errors, firstName: 'First name is a required field!' };
}

if (!lastName) {
errors = { ...errors, lastName: 'Last name is required too!' }
}

return errors;
}

export function ValidatedForm() {
return (
<ReactForms validateOnBlur validate={validate}>
{() => {
return (
<Form>
<Field
name='firstName'
label='First name'
Input={TextInput}
>
<Field
name='lastName'
label='Last name'
Input={TextInput}
>
</Form>
)
}}
</ReactForms>
)
}