@noaignite/formit
v3.1.0
Published
Form helper components built with React and MUI in mind.
Downloads
403
Readme
@noaignite/formit
Form helper hook & components built with React and MUI in mind. Heavily inspired by Formik.
Motivation
The main motivation was to create a standardized way of handling form fields using native browser APIs for the build footprint to be as small as possible. Validation is therefore meant to be implemented directly in the html with attributes like required
, type="email"
, pattern="[0-9]{4}"
and so on. Secondly, to have a simple way of using this library together with MUI.
Installation
Formit is available as an npm package.
// with npm
npm install @noaignite/formit
// with yarn
yarn add @noaignite/formit
Usage
There's two ways of using formit, the first being via the useFormit
hook and secondly via the formit components. These being Formit
, Form
, Field
& FormitConsumer
. Let's look at how these can be implemented.
Using the hook
Using the useFormit
hook is the simplest way to use formit and handle your forms. The initialValues
key is mandatory, and is used for registering all the fields that you want to be controlled by formit. The validationErrors
key is optional and can be used for printing error messages based on the validation rules you've defined in the html of each respective form field. The onSubmit
callback will give you the controlled values as the first argument and actions as the second argument. In the example below we use the setSubmitting
action to set formit into a loading state, momentarily disabling the submit button until the Promise has been resolved.
import * as React from 'react'
import { TextField } from '@mui/material'
import { useFormit } from '@noaignite/formit'
const BasicExample = () => {
const { getFieldMeta, getFieldProps, isSubmitting, onSubmit } = useFormit({
initialValues: {
email: '',
password: '',
},
validationErrors: {
email: 'Please enter a valid email address',
password: 'Password must contain an uppercase letter, a lowercase letter, and a number',
},
onSubmit: async (values, { setSubmitting /*, and more */ }) => {
setSubmitting(true)
await new Promise((resolve) => setTimeout(() => {
console.log(values)
resolve()
}, 1000))
setSubmitting(false)
}
})
return (
<form onSubmit={onSubmit}>
<h1>My formit form</h1>
<TextField
type="email" // Email validation.
required // Required validation.
{...getFieldProps('email')} // Sets `name`, `value` & `onChange`.
{...getFieldMeta('email')} // Sets Mui specific `error` and `herlperText`.
label="Email"
/>
<TextField
pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s).*$" // Password validation.
required // Required validation.
{...getFieldProps('password')} // Sets `name`, `value` & `onChange`.
{...getFieldMeta('password')} // Sets Mui specific `error` and `herlperText`.
label="Password"
type="password"
/>
<button type="submit" disabled={isSubmitting}>Submit</button>
</form>
)
}
export default BasicExample
Using the context components
Using the context components is very similar to using the useFormit
hook. The difference being that the formit state is saved to a React context using the Formit
component, this is then consumed by formit's context components. The advantage to this approach is that your component, in this case BasicExample
, won't have to re-render for each keystroke as the values
& errors
state updates. In the example below, <h1>My formit form</h1>
won't re-render as you type into the the form fields.
A potential downside to this approach can be that it's more difficult to organize your logic. Try out the different approaches and use the one that suits your need the best.
import * as React from 'react'
import { TextField } from '@mui/material'
import { Formit, Form, Field, FormitConsumer } from '@noaignite/formit'
const BasicExample = () => {
return (
<Formit
initialValues={{
email: '',
password: '',
}}
validationErrors={{
email: 'Please enter a valid email address',
password: 'Password must contain an uppercase letter, a lowercase letter, and a number',
}}
onSubmit={async (values, { setSubmitting /*, and more */ }) => {
setSubmitting(true)
await new Promise((resolve) => setTimeout(() => {
console.log(values)
resolve()
}, 1000))
setSubmitting(false)
}}
>
<Form>
<h1>My formit form</h1>
<Field
component={TextField} // Renders Mui TextField.
name="email" // Sets values from `getFieldProps` & `getFieldMeta` based on `name`.
type="email" // Email validation.
required // Required validation.
label="Email"
/>
<Field
component={TextField} // Renders Mui TextField.
name="password" // Sets values from `getFieldProps` & `getFieldMeta` based on `name`.
pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s).*$" // Password validation.
required // Required validation.
type="password"
label="Password"
/>
<FormitConsumer>
{({ isSubmitting }) => (
<button type="submit" disabled={isSubmitting}>Submit</button>
)}
</FormitConsumer>
</Form>
</Formit>
)
}
export default BasicExample
The Field component
Worth mentioning regarding the Field
component is that it also accepts a function as it's children
prop, exposing field specific formit state and functions. This can be handy in those cases where one might need to tweak the behaviour, for example for the Mui Datepicker where the onChange argument does not expose the event as it's argument.
<Field name="date">
{({ field, setValue }) => (
<DatePicker
label="Date"
name={field.name}
value={field.value}
onChange={(newValue) => setValue(newValue)}
/>
)}
</Field>
Preparing your data for the API request
A lot of times your form data will need to comply with a certain data structure that the recipient API requires. In these cases formit can help you out by allowing you to predefine the data structure of your form values so it's ready to be sent to the API. Let's look at how this can be done.
import * as React from 'react'
import { Checkbox, FormControlLabel, TextField } from '@mui/material'
import { Formit, Form, Field, FormitConsumer } from '@noaignite/formit'
const BasicExample = () => {
return (
<Formit
initialValues={{
user: {
firstName: '',
lastName: '',
phoneNumbers: ['', ''],
},
newsletter: false,
}}
onSubmit={(values) => {
/*
`values` would then be structured like below:
{
user: {
firstName: 'Jon',
lastName: 'Doe',
phoneNumbers: ['111111111', '222222222'],
},
newsletter: true,
}
*/
console.log(values)
}}
>
<Form>
<h1>My formit form</h1>
<Field
component={TextField}
name="user.firstName" // Path to formit state
label="First name"
/>
<Field
component={TextField}
name="user.lastName" // Path to formit state
label="Last name"
/>
<Field
component={TextField}
name="user.phoneNumbers[0]" // Path to formit state
label="Primary phone number"
/>
<Field
component={TextField}
name="user.phoneNumbers[1]" // Path to formit state
label="Secondary phone number"
/>
<Field
component={FormControlLabel}
control={<Checkbox />}
name="newsletter" // Path to formit state
label="Newsletter"
/>
<FormitConsumer>
{({ isSubmitting }) => (
<button type="submit" disabled={isSubmitting}>Submit</button>
)}
</FormitConsumer>
</Form>
</Formit>
)
}
export default BasicExample
Sourcing values from API call
Sometimes, the "initial values" you want to init formit with originates from an API endpoint. This means that the desired initial values would be defined after formit has initialized. To solve this, one needs to pass the enableReinitialize
prop to formit. This ensures that formit will reinitialize whenever the values of the initialValues
change.
Below, you have an example of this being done with the context components from formit:
const BasicExample = () => {
const [initialValues, setInitialValues] = React.useState({
email: '',
password: '',
})
React.useEffect(() => {
const fetch = async () => {
// Faked API call, for demonstration.
await new Promise((resolve) =>
setTimeout(() => {
const apiOriginatedInitialValues = {
email: '[email protected]',
password: '****',
}
setInitialValues(apiOriginatedInitialValues)
resolve()
}, 1000),
)
}
fetch()
}, [setInitialValues])
return (
<Formit
initialValues={initialValues}
enableReinitialize
>
{/* ... */}
</Formit>
)
}
Or using the hook:
// ...
const formit = useFormit({
enableReinitialize: true
})
// ...