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
globalproperty and not present in thevalidatorproperty at the form level - not present in the
globalproperty but present in thevalidatorproperty 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>
);
}