- 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 much 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 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>
).
function onSubmit(event) {
const formControlsList = Array.from(event.target.elements);
const data = {};
formControlsList.reduce(composeData, data);
}
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 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.
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"
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' }
.
<Form>
accepts a number of event handlers (the usual onChange
, onSubmit
, etc), as well as onDirty
and onPristine
, which are called when the form's pristine
state changes; each is called only once per change (eg when the form becomes dirty and when it becomes pristine
again after submit). These can be leveraged to, for instance, prevent submitting a pristine form (as you can partially see in the demo: the submit button is disabled, but the form itself doesn't consume isDirty
so data objects can still be observed).
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:
<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.
Button supports the following configuration via props:
prop | purpose | default |
---|---|---|
appearance | alter the aesthetics of the element | primary |
fluid | whether the button should act like a liquid: fill its container | false |
variant | the kind of appearance the button takes on (classic CTA, icon, etc) | cta |
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 first called and then handlers passed 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}
(also works with native <input>
).
<Field>
also offers a small easter-egg: a toggle, which is effectively a checkbox with fancy styling.
prop | purpose | default |
---|---|---|
arrangement | the fields position relative to its field | inline |
as | the element or react component to use | input |
variant | display a checkbox as a toggle | null |