Local fields
Local validation
When your form is complex you might want to split you form into multiple components.
In that case it might be preferable for you to declare the validator with the field and not on the form level.
In such scenario you can use the useInput
hook to achieve this goal.
Just pass the name of the field and and the validator as parameters of the useInput
hook.
import type { FormEvent } from 'react';
import type { IFormValues } from '@per-form/react';
import type { IProps } from '../types';
import { FormProvider, useForm, useInput } from '@per-form/react';
const validator = (values: IFormValues) =>
String(values.text).includes('foo') ? '' : 'Value does not include "foo"';
function Input() {
const { errors } = useInput({ id: 'toto', name: 'text', validator });
return (
<>
<input name="text" required />
{errors.all.text && <div className="error">{errors.all.text}</div>}
</>
);
}
export default function Demo(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}
const { formProps, ...context } = useForm({
...props,
onSubmit: handleSubmit,
});
return (
<FormProvider {...context}>
<form {...formProps}>
<Input />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
}
With the hook version you will have to add yourself the @per-form/react <FormProvider>
component !
By default, for performance reason, local errors won't be present in the errors
object at the form level.
In the demo we use filterLocalErrors=false
on useForm
and <Form>
to be able to display the error (when you use filterLocalErrors=false
error will also show up at the form level).
This is also the reason why the render counter still increments on local fields demos.
The local validator is not present in the global property of the error object at the form level.
Only global validators (validators set up at the form level) will show up in the global object.
With default filterLocalErrors=true
, if you have a global validator targeting a local field, then the global validator error will be :
- present in the
global
property and not present in thevalidator
property at the form level - not present in the
global
property but present in thevalidator
property at the field level
Local cross field validation
You can mix local validation and cross field validation using the useInputs
hook.
import type { FormEvent } from 'react';
import type { IFormValues } from '@per-form/react';
import type { IProps } from '../types';
import { FormProvider, useForm, useInputs } from '@per-form/react';
const validator = (values: IFormValues) =>
Number(values.a) + Number(values.b) === 42
? ''
: 'The sum must be equal to 42';
const names = ['a', 'b'];
function Inputs() {
const { errors } = useInputs({ names, validators: validator });
return (
<>
<input name="a" required type="number" />
<input name="b" required type="number" />
{errors.validator['a,b']?.error && (
<div className="error">{errors.validator['a,b'].error}</div>
)}
</>
);
}
export default function Demo(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}
const { formProps, ...context } = useForm({
...props,
onSubmit: handleSubmit,
});
return (
<FormProvider {...context}>
<form {...formProps}>
<Inputs />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
}
In that case the system will define an id for the validator based on the names (here 'a,b'
).
But you can also pass an object for the validators
property where key
are ids and values are validators:
useInputs({ names, validators: { sum: validator } })
Or simply use the id
property on useInputs
(it only works if you provide one validator):
useInputs({ id: 'sum', names, validators: validator })
Default values and transformers
You can also provide default values and transformers for local fields:
import type { FormEvent } from 'react';
import type { IFormValues } from '@per-form/react';
import type { IProps } from '../types';
import { FormProvider, useForm, useInput } from '@per-form/react';
function Input() {
const { errors } = useInput({
defaultValue: 0,
name: 'count',
transformer: Number,
});
return (
<>
<input name="count" required type="number" />
{errors.all.count && <div className="error">{errors.all.count}</div>}
</>
);
}
export default function Demo(props: IProps) {
function handleSubmit(_e: FormEvent<HTMLFormElement>, values: IFormValues) {
console.log(values);
}
const { formProps, ...context } = useForm({
...props,
onSubmit: handleSubmit,
});
return (
<FormProvider {...context}>
<form {...formProps}>
<Input />
<div className="actions">
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</div>
</form>
</FormProvider>
);
}