This package is a very lightweight implementation of a form handler that works with React input based on the Composable Form Specification. ReactoForm works best with inputs that fully implement this specification, but it can be adjusted to work with most React form input components that are at least similar to the specification.
This package exports the following things that help you quickly combine, validate, and submit form data collected by React input components:
useReactoForm
React hook (preferred)Form
React component (in case you're stuck with a class component)
Additionally, it exports a FormList
React component that is an example of building a dynamic array from form inputs.
Another package, reacto-form-inputs, provides examples of various types of inputs that conform to the spec. In general they are robust, tested, and production ready, but you may want to copy and modify them to style them to your needs. Alternatively, ReactoForm can be made to work with many popular React UI frameworks, and this is most likely what you want to do with this package.
npm i reacto-form
Import CommonJS from reacto-form/cjs/<ComponentName>
. Example, assuming you have Babel configured to convert all import
to require
:
import Form from "reacto-form/cjs/Form";
import FormList from "reacto-form/cjs/FormList";
import useReactoForm from "reacto-form/cjs/useReactoForm";
Import ECMAScript module from reacto-form/esm/<ComponentName>
. Example:
import Form from "reacto-form/esm/Form";
import FormList from "reacto-form/esm/FormList";
import useReactoForm from "reacto-form/esm/useReactoForm";
You can also use named imports from the package entry point, but this may result in a larger bundle size versus importing directly from the component path.
import { Form, FormList, useReactoForm } from "reacto-form";
See https://github.com/longshotlabs/reacto-form-inputs#example
cd demo-app
npm start
Available since v1.2.0
The newest and best way to use ReactoForm is with the aptly named useReactoForm
React hook. Unless your form is in a class component, where React hooks don't work, you should always use this hook. For class components, use the Form
component described below.
In a nutshell, you call the hook in your component function, passing options, and then use the returned functions to inject the proper form logic into all of your input components as standard props.
Here's the simplest possible example, using SimpleSchema to create the validator function. You could choose to write your own validation function or use any validation package you like, with a small wrapper to adjust the errors structure if necessary.
import React from "react";
import Button from "@material-ui/core/Button";
import { ErrorsBlock, Field, Input } from "reacto-form-inputs";
import useReactoForm from "reacto-form/esm/useReactoForm";
import SimpleSchema from "simpl-schema";
const formSchema = new SimpleSchema({
firstName: {
type: String,
min: 4,
},
lastName: {
type: String,
min: 2,
},
});
const validator = formSchema.getFormValidator();
export default function ReactoFormHookExample() {
// Here we call the hook function. None of the options are required, but in general
// you would always want a `validator` function and an `onSubmit` function.
const { getErrors, getInputProps, submitForm } = useReactoForm({
onChange: (formData) => {
console.log("onChangeForm", formData);
},
onChanging: (formData) => {
console.log("onChangingForm", formData);
},
onSubmit: (formData) => {
console.log("onSubmitForm", formData);
},
validator,
// value - optionally pass an object representing the current form data, if it's an update form or has default values
});
return (
/* Note that we need not wrap our fields in <form>, or really in anything */
<div>
/* We can use `getErrors` to get all of the errors related to one or more
fields, based on the field path */
<Field
name="firstName"
errors={getErrors(["firstName"])}
label="First name"
>
/* We can use `getInputProps` to get all props for a single field path
*/
<Input {...getInputProps("firstName")} />
<ErrorsBlock errors={getErrors(["firstName"])} />
</Field>
<Field name="lastName" errors={getErrors(["lastName"])} label="Last name">
<Input {...getInputProps("lastName")} />
<ErrorsBlock errors={getErrors(["lastName"])} />
</Field>
/* The submit action must call the `submitForm` function that `useReactoForm`
returned */
<Button onClick={submitForm}>Submit</Button>
</div>
);
}
Here's a full list of what you can pass to useReactoForm
:
hasBeenValidated
: Pass a boolean to override the internal tracking of whether thevalidator
function has been called since the form was created or reset.isReadOnly
. Pass a boolean or a function that accepts the current form data object as its only argument and returns a boolean. Iftrue
, all inputs controlled by the form will be in read-only mode (disabled). ReactoForm also automatically makes all of the inputs read only while the form is being submitted.logErrorsOnSubmit
: Passtrue
to log all errors in the console whensubmitForm
is called, if there are any errors. This can be helpful during initial development and when debugging in case you have forgotten to show any errors in the UI.onChange
: This function will be called with the new form data object whenever any input changesonChanging
: This function will be called with the new form data object whenever any input is in the process of changing (for example, while a slider is moving but not yet released, while a finger is moving but not yet lifted, while a user is typing but hasn't yet tabbed to the next field).onSubmit
: This function will be called with the form data object when you callsubmitForm
, if the form is valid orshouldSubmitWhenInvalid
istrue
.revalidateOn
: Set this to "changing", "changed", or "submit". The default is "changing". This determines how oftenvalidator
will be called (thus reactively updatingerrors
) whenhasBeenValidated
istrue
. WhenhasBeenValidated
isfalse
, then thevalidateOn
setting is used.- Note that these are additive; "changing" causes validation before
onChanging
is called, beforeonChange
is called, AND beforeonSubmit
is called; "changed" causes validation beforeonChange
is called AND beforeonSubmit
is called; "submit" causes validation only beforeonSubmit
is called. - If you don't need validation, simply don't pass a
validator
function.
- Note that these are additive; "changing" causes validation before
shouldSubmitWhenInvalid
: NormallyonSubmit
will not be called ifvalidator
returns any errors. To override this and callonSubmit
anyway, set this option totrue
. The second argument passed toonSubmit
will be anisValid
boolean.validateOn
: Set this to "changing", "changed", or "submit". The default is "submit". This determines how oftenvalidator
will be called (thus reactively updatingerrors
) whenhasBeenValidated
isfalse
. WhenhasBeenValidated
istrue
, then therevalidateOn
setting is used.- Note that these are additive; "changing" causes validation before
onChanging
is called, beforeonChange
is called, AND beforeonSubmit
is called; "changed" causes validation beforeonChange
is called AND beforeonSubmit
is called; "submit" causes validation only beforeonSubmit
is called. - If you don't need validation, simply don't pass a
validator
function.
- Note that these are additive; "changing" causes validation before
validator
: This is the validation function. Use any validation library you want as long as you return an errors array with this structure, or a Promise that resolves with such an array.value
: The current form data. Pass this for an update form or to provide default values for some of the inputs.
Here's a full list of what you can get from the object returned by useReactoForm
:
getInputProps
: A function that returns a props object that conforms to the Composable Form Input Specification. Pass a unique field path string as the first argument. For example,getInputProps("email")
will return input props that result in the form data object{ email: "" }
whilegetInputProps("address.city")
will return input props that result in the form data object{ address: { city: "" } }
. If you are using a compliant input component, simply pass the returned props to that input and everything will be wired up for you. If you are using a non-compliant input component, you may still be able to make it work. See the Material UI example below.formData
: The current form data object. This initially matches thevalue
you provide but changes as the user fills out the form. If you callresetValue
, this will once again match thevalue
you provide.getErrors
: A function that returns an errors array like this: https://composableforms.netlify.app/spec/errors/#errors. The signature is(fieldPaths, { includeDescendantErrors = false } = {})
.fieldPaths
is an array of object paths.includeDescendantErrors
would for example include an error for"address.city"
whenfieldPaths
is["address"]
.getFirstError
: A function similar togetErrors
but returns only the first error matching any field path, ornull
if there are none. The signature is(fieldPaths, { includeDescendantErrors = false } = {})
.getFirstErrorMessage
: A function similar togetFirstError
but returns only the first error message string matching any field path, ornull
if there are none. The signature is(fieldPaths, { includeDescendantErrors = false } = {})
.hasBeenValidated
: Boolean indicating whethervalidator
has been called since the form was created or sinceresetValue
was last called.hasErrors
: A function similar togetErrors
but returns onlytrue
if there are any errors orfalse
if not. The signature is(fieldPaths, { includeDescendantErrors = false } = {})
.isDirty
: This will betrue
if the form data state has changed from the initial formvalue
(i.e. if the user has changed any inputs).resetValue
: Call this function to resetformData
tovalue
, thus causingisDirty
to befalse
.submitForm
: Call this function to validate and submit all inputs (i.e., to callvalidator
followed byonSubmit
).
Material UI is a great framework, but unfortunately the React input components do not currently match the Composable Form Input Specification in several ways. For example, the TextField has the following differences:
- It complains when you pass
null
asvalue
, and it considers the input to be "uncontrolled" when you passundefined
asvalue
. Instead, it expects an empty string. onChange
is called while changing,onBlur
is called after the change, andonChanging
is never called and causes a console warning.isReadOnly
prop is nameddisabled
Fortunately, the useReactoForm
getInputProps
function takes some options which allow us to change the names of the returned props, omit returned props, and convert null
value to some other value:
getInputProps("email", {
nullValue: "",
onChangeGetValue: (event) => event.target.value,
onChangingGetValue: (event) => event.target.value,
propNames: {
errors: false,
hasBeenValidated: false,
isReadOnly: "disabled",
onChange: "onBlur",
onChanging: "onChange",
onSubmit: false,
},
});
To simplify this further, this package exports these options as muiOptions
:
import muiOptions from "reacto-form/esm/muiOptions";
getInputProps("email", muiOptions);
Similarly, you can import muiCheckboxOptions
for an MUI Checkbox
component:
import muiOptions from "reacto-form/esm/muiCheckboxOptions";
getInputProps("isMarried", muiCheckboxOptions);
Here's a full example:
import React from "react";
import Button from "@material-ui/core/Button";
import Checkbox from "@material-ui/core/Checkbox";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormGroup from "@material-ui/core/FormGroup";
import TextField from "@material-ui/core/TextField";
import muiCheckboxOptions from "reacto-form/esm/muiCheckboxOptions";
import muiOptions from "reacto-form/esm/muiOptions";
import useReactoForm from "reacto-form/esm/useReactoForm";
import SimpleSchema from "simpl-schema";
const formSchema = new SimpleSchema({
firstName: {
type: String,
min: 4,
},
lastName: {
type: String,
min: 2,
},
isMarried: {
type: Boolean,
optional: true,
},
});
const onSubmit = (formData) => {
console.log("onSubmitForm", formData);
};
const validator = formSchema.getFormValidator();
export default function ReactoFormHookExampleMUI() {
const {
getFirstErrorMessage,
getInputProps,
hasErrors,
submitForm,
} = useReactoForm({
onSubmit,
validator,
});
return (
<div>
<TextField
label="First name"
error={hasErrors(["firstName"])}
fullWidth
helperText={getFirstErrorMessage(["firstName"])}
{...getInputProps("firstName", muiOptions)}
/>
<TextField
label="Last name"
error={hasErrors(["lastName"])}
fullWidth
helperText={getFirstErrorMessage(["lastName"])}
{...getInputProps("lastName", muiOptions)}
/>
<FormGroup row>
<FormControlLabel
control={<Checkbox color="primary" />}
label="Are you married?"
{...getInputProps("isMarried", muiCheckboxOptions)}
/>
</FormGroup>
<Button onClick={submitForm}>Submit</Button>
</div>
);
}
Implements the Form spec.
In addition to following the spec, these props are supported:
- Use
style
orclassName
props to help style the HTML form container, which is a DIV rather than a FORM. - Set
logErrorsOnSubmit
totrue
to log validation errors to the console when submitting. This can help you figure out why your form isn't submitting if, for example, you forgot to include an ErrorsBlock somewhere so there is an error not shown to the user.
Works in 1.3.0+
Material UI is a great framework, but unfortunately the React input components do not currently match the Composable Form Input Specification in several ways. For example, the TextField has the following differences:
- It complains when you pass
null
asvalue
, and it considers the input to be "uncontrolled" when you passundefined
asvalue
. Instead, it expects an empty string. onChange
is called while changing,onBlur
is called after the change, andonChanging
is never called and causes a console warning.isReadOnly
prop is namedreadOnly
Fortunately, the Form
component takes some options in the inputOptions
props which allow us to change the names of the returned props, omit returned props, and convert null
value to some other value:
const inputOptions = {
nullValue: "",
propNames: {
errors: false,
hasBeenValidated: false,
isReadOnly: "readOnly",
onChange: "onBlur",
onChanging: "onChange",
onSubmit: false,
},
};
<Form inputOptions={inputOptions}>/* MUI inputs */</Form>;
To simplify this further, this package exports these options as muiOptions
:
import muiOptions from "reacto-form/esm/muiOptions";
<Form inputOptions={muiOptions}>/* MUI inputs */</Form>;
Here's a full example:
import React, { useRef } from "react";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Form from "reacto-form/esm/Form";
import muiOptions from "reacto-form/esm/muiOptions";
import SimpleSchema from "simpl-schema";
const formSchema = new SimpleSchema({
firstName: {
type: String,
min: 4,
},
lastName: {
type: String,
min: 2,
},
});
const onSubmit = (formData) => {
console.log("onSubmitForm", formData);
};
const validator = formSchema.getFormValidator();
export default function ReactoFormExampleMUI() {
const formRef = useRef(null);
return (
<div>
<Form
inputOptions={muiOptions}
onSubmit={onSubmit}
ref={formRef}
validator={validator}
>
<TextField
error={formRef.current && formRef.current.hasErrors(["firstName"])}
fullWidth
helperText={
formRef.current &&
formRef.current.getFirstErrorMessage(["firstName"])
}
label="First name"
name="firstName"
/>
<TextField
error={formRef.current && formRef.current.hasErrors(["lastName"])}
fullWidth
helperText={
formRef.current &&
formRef.current.getFirstErrorMessage(["lastName"])
}
label="Last name"
name="lastName"
/>
<Button onClick={() => formRef.current && formRef.current.submit()}>
Submit
</Button>
</Form>
</div>
);
}
Implements the FormList spec.
This implementation appears as a list with the item template on the right and remove buttons on the left, plus a final row with an add button in it.
In addition to following the spec, you can use the following props to help style the component:
- addButtonText: String to use as the text of the add button. Default "+"
- addItemRowStyle: Style object for the row after the last item, where the add button is
- buttonClassName: String of space-delimited classes to use on the add and remove buttons
- buttonStyle: Style object for the add and remove buttons
- className: String of space-delimited classes to use on the list container
- itemAreaClassName: String of space-delimited classes to use on the inner container of each item
- itemAreaStyle: Style object for the inner container of each item
- itemClassName: String of space-delimited classes to use on the outer container of each item
- itemStyle: Style object for the outer container of each item
- itemRemoveAreaClassName: String of space-delimited classes to use on the remove button area of each item
- itemRemoveAreaStyle: Style object for the remove button area of each item
- removeButtonText: String to use as the text of the remove buttons. Default "–"
- style: Style object for the list container
If you want a different add/remove experience that can't be achieved with classes or styles, then you'll need to make your own implementation of FormList.