Installation & Usage
First install the module via yarn or npm:
- npm
- Yarn
npm install --save @10xjs/formyarn add @10xjs/formBasic Usage#
import {fields, Form, FormProvider, useFormStatus} from '@10xjs/form';SyntaxError: No-Inline evaluations must call `render`.
Field Mapping#
A core concept of @10xjs/form is a clean separation of form state and any inputs that interact with it. Each input is mapped to a slice of the form value state through the path prop. Field state is not directly accessible from your outer form component and should be accessed using useField (or the utility fields.* components). This pattern ensures that changes to the state of one field do not propagate as render updates to the entire form. A mapped field only re-renders when state under its path changes.
Custom mapped input example#
import {TextField} from '@material-ui/core';
const Field = (path) => { const {path, ...TextFieldProps} = props;
const [data, field] = useField(path);
let error;
if (data.touched) { error = data.error; }
return ( <TextField {...TextFieldProps} error={!!error} value={data.value} onFocus={() => field.focus()} onBlur={() => field.blur()} onChange={(event) => { field.setValue(event.target.value); }} helperText={error} /> );};
const ExampleForm = () => { return ( <FormProvider values={{ fieldA: 'example initial value', fieldB: 'example initial value', }} onSubmit={handleSubmit} > <Field path="fieldA" label="Field A" /> <Field path="fieldB" label="Field B" /> </FormProvider> );};The path of each field can map to any part of the form value state and can even share state with other fields. The useField hook is very handy for accessing state values - mapping to an input is not required!
Non-input useField example#
const Summary = () => { const [data] = useField('entries'); const entries = data.value;
return <div>{entries.length} entries</div>;};
const ExampleForm = () => { return ( <FormProvider values={{ entries: { a: {name: 'entry a', description: ''}, b: {name: 'entry b', description: ''}, c: {name: 'entry c', description: ''}, }, }} onSubmit={handleSubmit} > <fields.input path="entries.a.name" placeholder="Name A" /> <fields.input path="entries.a.description" placeholder="Description A" />
<fields.input path="entries.b.name" placeholder="Name B" /> <fields.input path="entries.b.description" placeholder="Description B" />
<fields.input path="entries.c.name" placeholder="Name C" /> <fields.input path="entries.c.description" placeholder="Description C" />
<Summary /> </FormProvider> );};Validation & Submit#
@10xjs/form handles validation as a single operation on the entire form value state through a validate callback. Validation errors are mapped to each field using the same path used to map value.
The validate callback is called whenever any part of the form value state changes or when the validate function itself changes (it may be necessary to wrap an inline validate function in useCallback to avoid unnecessary updates in some cases).
Each connected field has access to the current error state. Since validation is run continuously, use touched, dirty, and visited to conditionally display error messages at the field level.
Sync validation example#
const Field = (props) => { const [data] = useField(path);
let error;
if (data.touched) { error = data.error; }
return ( <label> {props.label} <br /> <fields.input path={props.path} /> {!!error && <div>{error}</div>} </label> );};
const ExampleForm = () => { return ( <FormProvider values={{ username: '', password: '', }} validate={(values) => { const errors = {};
if (values.username === '') { errors.username === 'Username is required.'; }
if (values.password === '') { errors.password === 'Password is required.'; }
return {}; }} onSubmit={handleSubmit} > <Form> <Field path="username" label="Username" /> <Field path="password" label="Password" /> </Form>
<button>Submit</button> </FormProvider> );};Validation errors can also be returned from the onSubmit handler by returning a failing SubmitResult object containing {ok: false, errors: [validation errors]}. This provides a mechanism to integrate API validation errors and to handle more expensive or async local validation operations.
The onSubmit callback must return a SubmitResult value (or a Promise resolving a SubmitResult value) with ok and data/error properties.
Submit validation example#
const ExampleForm = () => { return ( <FormProvider values={{ username: '', password: '', }} onSubmit={(values) => { const errors = {};
if (values.username === '') { errors.username === 'Username is required.'; }
if (values.password === '') { errors.password === 'Password is required.'; }
if (Object.keys(errors).length) { return {ok: false, errors}; }
return {ok: true}; }} > <Form> <Field path="username" label="Username" /> <Field path="password" label="Password" /> </Form>
<button>Submit</button> </FormProvider> );};