Pathform was built to scratch an itch for recursive, nested, dynamic forms. Using paths as an array, we can spread nested fields around like butter.
We can derive a lot from the path... Why is this useful?
npm install --save react-pathform
# or
yarn add react-pathform
import React from 'react';
import { PathFormProvider, PathForm, PathFormField } from 'react-pathform';
import { Button, TextField } from '@material-ui/core';
function App() {
return (
<PathFormProvider
initialRenderValues={{
nested: {
items: [{ name: "Joey Joe Joe Jr." }]
}
}}
>
<PathForm onSubmit={(values) => alert(JSON.stringify(values, null, 2))}>
<PathFormField
path={["nested", "items", 0, "name"]}
defaultValue=""
render={({ inputProps, meta }) => {
return (
<TextField
label="Name"
error={!!meta.error}
helperText={meta.error?.message}
{...inputProps}
/>
);
}}
/>
<Button type="submit">Submit</Button>
</PathForm>
</PathFormProvider>
);
}
Check out the Example App
-
Components
-
Hooks
The store context provider for your form. You must place it at the root of your form to be able to use usePathForm
, and PathForm*
components.
The data in the store will remain until the PathFormProvider
is unmounted.
The initial data for your form on the initial render only.
Set the mode in which the validation will happen.
- onSubmit (default): will run the validation when the form is submitted.
- onChange: Validation will trigger on the change event with each input, this may trigger multiple rerenders. The PathForm.onValidate will continue to be triggered on the submit event.
Renders a browser native form
element and allows you to hook into onValidate
and onSubmit
.
Called just before submitting. Throw an error to stop onSubmit
from triggering.
Called on successful submission after validation.
Binds to a value at the given path in the store from.
The path selector to the item in your store.
The value use on the initial render, if the store item does not already exist.
The callback to render the field.
Register validations for this field.
<PathFormField
path={['person', 0, 'name']}
defaultValue=""
validations={[
{ type: 'required', message: 'This person must have a name.' },
{ type: 'maxLength', value: 16, message: 'This must be less than 16 characters.' },
{ type: 'custom', value: (value) => value.trim().toLowerCase() !== 'joe', message: 'This person can\'t be Joe.' },
]}
render={({ inputProps, meta }) => {
return (
<TextField
label="Name"
error={!!meta.error}
helperText={meta.error?.message}
{...inputProps}
/>
);
}}
/>
Binds to an array at the given path in the store from. The render
callback will be called for
each item in the array.
Use the meta.uuid
on your root item key
.
<PathFormArray
path={['path', 'to', 'array']}
defaultValue={[]}
renderItem={({ arrayPath, itemPath, index, totalRows, meta }) => (
<div key={meta.uuid}>
<NameField key={meta.uuid} path={[...itemPath, 'name']} />
<button onClick={() => array.remove(arrayPath, index)} disabled={totalRows <= 1}>Delete</button>
</div>
)}
renderEmpty={() => <>No Items!</>}
/>
Provides an outlet to render all errors in the form in one place.
The errors
given back are flattened store items,
which wraps each store item with path
, dotpath
, and storeItem
, which
you could use to pick or omit certain fields from rendering.
<PathFormErrors
render={(errors: Array<PathFormStoreItemFlat>) => {
// you can pick or filter specific errors if you want
return (
<>
{errors.map((error) => {
return <AlertError message={error.storeItem.meta.error.message}>;
}}
</>
);
}}
/>
The path selector to the item in your store.
The value use on the initial render, if the store item does not already exist.
The callback to render an item in the array.
The callback of what to render when the array is empty.
Use this hook from any child component scope to access the context of your form.
Returns the form context provider object
with helper functions:
const { setValue, setTouched, addError, clearError, reset, isDirty, array } = usePathForm();
Sets the store item value
at the given path
.
Marks the store item at the given path as touched
.
Adds an error
at the given path
.
Clears an error
at the given path
.
Resets the form to existing defaultValues
(from initialRenderValues
), or changes and resets to new defaultValues
if provided in options.
Checks if any fields in the form have are dirty. You could use this to check for unsaved changes before navigating away.
An object of utilities for mutating array items in your form.
const { array } = usePathForm();
Appends an item
to the end of the array at given path
.
array.append(["deeply", "nested", "items"], { "name": "Santa's Little Helper" });
Insert items
into a collection at the given path
/ index
.
array.insert(["deeply", "nested", "items"], 3, { "name": "Santa's Little Helper" });
Moves an item
in the array at given path
, from the fromIndex
to the toIndex
. Useful for reordering items.
array.move(["deeply", "nested", "items"], 3, 4);
Prepends an item
to the beginning of the array at given path
.
array.prepend(["deeply", "nested", "items"], { "name": "Santa's Little Helper" });
Removes items from the array at given path
at index
. Removes 1
item by default.
// remove one item at index 2
array.remove(["deeply", "nested", "items"], 2);
// remove three items starting at index 2
array.remove(["deeply", "nested", "items"], 2, 3);
Splice the array at given path
/ index
.
// replace 2 items starting at index 6
array.splice(["deeply", "nested", "items"], 6, 2, 'replaced-1', 'replaced-2');
Returns an array of [value, meta]
at the given path
.
const [nameValue, nameMeta] = usePathFormValue(['person', 'name']);
const [ageValue, ageMeta] = usePathFormValue(['person', 'age']);
react-pathform
has basic validators which you can define on each field.
<PathFormField
path={['email']}
defaultValue=""
validators={[
{ type: 'required', message: 'Email is required' },
{ type: 'regex', value: REGEX_EMAIL, message: 'Email is invalid' },
]}
render={({ inputProps, meta }) => {
return (
<TextField
label="Email"
placeholder="[email protected]"
{...inputProps}
error={!!meta.error}
helperText={meta.error?.message}
/>
)
}}
/>
You can validate against a yup
schema with validateYupSchema
.
<PathForm
onValidate={validateYupSchema(schema)}
onSubmit={(values) => {
// validated!
console.log(values);
}}
>
You can Bring Your Own Validator. Take a look at validateYupSchema.
type PathFormPath = Array<string | number>;
const path: PathFormPath = ["deeply", "nested", "items", 0, "children", 0, "name"];
The path to an item in your form.
Strings imply object property.
Numbers imply array index.
The input props to hook your store into your component.
type PathFormInputProps = {
name: string;
value: any;
onChange: (event?: any, value?: any) => any;
onBlur: (event?: any) => any;
}
type PathFormValidation =
| { type: 'required'; message: string }
| { type: 'minLength' | 'maxLength' | 'min' | 'max'; value: number; message: string }
| { type: 'regex'; value: RegExp; message: string }
| { type: 'custom'; value: (value: any, store?: any) => boolean; message: string };
type PathFormError = {
type: string;
message: string;
value: any;
};
type PathFormStoreMeta = {
uuid: string;
dirty: boolean;
touched: boolean;
error: null;
};
type PathFormStoreItemFlat = {
dotpath: string;
path: PathFormPath;
storeItem: PathFormStoreItem;
};
type PathFormResetOptions = {
defaultValues?: any;
}
I have loved many form react form libraries (wow, holy nerd right?). I have gone from redux-form to react-final-form to formik to react-hook-form. They are all amazing libraries. This project aims to provide all the best things from each library: the global control of redux-form, the observable model of react-final-form, the api of formik, and the performance of react-hook-form.
These libraries use the native input property name
as a dot notation string to bind or select data:
const name = "deeply.nested.items[0].children[0].name";
Whereas this library derives the input name
from the path
. The difference is,
you can easily spread arrays, not strings.
// name="deeply.nested.items"
const parentPath = ["deeply", "nested", "items"];
// name="deeply.nested.items[0].children"
const childPath = [...parentPath, itemIndex, "children"];
// name="deeply.nested.items[0].children[0].name"
const deepPath = [...childPath, childIndex, "name"];
This makes nested / recursive form components much cleaner.
The internal form store wraps the form structure alongside meta
next to values.
Values in the store are either an object, array, or primitive.
Objects and Arrays can have both child items, but only array is iterable.
Primitive values cannot have any children.
- Reset to
defaultValues
- Meta
dirty
/touched
- CONTRIBUTING.md