- CSS modules
- React 17+
This package distributes JSX so you can decide how it gets compiled.
https://JakobJingleheimer.github.io/form5
Use what you want. No component requires any other component (ex <Form>
works just fine with plain <input>
, <select>
, etc). You can also use the utilities that underpin <Form>
without the react component itself (you're free to re-invent the wheel), just import them and go:
import composeData from 'form5/composeData';
import deepDiff from 'form5/deepDiff';
A good practice in data management is to keep it minimal. When changing data, it's generally best to track only what changed (especially important for large data models). Out of the box, Form5 provides a deep diff to <Form>
's onSubmit handler.
Form5 supports a 1:1 DOM to data model mapping: To create nested data objects, use a plain old <fieldset>
; when a name
prop is supplied, the data object provided to onSubmit
will nest fields inside under that name. See examples below.
This package leverages as many native APIs as possible: HTML5's Constraints Validation, CSS variables, latest media queries and properties, etc. Anything that doesn't is as closely aligned and friendly as possible to any existing native APIs (with a couple small but personally frustrating exceptions—the lesser of two evils).
Performance was one of the main reasons I created this; I was gobsmacked by the horrendous performance of leading libs *cough* formik *cough*; that may not be the case anymore (I haven't checked). There are no redundant/superfluous re-renders etc from this package and no divitis.
This package encourages/supports (and enforces a bit) accessibility. It leverages the native Constraints Validation API, and very rudimentary structural styling via CSS Grid Layout.
State can be controlled or uncontrolled without impacting the validation this package facilitates.
General layout structure and basic validation state appearance is handled by this package. There are some css variables that are consumed if available:
CSS variables
css var | purpose |
---|---|
--colour-background |
field background colour |
--colour-danger |
borders and text colours when field has invalid input |
--colour-light |
icon-based button background colour |
--colour-medium |
default border colours, |
--colour-muted |
disabled & readonly field's label |
--colour-primary |
submit button |
--colour-success |
success button colour |
--colour-text-deemphasised |
label text and border colour of disabled fields |
--colour-warning |
background colour of a "warning" button |
--default-border-radius |
field corner rounding |
--default-boxshadow |
drop shadow on buttons and fields |
--default-padding |
inner spacing of fields and minimum inner spacing for buttons |
--default-transition-duration |
value used for transition-duration on various Fields |
--default-transition-easing |
value used for transition-timing-function on various Fields |
--grid-gutter |
space between css grid layout cells (labels + fields, fields + fields); value used for gap |
Labels of fields set to required are automatically styled with an "*" in an accessibility-friendly way.
Implementer's css modules are consumed via css modules (passed as normal via className
).
Produce a key-value map of field name
s (or id
s) to their value
(or equivalent, like an <input checked>
). If you want to roll your own instead of having form5's <Form>
do it for you, it would look something like this:
function onSubmit(event) {
const formControlsList = Array.from(event.target.elements);
const data = {};
formControlsList.reduce(composeData, data);
}
<form onSubmit={onSubmit}>
{/* … */}
</form>
This utility has a couple noteworthy extras related to <fieldset>
:
- Named
<fieldset>
s to create nested key-value maps (see<fieldset>
below); anonymous<fieldset>
s are themselves ignored in output (but their descendants are present, as normal). - Disabled
<fieldset>
s output their descendant's value asnull
, unless the<fieldset>
is alsoreadonly
¹.
¹ A named <fieldset>
with no items is excluded from composeData()
's output (there is no empty
object).
Produce a delta of A & B, maintaining the shape of the inputs. In form5, this is used in combination with composeData()
: On mount, <Form>
runs composeData()
on all its fields to create the initial state ("A"), and then on submit, runs composeData()
again to get its fields' current values ("B"), feeding A & B into deepDiff (deepDiff(A, B)
), and finally resets A to B (for
a potential subsequent submission).
const initial = {
country: 'Canada',
forename: 'Jakob',
};
const current = {
country: 'Holland',
forename: 'Jakob',
};
deepDiff(initial, current); // { country: 'Holland' }
If a field is supplied validation props (minLength
, required
, etc), the component will automatically trigger its fields ("form controls") to self-validate and display relevant errors upon submit. Each field supports supplying custom validation messages via the Constraints Validation API—when no custom message(s) are supplied, browser defaults (which are pretty good and support i18n for free) are used. A Form submitted with no changes or invalid fields will abort the submission (the supplied onSubmit
callback will not be called).
Any props not specific to this package are passed to the relative element.
Code example
import {
Button,
FileInput,
Form,
Input,
} from 'form5/react';
import AddressService from '../AddressService.js';
import ClientsService from '../ClientsService.js';
import yourCustomStyles from './something.module.css';
const validateAddress = (event) => {
const { target } = event;
const isInvalid = AddressService.validate(target.value);
let msg = 'Address is not recognised';
if (isInvalid) {
if (typeof msg === 'boolean') msg = isInvalid;
target.setCustomValidity(msg);
target.reportValidity(); // required because reasons
}
};
const validateDomain = (event) => {
const { target } = event;
const isInvalid = ClientsService.validate(target.value);
let msg = 'Email address is not allowed';
if (isInvalid) {
if (typeof msg === 'boolean') msg = isInvalid;
target.setCustomValidity(msg);
target.reportValidity(); // required because reasons
}
};
const sendToCloud = (
delta,
{
forename,
surname,
contactDetails: {
email,
tel,
},
newsletterOptIn,
},
event,
) => {
return fetch(…);
};
const SFC = (props) => (
<Form
className={yourCustomStyles.SignupForm}
name="signup"
onDirty={setIsDirty}
onPristine={setIsDirty}
onSubmit={sendToCloud}
>
<Field
name="forename"
minLength={2}
placeholder="Jane"
required
type="text"
/>
<Field
name="surname"
minLength={2}
placeholder="Smith"
required
type="text"
/>
<fieldset name="contactDetails">
<Field
name="email"
onBlur={validateDomain}
placeholder="jane@example.com"
type="email"
/>
<Field
as="textarea"
name="mailingAddress"
onChange={validateAddress}
/>
<Field
name="tel"
placeholder="5555555555"
type="tel"
/>
</fieldset>
<Field
name="newsletterOptIn"
type="checkbox"
variant={Input.VARIANTS.TOGGLE}
/>
<Button
appearance={Button.APPEARANCES.BASIC}
type="submit"
/>
</Form>
);
This component facilitates validation and provides a data model object for uncontrolled fields. Fields wrapped in a name fieldset
result in a nested object.
It also provides a delta
of data changes, useful for PATCH
ing existing field(s) changed by the user: the delta object contains only properties differing from their initial value. Ex an existing user changes their forename: delta = { forename: 'Jakob' }
; if a Form is submitted multiple times, note that its delta changes—the delta is the difference from the last "clean" state until "now" (so it will reflect changes since the last submit).
<Form>
accepts a number of event handlers (the usual onChange
, onSubmit
, etc), as well as some other useful extras:
These optional handlers are called with true
and false
respectively as they're typically used to track isDirty
(so onDirty={setIsDirty}
and onPristine={setIsDirty}
).
<Form>
tracks pristine
and touched
regardless of whether handlers are supplied because it uses them internally (such as to abort a submission when pristine
). Each handler is called only once per change (eg when the form becomes dirty and then when it becomes pristine
again after submit). These can be leveraged to, for instance, warn the user they are about to abandon the current changes.
Form5 itself has no component for <fieldset>
, but it does have special support for the native
<fieldset>
when composing data. Natively, readonly
has no effect on a <fieldset>
(or its
descendants), to the surprise and upset of many. In form5, setting <fieldset readonly>
will result in
composeData()
ignoring its descedants in output (as if the descendants themselves were readonly
). However, in order to get native html to behave
like the descendant fields are readonly
, you must also set disabled
on the fielset: <fieldset disabled readonly>
. composeData()
ordinarily outputs descendants' value as null
when <fieldset disabled>
, but when it is also readonly
, it instead ignores them.
In order to create nested data, a named fieldset can be used:
Code example
<fieldset name="something">
<input name="a" />
<input name="b" />
<fieldset name="else">
<input name="c" />
<input name="d" />
</fieldset>
<fieldset><!-- anonymous -->
<input name="e" />
<input name="f" />
</fieldset>
</fieldset>
Results in composeData()
outputting:
{
something: {
a: '…',
b: '…',
else: {
c: '…',
d: '…',
}
e: '…',
f: '…',
}
}
The most important difference here is the type
prop defaults to button
instead of submit
because forgetting to override "submit" has confusing, adverse behaviour.
A file picker for uploading from disk. The main difference between this component and this package's <Field>
component is that it generates a preview for image-type files or lists filenames of selected files (when a preview cannot be generated).
This component is a thin wrapper for native label
and form fields (input
, select
, textarea
) that helps to ensure accessibility and facilitate validation. It leverages onBlur
and onChange
listeners to track pristine
and touched
(to avoid erroneously invalidating the field); its internal handlers are called first, and then handlers optionally supplied via props.
To create a list of values within a single property, append []
to the name
attribute, like <Field name="foo[]" />
; to avoid conflicting with the likes of react, a number may optionally be included within the brackets, like <Field name="foo[0]" />
, but the number (index) itself will be ignored (sequence in data objects is determined by the input's place in the DOM).
When you have existing data with which to pre-populate form fields that users will subsequently change, you can still have an uncontrolled component (meaning you don't need to manage onChange
etc) by using React's defaultChecked={valueFromDb}
or defaultValue={valueFromDb}
(which also works with a native <input>
). Note that unlike native HTML checkboxes and radio "buttons", form5 enforces the expected behaviour of readOnly
on these fields (natively, readOnly
only affects setting value
, and checkboxes and radios change checked
—seemingly a distinction without a difference). So <Field checked readOnly type="checkbox" />
cannot be unchecked.
<Field>
also offers a small easter-egg: a toggle, which is effectively a checkbox with fancy styling.