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:
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:
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:
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:
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>
);
}
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:
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>
);
}
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;
}
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:
Property | Type | Description |
---|---|---|
native | Record<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) |
validator | Record<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 |
manual | Record<string, string | null> | Contains errors only relative to manual validation (See guide page about validation for more information). Keys are field names |
global | Record<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 |
all | Record<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:
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>
);
}
- Submit without choosing any date and you will trigger the native validation (with the
required
attribute) - Choose a date that is before the 15th of each month to trigger the validation error (with the validator).
- 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).
global
contains the same as validator
because validators are set at the form level.
Check local validation for non global validators.