form4react
Abstract form handling for any purpose (< 2 kb gzipped)
Alternatives...
The following statements represent only my personal opinion, although I did
not work a lot with the following pretty good packages.
-
redux-form:
A lot of features, nice documented and easy to use
form handling tool, but it comes with the cost of a little integrative work,
the cost of big package size, forces you to hold the form data in the redux store
and to connect every form and has poor flow
assistance.
-
react-final-form:
A lot of features, optimized for performance,
small bundle size, totally customizable and needs no integrative work,
but is not as well documented and has no flow
assistance.
Why form4react?
- Optimized
flow
assistance - No dependencies
- Small bundle size
- High performance
- Nice features (but not as much as the above packages)
- Totally customizable
- Well documented (WIP)
Installation
With yarn
yarn add form4react
With npm
npm install --save form4react
Introduction
First take a look at the outer form element.
import { Form } from 'form4react';
const render = () => (
<Form validation={validation}
onChange={onChange}
data={data}
onSubmit={onSubmit}>
{/* form content */}
</Form>
);
It expects the following props: (See below for the missing Flow types)
Props | Type | Description | Example |
---|
className | string | Will be applied to the form tag | 'my-form' |
validation | FormValidation<V> | Contains all validators | { name: { onChange: customValidator } } |
data | FormData<V> | Contains all values, errors and submitting state | { values: { name: 'Scotty' }, errors: { name: { id: 'does.not.know' } }, submitting: false } |
onChange | (FormData<V>) => void | Handles the next data after any changes have been made | |
onSubmit | V => void \ Promise<void> | Will be called if submitted without any failing validators | |
onSubmitFailed | void => void | Will be called if submitting was aborted due to validation errors | |
onSubmitFinished | void => void | Will be called if after submitting finished | |
Now lets take a look on a single integrated form element:
import { Form, Field } from 'form4react';
const render = () => (
<Form validation={validation}
onChange={onChange}
data={data}
onSubmit={onSubmit}>
<Field name="phone" >
{({ onChange, onBlur, required }) => (
<div>
<label>{'Phone' + required ? '*' : ''}</label>
<input value={data.values.phone} onChange={onChange} onBlur={onBlur} />
<span className="error">{__(data.errors.phone)}</span>
</div>
)}
</Field>
</Form>
);
By using the Field
which excepts only one property name
of type string,
the rendered children get access to the three attributes:
-
onChange
: Receives the next value, runs the onChange-Validator on it
and calls the Form
- onChange
where the data.values.phone
holds the
updated value and data.errors.phone
will maybe hold an occurred validation error.
-
onBlur
: Same as the onChange
but will be triggered if the input loses
the users focus.
-
required
: A boolean which indicates if the value undefined
for "phone"
would pass all validations on that field. If not the value is treated as
required.
The above example shows a Form
with a single Field
.
The label shows "Phone*" if the value is required.
It will update the form data accordingly to the specified onChange
handler
supplied to the Form.
It will display an error message below the input if any error is encountered.
Asynchronous form field handling
To implement async form field validation, you simply have to return a promise, which
will resolve the validation result. You can write validators which are both:
async AND sync validator at the same time. One example:
const specialValidator = (val: any) => {
if (!val) {
return {id: 'value.required'};
}
if (typeof val !== 'number') {
return myFetch('/my/api').then(result => {
if (result.isInvalid) {
return {id: 'value.invalid'};
}
})
}
}
HINT: You should always validate the value undefined
synchronously, because
form4react
determines the requirements by these criteria. So if you use the preferable
async-await
-Syntax, you should split your validator into to parts.
Control your submit
When the user submits the form, the following will happen:
- The
submitting
property will be propagated as true
to your onChange
- All registered validators will be executed and their validation results will be awaited
- When
2
returned any errors your onChange
will be called with the found errors and submitting
as false
(Early exit) - When
2
returned no errors your onSubmit
will be called - When
4
took place, the submitting
property will be propagated as false
(if your onSubmit
returned a Promise, this will happen after this Promise resolved)
Flow types
form4react comes with a lot of types and exports FormData<T>
, ErrorMessage
and Validator
.
In this table you get an overview of relevant types.
Name | Flow declaration | Information |
---|
ErrorMessage | { id: string, values?: {[string]: mixed}} | This structure allows to handle internationalization by transporting the required information like the intl key and placeholder values |
FormData<V> | { values: V, errors: { [$Keys<V>]: void \ ErrorMessage } } | This is the main structure for the data represent the form state |
Validator | any => ErrorMessage \ void \ Promise<ErrorMessage \ void> | The validator returns void if no error occurred or a Promise if the validation is asynchronous |
ValidationType | 'onChange' \ 'onBlur' \ 'onSubmit' | Currently only those to validation types can be specified |
FormValidation<V> | { [$Keys<V>]: { [ValidationType]: Validator \ void } } | For each key of the specified form values V here can be specified all validations for this field |
Example
In this chapter we will examine how a sample form could be implemented.
The sample is hosted be GitHub Pages.
We first define the types of the form values...
type MyFormValues = {
gender: string,
firstName: string,
lastName?: string,
age: number,
};
const genderOptions = [{ value: 'M', label: 'male' }, { value: 'F', label: 'female' }];
Then we define the validations... (see here for the used validators below)
const validation = {
firstName: { onChange: Validators.string({ min: 1, max: 10 }) },
lastName: { onBlur: Validators.string({ min: 1, max: 10 }) },
age: { onChange: Validators.number({ minLength: 1, maxLength: 3 }) },
};
Let's say we decide to control the form state inside our app state...
type AppState = { data: FormData<MyFormValues> };
Now we define the first version of the app... (see here for all used custom components below)
import React, { Component } from 'react';
import { Form } from 'form4react';
import type { FormData } from 'form4react';
class App extends Component<{}, AppState> {
state: AppState = { data: { values: { gender: 'M', firstName: 'Nick', age: 21 }, errors: {} } };
onChange = (data: FormData<MyFormValues>) => this.setState({ data });
onSubmit = ({ gender, firstName, lastName, age }: MyFormValues): void => {
};
render(): React$Node {
const data = this.state.data;
const { values, errors } = data;
return (
<Form validation={validation} onChange={this.onChange} data={data} onSubmit={this.onSubmit}>
<div className="row">
<FormInput
name="firstName"
value={values.firstName}
error={errors.firstName}
label="First name"
className="form-group col-sm-6"
/>
<FormInput
name="lastName"
value={values.lastName}
error={errors.lastName}
label="Last name"
className="form-group col-sm-6"
/>
</div>
<div className="row">
<FormSelect
name="gender"
value={values.gender}
error={errors.gender}
options={genderOptions}
label="Gender"
className="form-group col-sm-6"
/>
<FormNumberInput
name="age"
value={values.age}
error={errors.age}
label="Age"
className="form-group col-sm-6"
/>
</div>
<button className="btn btn-primary">Submit</button>
</Form>
);
}
}
This version does the specified validations on the fields and prevents our empty
onSubmit
function from being called, when any validations would not succeed
So now we add a table (see here
for the implementation and the new type Person
) to hold all entered person data. What changes...
type AppState = {
data: FormData<MyFormValues>,
persons: Person[],
};
state: AppState = { data: { values: { gender: 'M', firstName: 'Nick', age: 21 }, errors: {} }, persons: [] };
<PersonTable persons={this.state.persons} />
onSubmit = ({ gender, firstName, lastName = '', age }: MyFormValues): void => {
this.setState({
persons: [...this.state.persons, { gender: gender === 'M' ? 'M' : 'F', firstName, lastName, age }],
});
};
Now each time we submit the entered data which succeeds all validations, a new
person entry will be displayed in the PersonTable
.
Finally we will add a clear button for all inputs. What changes...
onClear = () => {
this.setState({ data: { values: { gender: 'M', firstName: '', age: 0 }, errors: {} } });
};
<div className="btn-toolbar" role="toolbar">
<button className="btn btn-secondary" type="button" onClick={this.onClear}>
Clear
</button>
<button className="btn btn-primary" disabled={Form.hasErrors(data)}>
Submit
</button>
</div>
Now if we hit the clear button all entered values will be cleared.
FAQ
-
What about asynchronous form error handling?
Since you are the owner and controller of any updates on the form data,
you can simply add new errors for any fields after the server responded
with any error. If you want to write a util which maps specific server
errors to specific fields, you have the ability to do so.
-
Why are there no ready-to-use components exported like FormSelect
from the above examples?
form4react is primarily made to handle updates, validations, storing of
data, save the developer by good flow assistance and as a guideline for form handling
Later there might be subdirectories which you can optionally use, but most
often in larger projects you want to have full control over all form components.