npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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

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>
)