react-form-fp
v2.2.0
Published
A form validation library for React, build with FP principles for easy form state and validation handling or even individual fields.
Downloads
29
Maintainers
Readme
React Form FP
Yet another form library for React, created as context provider, following functional programming principles.
React Form FP is a state management and validation library for easy form manipulation. Entire API is exposed though context provider, so it makes it easy to work with class components or functional components
Library has two major functionalities FormContext for form state and validation and WizardContext for multi-step forms.
Installation
Install the package from NPM by running:
npm install react-form-fp
or
yarn add react-form-fp
Usage
Form Context has on 2 major blocks
- State
- Form State and Errors
- Field value update handler
- Adding and Removing form fields
- Validation Schema
- Schema
- Validators - Single Field Validator - Entire Form Validator - Registering New Validators Dynamically
Form State
Form State is created from the initial state provided to the FormContextProvider and extended with errors property.
<FormContextProvider initialState={...} ...> ... </FormContextProvider>
State model is really simple but strict, initial state must have form name as top level key, and form fields as nested level properties, this is because of support for multiple forms on single view as well as multiple steps forms (Wizard).
So for example if we want to handle login form which have a username and password fields, our initial state model would look like this:
const initialState = { login: { username: '', password: '' } }
And inside of your component you can consume that state by using useFormContext()
const { state: { login } } = useFormContext()
And this login object is of same structure as you initial state, so you can now have some input with value, for example, username
<input value={login.username} ... />
Field value update handler
To update any field value inside the form you need to use setFieldValue handler from useFormContext. It is a very simple method that is in charge of updating specific field within the form.
Method is curried function and accepts 3 params formName(form key in FormContext state) field(key within the form) value(new value for the field)
const { setFieldValue } = useFormContext()
...
const handleFormChange = setFieldValue('login')
const handleFieldChange = (field) => ({ target: { value }}) => handleFormChange(field)(value);
...
<input value={login.username} onChange={handleFieldChange('username')} ... />
Adding and Removing Form Fields
Sometimes you will need to add new fields to a form, or to remove the existing ones, depending on your application logic
Add Form Field (Validator is optional)
type AddField<FormName extends string | number, ValueType = FieldValue> = ( formName: FormName, field: string, value: FieldValue, validator?: Validator<ValueType> ) => void
Remove Form Field (Validator will be removed)
type RemoveField<FormName extends string | number> = ( formName: FormName, field: string ) => void
Errors
Errors are stored inside of the FormContext state by the errors key and every form field that has a defined validator have either null or string value
Error State type is:
type ErrorState<FormName extends number | string, T> = { errors: { [key in FormName]: { [key in FieldName<T>]: string | null } }
In our login example, error message logic for username field should look something like this
...
{!!errors.example.username && <p className='text-danger'>{errors.example.username}</p>}
...
Validation Schema
Validation schema is used to define form validation and it follows the same structure as initial form state
{ [formName]: { formField: validatorFunc } }
Validator function is a simple function that accepts a value and returns either string | null
ValidationSchema is generic type accepts two additional types FormName FormType export type Validator<VT = unknown, ST = unknown> = ( value: VT, state?: ST ) => string | null export type ValidationSchema< FormName extends number | string = string, FormField extends number | string = string > = { [key in FormName]: { [key in FormField]: Validator } }
So for our login form we can define a validation schema:
{ login: { firstName: (val: string) => !!val.trim() ? null : 'First name is required', lastName: () => null // Empty validator } }
The idea behind it is really simple, every field that has a defined validator will be validated against it, and if validator returns a null field is valid.
Single Field Validator
Field validator is a function which takes field value as an argument and returns either string or null. Additionally every validator gets a FormContext state as second a parameter, which can be useful when writing a conditional validations (if the field validation logic depends on a another field value within the state. ex: Passport number validation is bound to selected country)
Validator function type is:
type Validator<VT, ST> = (value: VT, state?: ST) => string | null
Validate Entire Form
Validate entire form will run all defined validator and return true or false, also while running the validators will set form errors if any of the validator fails
Validate form function type is
ValidateForm<FormName extends string | number, T> = (formName: FormName, values: T) => boolean
Set Field Validator
Set filed validator is used for adding, removing or updating validators from initial validation schema.
Type of setValidator function is:
sSetValidator<FormName extends string | number, T, ValueType = unknown> = ( formName: FormName, field: keyof T, validator: Validator<ValueType> ) => void
Examples
Simple Form validation
import React, { useCallback, ChangeEvent } from 'react'
import {
FormContextProvider,
ValidationSchema,
useFormContext
} from 'react-form-fp'
enum FormName {
Example = 'example'
}
enum FormField {
FirstName = 'firstName',
LastName = 'lastName'
}
type FormState = {
firstName: string
lastName: string
}
const validation: ValidationSchema<FormName, FormField> = {
example: {
firstName: (val: string) =>
!!val.trim() ? null : 'First name is required',
lastName: () => null // Empty validator
}
}
const SimpleFormValidationExample = () => {
const {
setFieldValue,
validate,
validateForm,
state: { errors, example }
} = useFormContext<FormName, FormState>()
const handleFieldChange = useCallback(
(field: FormField) => ({
target: { value }
}: ChangeEvent<HTMLInputElement>) => {
setFieldValue(FormName.Example)(field)(value)
},
[setFieldValue]
)
const validateField = useCallback(
(field: FormField) => ({
target: { value }
}: ChangeEvent<HTMLInputElement>) => {
validate(FormName.Example)(field)(value || example[field])
},
[validate, example]
)
const submit = useCallback(() => {
if (validateForm(FormName.Example, example)) {
alert(JSON.stringify(example, null, 2))
}
}, [validateForm, example])
return (
<div className='form-group'>
<label>First Name</label>
<input
value={example.firstName}
onChange={handleFieldChange(FormField.FirstName)}
onBlur={validateField(FormField.FirstName)}
/>
{!!errors.example.firstName && (
<p className='text-danger'>{errors.example.firstName}</p>
)}
<label>Last Name</label>
<input
value={example.lastName}
onChange={handleFieldChange(FormField.LastName)}
onBlur={validateField(FormField.LastName)}
/>
{!!errors.example.lastName && (
<p className='text-danger'>{errors.example.lastName}</p>
)}
<button onClick={submit}>Submit Form</button>
</div>
)
}
export default () => (
<FormContextProvider<FormName, FormState>
initialState={{ example: { firstName: '', lastName: '' } }}
validationSchema={validation}
>
<SimpleFormValidationExample />
</FormContextProvider>
)
Conditional Form validation
import React, { useCallback, ChangeEvent } from 'react'
import {
FormContextProvider,
ValidationSchema,
useFormContext
} from 'react-form-fp'
enum FormName {
Example = 'example'
}
enum FormField {
FirstName = 'firstName',
LastName = 'lastName',
NumberOfChildren = 'numberOfChildren',
HasKids = 'hasKids'
}
type FormState = {
firstName: string
lastName: string
hasKids: boolean
numberOfChildren?: number
}
const validation: ValidationSchema<FormName, FormField> = {
example: {
firstName: (val: string) =>
!!val.trim() ? null : 'First name is required',
lastName: () => null,
hasKids: () => null,
numberOfChildren: (val: number, state?: { example: FormState }) => {
if (state?.example.hasKids) {
return val > 0 ? null : 'Please specify how many children do you have.'
}
return null
}
}
}
const ConditionalFormValidationExample = () => {
const {
setFieldValue,
validate,
validateForm,
state: { errors, example }
} = useFormContext<FormName, FormState>()
const handleFieldChange = useCallback(
(field: FormField) => ({
target: { value }
}: ChangeEvent<HTMLInputElement>) => {
setFieldValue(FormName.Example)(field)(value)
},
[setFieldValue]
)
const handleCheckboxChange = useCallback(() => {
setFieldValue(FormName.Example)(FormField.HasKids)(!example.hasKids)
}, [setFieldValue, example])
const validateField = useCallback(
(field: FormField) => ({
target: { value }
}: ChangeEvent<HTMLInputElement>) => {
validate(FormName.Example)(field)(value || example[field])
},
[validate, example]
)
const submit = useCallback(() => {
if (validateForm(FormName.Example, example)) {
alert(JSON.stringify(example, null, 2))
}
}, [validateForm, example])
return (
<div className='form-group'>
<label>First Name</label>
<input
value={example.firstName}
onChange={handleFieldChange(FormField.FirstName)}
onBlur={validateField(FormField.FirstName)}
/>
{!!errors.example.firstName && (
<p className='text-danger'>{errors.example.firstName}</p>
)}
<label>Last Name</label>
<input
value={example.lastName}
onChange={handleFieldChange(FormField.LastName)}
onBlur={validateField(FormField.LastName)}
/>
{!!errors.example.lastName && (
<p className='text-danger'>{errors.example.lastName}</p>
)}
<label className='row paper-check'>
<input
id='paperSwitch4'
name='paperSwitch4'
type='checkbox'
checked={example.hasKids}
onChange={handleCheckboxChange}
/>
<span className='paper-switch-slider'>Do you have kids?</span>
</label>
<br />
{example.hasKids && (
<>
<label>Number Of children</label>
<input
type='number'
value={example.numberOfChildren}
onChange={handleFieldChange(FormField.NumberOfChildren)}
onBlur={validateField(FormField.NumberOfChildren)}
/>
{!!errors.example.numberOfChildren && (
<p className='text-danger'>{errors.example.numberOfChildren}</p>
)}
</>
)}
<button onClick={submit}>Submit Form</button>
</div>
)
}
export default () => (
<FormContextProvider<FormName, FormState>
initialState={{
example: {
firstName: '',
lastName: '',
hasKids: false,
numberOfChildren: 0
}
}}
validationSchema={validation}
>
<ConditionalFormValidationExample />
</FormContextProvider>
)
Adding and removing Validators
import React, { useCallback, ChangeEvent, useEffect, useMemo } from 'react'
import {
FormContextProvider,
ValidationSchema,
useFormContext
} from 'react-form-fp'
enum FormName {
Example = 'example'
}
enum FormField {
FirstName = 'firstName',
LastName = 'lastName',
Married = 'married',
WifeName = 'wifeName'
}
type FormState = {
firstName: string
lastName: string
married: boolean
wifeName: string
}
const validation: ValidationSchema<FormName, FormField> = {
example: {
firstName: (val: string) =>
!!val.trim() ? null : 'First name is required',
lastName: () => null,
married: () => null,
wifeName: () => null
}
}
const DynamicValidators = () => {
const {
setFieldValue,
validate,
validateForm,
state: { errors, example },
setValidator
} = useFormContext<FormName, FormState>()
const married = useMemo(() => example.married, [example])
const handleFieldChange = useCallback(
(field: FormField) => ({
target: { value }
}: ChangeEvent<HTMLInputElement>) => {
setFieldValue(FormName.Example)(field)(value)
},
[setFieldValue]
)
const handleCheckboxChange = useCallback(() => {
setFieldValue(FormName.Example)(FormField.Married)(!example.married)
}, [setFieldValue, example])
const validateField = useCallback(
(field: FormField) => ({
target: { value }
}: ChangeEvent<HTMLInputElement>) => {
validate(FormName.Example)(field)(value || example[field])
},
[validate, example]
)
const submit = useCallback(() => {
if (validateForm(FormName.Example, example)) {
alert(JSON.stringify(example, null, 2))
}
}, [validateForm, example])
useEffect(() => {
if (married) {
/*
THIS IS JUST AN EXAMPLE, IF YOU HAVE A VALIDATOR THAT IS DEPENDANT OF STATE USE CONDITIONAL VALIDATORS
*/
// Set new validator
setValidator(FormName.Example, FormField.WifeName, (val: string) => {
return val?.length > 2 ? null : 'Name must be at least 3 chars long.'
})
} else {
// Clear validator
setValidator(FormName.Example, FormField.WifeName, () => null)
}
}, [married])
return (
<div className='form-group'>
<label>First Name</label>
<input
value={example.firstName}
onChange={handleFieldChange(FormField.FirstName)}
onBlur={validateField(FormField.FirstName)}
/>
{!!errors.example.firstName && (
<p className='text-danger'>{errors.example.firstName}</p>
)}
<label>Last Name</label>
<input
value={example.lastName}
onChange={handleFieldChange(FormField.LastName)}
onBlur={validateField(FormField.LastName)}
/>
{!!errors.example.lastName && (
<p className='text-danger'>{errors.example.lastName}</p>
)}
<label className='row paper-check'>
<input
id='switch8'
type='checkbox'
checked={example.married}
onChange={handleCheckboxChange}
/>
<span className='paper-switch-slider'>Are you married?</span>
</label>
<br />
{example.married && (
<>
<label>Wife's Name</label>
<input
value={example.wifeName}
onChange={handleFieldChange(FormField.WifeName)}
onBlur={validateField(FormField.WifeName)}
/>
{!!errors.example.wifeName && (
<p className='text-danger'>{errors.example.wifeName}</p>
)}
</>
)}
<button onClick={submit}>Submit Form</button>
</div>
)
}
export default () => (
<FormContextProvider<FormName, FormState>
initialState={{
example: {
firstName: '',
lastName: '',
married: false,
wifeName: undefined
}
}}
validationSchema={validation}
>
<DynamicValidators />
</FormContextProvider>
)