:clipboard: DirectForm
A simple way to modify complex data with react forms
Install
npm install react-directform
Quickstart
import React, { useState } from 'react';
import { DirectForm } from 'react-directform';
import { object, string } from 'yup';
function App() {
const [value, setValue] = useState();
const schema = object().schema({
firstname: string().required('First name is required'),
lastname: string().required('Last name is required'),
});
return (
<DirectForm schema={schema} value={value} onSubmit={setValue}>
{({ register, submit, getError }) => (
<form onSubmit={submit}>
<input {...register('firstname')} />
{getError('firstname')}
<input {...register('lastname')} />
{getError('lastname')}
<input type={'submit'} />
</form>
)}
</DirectForm>
);
}
Usage Guide
The intentions behind creating this library were, to keep everything as near as possible to the react standard toolset.
This means, that this library does not use states outside react or interfere with form fields directly. This may seem as
a waste of performance at first but highly increases the overall compatibility with common component frameworks.
Concept
Using this library is as easy as one-two-three. The DirectForm
component has a single required property
called value
which includes the initial state of the form. As soon as the form gets submitted, the onSubmit
event callback gets triggered and can be used to update the state containing the value object as shown in the example
below.
import { DirectForm } from "./DirectForm";
function App() {
const [value, setValue] = useState({ firstname: 'Max', lastname: 'Mustermann' });
return (
<DirectForm value={value} onSubmit={setValue}>
{({ getValue, setValue, submit }) => (
...
)}
</DirectForm>
);
}
Manually adding input fields
To add an input field to the form, just assign the corresponding methods to the input events.
import { DirectForm } from "./DirectForm";
function App() {
const [value, setValue] = useState({ firstname: 'Max', lastname: 'Mustermann' });
return (
<DirectForm value={value} onSubmit={setValue}>
{({ getValue, setValue, submit }) => (
<input name={'firstname'} onChange={(e) => setValue('firstname', e.target.value)}
value={getValue('firstname')} />
<input name={'lastname'} onChange={(e) => setValue('lastname', e.target.value)} value={getValue('lastname')} />
<button onClick={submit}>Submit</button>
)}
</DirectForm>
);
}
Register fields automatically
In the majority of cases the pattern of assigning setValue
to onChange
and getValue
to value
is used.
That's exactly what the register
method is doing behind the scenes. It also sets the name of the input the fields
values name.
import { DirectForm } from "./DirectForm";
function App() {
const [value, setValue] = useState({ firstname: 'Max', lastname: 'Mustermann' });
return (
<DirectForm value={value} onSubmit={setValue}>
{({ register, submit }) => (
<input {...register('firstname')} />
<input {...register('lastname')} />
<button onClick={submit}>Submit</button>
)}
</DirectForm>
);
}
Validation
To validate the form to ensure, that the value returned by the onSubmit
handler is always valid, you can use
a Yup validation schema. If the schema fails to validate the form, getError
will
result in the corresponding error message.
import React, { useState } from 'react';
import { DirectForm } from 'react-directform';
import { object, string } from 'yup';
function App() {
const [value, setValue] = useState();
const schema = object().schema({
firstname: string().required('First name is required'),
lastname: string().required('Last name is required'),
});
return (
<DirectForm schema={schema} value={value} onSubmit={setValue}>
{({ register, submit, getError }) => (
<form onSubmit={submit}>
<input {...register('firstname')} />
{getError('firstname')}
<input {...register('lastname')} />
{getError('lastname')}
<input type={'submit'} />
</form>
)}
</DirectForm>
);
}
Access form from hook
It is possible to access all methods of the form using the useDirectForm
hook.
import React, { useState } from 'react';
import { DirectForm, useDirectForm } from 'react-directform';
import { object, string } from 'yup';
function PersonForm() {
const { register, getError } = useDirectForm();
return (
<>
<input {...register('firstname')} />
{getError('firstname')}
<input {...register('lastname')} />
{getError('lastname')}
</>
);
}
function App() {
const [value, setValue] = useState();
const schema = object().schema({
firstname: string().required('First name is required'),
lastname: string().required('Last name is required'),
});
return (
<DirectForm schema={schema} value={value} onSubmit={setValue}>
{({ register, submit, getError }) => (
<form onSubmit={submit}>
<PersonForm />
<input type={'submit'} />
</form>
)}
</DirectForm>
);
}
Custom register method
The default behaviour of the register
method is to assign the correct values to onChange
, value
, onBlur
and name
of the input field. For some component libraries this may not be the correct assignment or additional
fields need to be mapped. This can by either not using register at all or assigning a custom register method as shown
below for Material-UI.
import React, { useState } from 'react';
import { DirectForm } from 'react-directform';
import { object, string } from 'yup';
import { TextField } from '@material-ui/core';
function App() {
const [value, setValue] = useState();
const schema = object().schema({
firstname: string().required('First name is required'),
lastname: string().required('Last name is required'),
});
// Custom register method to add the validation errors to material ui TextFields
function register(ctx: DirectFormContextData<any>, org: DirectFormContextData<any>['register']) {
return (path: string, value?: 'value' | 'checked') => ({
...org(path, value),
error: !!ctx.getError(path),
helperText: ctx.getError(path),
});
}
return (
<DirectForm customRegister={register} schema={schema} value={value} onSubmit={setValue}>
{({ register, submit, getError }) => (
<form onSubmit={submit}>
<TextField {...register('firstname')} />
{getError('firstname')}
<TextField {...register('lastname')} />
{getError('lastname')}
<input type={'submit'} />
</form>
)}
</DirectForm>
);
}
Global form settings
You can assign specific config options to all DirectForm
components by providing them with
the DirectFormSettings
component.
import React, { useState } from 'react';
import { DirectForm, DirectFormSettings } from 'react-directform';
import { object, string } from 'yup';
import { TextField } from '@material-ui/core';
function App() {
const [value, setValue] = useState();
const schema = object().schema({
firstname: string().required('First name is required'),
lastname: string().required('Last name is required'),
});
// Custom register method to add the validation errors to material ui TextFields
function register(ctx: DirectFormContextData<any>, org: DirectFormContextData<any>['register']) {
return (path: string, value?: 'value' | 'checked') => ({
...org(path, value),
error: !!ctx.getError(path),
helperText: ctx.getError(path),
});
}
return (
<DirectFormSettings customRegister={register}>
<DirectForm schema={schema} value={value} onSubmit={setValue}>
...
</DirectForm>
<DirectForm schema={schema} value={value} onSubmit={setValue}>
...
</DirectForm>
</DirectFormSettings>
);
}
Deep object creation
This library is specifically useful if you need to create complex objects. All method which allow a property name as a
parameter support the common object path syntax.
import { DirectForm } from "./DirectForm";
function App() {
const [value, setValue] = useState({
person: { firstname: 'Max', lastname: 'Mustermann' }
});
return (
<DirectForm value={value} onSubmit={setValue}>
{({ getValue, register, submit }) => (
<input {...register('person.firstname')} />
<input {...register('person.lastname')} />
<button onClick={submit}>Submit</button>
)}
</DirectForm>
);
}
Dynamic lists
As all functions allow the object path syntax, it is also easily possible to create lists.
import { DirectForm } from "./DirectForm";
function App() {
const [value, setValue] = useState({
person: { firstname: 'Max', lastname: 'Mustermann' },
estates: [{ price: 10000 }, { price: 20000 }]
});
return (
<DirectForm value={value} onSubmit={setValue}>
{({ getList, register, submit }) => (
<input {...register('person.firstname')} />
<input {...register('person.lastname')} />
{getList('estates').map((data, index) => (
<input {...register(`estates[${index}].price`)} />
))}
<button onClick={submit}>Submit</button>
)}
</DirectForm>
);
}
Nested Forms
It's also possible to create nested forms to validate subsets of the data or allow partial submits. This can be especially useful when the path to the field is not static, or the form can be used with different data structures as demonstrated below. The registerForm
method is similar to the register
method used for fields. It automatically sets the value
property of the DirectForm
component to the getValue
method of the parent form and the onSubmit
method to the setValue
method. This way the sub form does not have to know the static path to zip
, city
and street
as from its point of view the object it modifies is completely flat.
import { DirectForm, useFormControl } from "./DirectForm";
function AddressForm() {
const { register } = useFormControl();
return (
<>
<input {...register('zip')} />
<input {...register('city')} />
<input {...register('street')} />
</>
);
}
function App() {
const [value, setValue] = useState({
personAddress: {},
estateAddress: {}
});
return (
<DirectForm value={value} onSubmit={setValue}>
{({ registerForm, submit }) => (
<DirectForm {...registerForm('personAddress')}>
<AddressForm />
</DirectForm>
<DirectForm {...registerForm('estateAddress')}>
<AddressForm />
</DirectForm>
<button onClick={submit}>Submit</button>
)}
</DirectForm>
);
}
API
DirectForm
value
Property | Type | Default | Description |
---|
| | | |
Example
onChange
Property | Type | Default | Description |
---|
| | | |
Example
onSubmit
Property | Type | Default | Description |
---|
| | | |
Example
prefix
Property | Type | Default | Description |
---|
| | | |
Example
submitOnChange
Property | Type | Default | Description |
---|
| | | |
Example
submitOnBlur
Property | Type | Default | Description |
---|
| | | |
Example
validateOnInit
Property | Type | Default | Description |
---|
| | | |
Example
validateOnChange
Property | Type | Default | Description |
---|
| | | |
Example
validateOnBlur
Property | Type | Default | Description |
---|
| | | |
Example
validateOnSubmit
Property | Type | Default | Description |
---|
| | | |
Example
validateInvalid
Property | Type | Default | Description |
---|
| | | |
Example
schema
Property | Type | Default | Description |
---|
| | | |
Example
customRegister
Property | Type | Default | Description |
---|
| | | |
Example
DirectFormContext
data
Property | Type | Default | Description |
---|
| | | |
Example
validate
Property | Type | Default | Description |
---|
| | | |
Example
getError
Property | Type | Default | Description |
---|
| | | |
Example
errors
Property | Type | Default | Description |
---|
| | | |
Example
getValue
Property | Type | Default | Description |
---|
| | | |
Example
getList
Property | Type | Default | Description |
---|
| | | |
Example
setValue
Property | Type | Default | Description |
---|
| | | |
Example
submit
Property | Type | Default | Description |
---|
| | | |
Example
registerForm
Property | Type | Default | Description |
---|
| | | |
Example
register
Property | Type | Default | Description |
---|
| | | |
Example
DirectFormSettings
customRegister
Property | Type | Default | Description |
---|
customRegister | CustomRegisterFnc | - | Can be used to globally overwrite the behaviour of the register method |
Example
import React, { useState } from 'react';
import { DirectForm, DirectFormSettings } from 'react-directform';
import { object, string } from 'yup';
import { TextField } from '@material-ui/core';
function App() {
const [value1, setValue1] = useState();
const [value2, setValue2] = useState();
// Custom register method to add the validation errors to material ui TextFields
function register(ctx: DirectFormContextData<any>, org: DirectFormContextData<any>['register']) {
return (path: string, value?: 'value' | 'checked') => ({
...org(path, value),
error: !!ctx.getError(path),
helperText: ctx.getError(path),
});
}
return (
<DirectFormSettings customRegister={register}>
<DirectForm value={value1} onSubmit={setValue1}>
...
</DirectForm>
<DirectForm value={value2} onSubmit={setValue2}>
...
</DirectForm>
</DirectFormSettings>
);
}
Hooks
useDirectForm
The useDirectForm
hook can be used to access the DirectForm context which contains all information usually provided by the render function.
Example
...
function PersonForm() {
const { register, getError } = useDirectForm();
return (
<>
<input {...register('firstname')} />
{getError('firstname')}
<input {...register('lastname')} />
{getError('lastname')}
</>
);
}
function App() {
const [value, setValue] = useState();
return (
<DirectForm schema={schema} value={value} onSubmit={setValue}>
<PersonForm />
</DirectForm>
);
}
...