Skip to main content

Errors and styling

By default @per-form/react use native form validation to avoid using any state at all (for performance reason).

But if you want to customize the display of form errors you can turn off the native validation with the useNativeValidation parameter.

With the useForm hook

You can directly access the error object from the useForm hook:

0
import type { FormEvent } from 'react';
import { type IFormValues, useForm } from '@per-form/react';

export default function Demo() {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { errors, formProps } = useForm({
onSubmit: handleSubmit,
useNativeValidation: false,
});

return (
<form {...formProps}>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
<button type="submit">Submit</button>
</form>
);
}

With the <Form> component

Render prop method

You can access the error object using the children of the <Form> component as a render prop:

0
import type { FormEvent } from 'react';
import { Form, type IFormContext, type IFormValues } from '@per-form/react';

export default function Demo() {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

return (
<Form onSubmit={handleSubmit} useNativeValidation={false}>
{({ errors }: IFormContext) => (
<>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
<button type="submit">Submit</button>
</>
)}
</Form>
);
}

Context method

Or you can access the error object using the context (in that case you need to use a sub-component).

You can use the useFormErrors hook to directly get access to the form errors:

0
import type { FormEvent } from 'react';
import { Form, type IFormValues, useFormErrors } from '@per-form/react';

function Input() {
const errors = useFormErrors();
return (
<>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
</>
);
}

export default function Demo() {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

return (
<Form onSubmit={handleSubmit} useNativeValidation={false}>
<Input />
<button type="submit">Submit</button>
</Form>
);
}

Styling

With CSS pseudo-classes

You can use :valid and :invalid CSS pseudo-classes to customize your fields depending on their internal validation state.

This works even if you choose not to use the native validation:

0
import type { IProps } from '../types';
import { type FormEvent, useId } from 'react';
import { type IFormValues, useForm } from '@per-form/react';

export default function Demo({ useNativeValidation }: IProps) {
const id = useId();
const safeId = id.replace(/:/g, '\\:');
const css = `#${safeId} input:valid {
background-color: rgba(0, 255, 0, 0.1);
border: 1px solid rgba(0, 255, 0, 0.8);
border-radius: 2px;
}
#${safeId} input:invalid {
background-color: rgba(255, 0, 0, 0.1);
border: 1px solid rgba(255, 0, 0, 0.8);
border-radius: 2px;
}`;

function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { errors, formProps } = useForm({
onSubmit: handleSubmit,
useNativeValidation,
});

return (
<form {...formProps} id={id}>
<style>{css}</style>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
<button type="submit">Submit</button>
</form>
);
}
info

A field is always either valid or invalid, so the style always applies.

With CSS classes

You can also use the error object to style your input if you want to.

The difference with the previous example is that it waits for you to submit the form before applying the styles:

0
import type { FormEvent } from 'react';
import { type IFormValues, useForm } from '@per-form/react';
import clsx from 'clsx';

export default function Demo() {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { errors, formProps } = useForm({
onSubmit: handleSubmit,
useNativeValidation: false,
});

return (
<form {...formProps}>
<input
className={clsx({ 'has-error': errors.all.text })}
name="text"
required
/>
{errors.all.text && <div className="error">{errors.all.text}</div>}
<button type="submit">Submit</button>
</form>
);
}
tip

You can also mix and match CSS classes with :valid and :invalid CSS pseudo-classes.

In this example we use the following CSS:

.has-error {
background-color: rgba(255, 0, 0, 0.1);
border: 1px solid rgba(255, 0, 0, 0.8);
border-radius: 2px;
}

.has-error:valid {
background-color: rgba(0, 255, 0, 0.1);
border: 1px solid rgba(0, 255, 0, 0.8);
border-radius: 2px;
}
note

You can also use the :has pseudo-class to achieve the same goal.

A CSS selector like input:has(+ .error) will apply some style on your input if it is followed by a element who has an "error" class.

The error object

Description

Inside the error object you can access all errors classified in different categories.

It has the following shape:

PropertyTypeDescription
nativeRecord<string, string>Contains errors only relative to native validation (required, min, maxlength, pattern...etc.). Keys are field names and values are error strings (empty string means no error)
validatorRecord<string, {error: string, global: boolean, names: string[]>Contains errors only relative to custom field validation (See guide page about validation for more information). Keys are validator id
manualRecord<string, string | null>Contains errors only relative to manual validation (See guide page about validation for more information). Keys are field names
globalRecord<string, {error: string, global: boolean, names: string[]>Contains errors only relative to custom field validation that are set at the form level (See guide page about validation for more information). Keys are validator id
allRecord<string, string>Contains all above errors, with one error per field (native errors first, then manual errors, then validator errors). Keys are field names
main{error: string, global: boolean, id: string, names: string[] }Contain the first field error (native errors first, then manual errors, then validator errors).

Example

This example showcase all type of errors:

0
import { DatePicker } from '@mui/x-date-pickers';
import dayjs, { type Dayjs } from 'dayjs';
import { type FormEvent, useState } from 'react';
import type { IProps } from '../types';
import { type IFormValues, useForm } from '@per-form/react';

const defaultValues = { mui: null };
const validators = {
mui: (values: IFormValues) => {
const date = values.mui as Dayjs;
return date?.date() > 15 ? '' : 'Choose a date after the 15th.';
},
};

export default function Demo({ useNativeValidation }: IProps) {
const [value, setValue] = useState<Dayjs | null>(null);

function handleReset() {
setValue(null);
}

function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}

const { errors, formProps, onChange, onError } = useForm({
defaultValues,
onChangeOptOut: 'mui',
onReset: handleReset,
onSubmit: handleSubmit,
useNativeValidation,
validators,
});

return (
<form {...formProps}>
<DatePicker
minDate={dayjs()}
name="mui"
onChange={onChange(setValue, { name: 'mui' })}
onError={onError('mui')}
slotProps={{ textField: { required: true } }}
value={value}
/>
{errors.all.mui && <div className="error">{errors.all.mui}</div>}
<div className="actions">
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</div>
</form>
);
}
  1. Submit without choosing any date and you will trigger the native validation (with the required attribute)
  2. Choose a date that is before the 15th of each month to trigger the validation error (with the validator).
  3. Enter a date like 01/01/2024 to see the error send back by the Material UI component (what we call "manual error", see the controlled components guide for more information).
info

global contains the same as validator because validators are set at the form level.

Check local validation for non global validators.