3.1.0 • Published 7 months ago

@mmckelvy/use-form v3.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

useForm

A lightweight, flexible hook for managing form state.

Features

  • Hooks based.
  • Simple API. No render props, custom components, Context, or magic.
  • Handles complex form structures including arrays and nesting.
  • Built in pre-validation, validation, and serialization with override options.
  • Joi integration.
  • Tools to handle custom form elements (e.g. React Select), undos, resets, wizards, and more.

Installation

npm install --save @mmckelvy/use-form

Usage

import useForm from '@mmckelvy/use-form';

import saveDataToServer from './path-to-persistence-library';

export default function MyForm() {
  const {
    fields,
    handleChange,
    handleSubmit
  } = useForm({
    firstName: {
      value: '',
      placeholder: 'Enter your first name'
    }
  )};

  return (
    <div>
      <label>{fields.firstName.label}</label>

      <input
        name="firstName"
        value={fields.firstName.value}
        placeholder={fields.firstName.placeholder}
        onChange={handleChange}
      />
    
      <span>{fields.firstName.error}</span>

      <button
        type="button"
        onClick={() => {
          const { isValid, values } = handleSubmit();

          console.log(values); // {firstName: 'John'}

          if (isValid) {
            saveDataToServer(values);
          } else {
            console.log('There was a problem with your inputs');
          }
        }}>
        
        Submit
      </button>
    </div>
  );
}

Demo

npm install

In one terminal tab run:

npm run start:dev

In another terminal tab run:

npm run watch:dev

Code for each example in the demo is located here.

Description

Overview

Call the useForm hook and pass it an initialFields object. useForm will return an object with the following properties:

  • A fields object with your initialFields updated with the latest state changes.
  • A handleChange function you can pass to your inputs' onChange handlers.
  • A handleSubmit function that will pre-validate, validate, and serialize your form.
  • A setFields function that will set multiple field properties to values you specify.
  • A replaceFields function that will replace multiple field properties with new fields.
  • A setResetPoint function that will save the current state of the form for subsequent resets.
  • A reset function that will reset the fields to their initial state or a reset point specified by setResetPoint.
  • A hasChanged boolean value that indicates if any property in the form has changed.

The general useForm lifecycle is: 1. Call useForm with your initialFields. 2. Use the returned fields object and handleChange to update the fields in response to user input. 3. Use setFields when you need to update selects, custom elements, or you just need more control over field updates than what the handleChange function provides. 4. Call handleSubmit when the user submits the form. handleSubmit will pre-validate all fields (remove extra space, parse numbers), validate all fields, and then serialize all fields (prepare the fields for submission to the server).

Field properties

useForm takes an initialFields object. Top level keys in the initialFields object correspond to the input name property. Keys in the leaf object correspond to actual field properties, which include input values, metadata, validation, and other state data. The full list of field properties, with their default values is as follows:

// value detail
value
displayValue
checked
snapshot
disabled

// metadata
type
label
placeholder
order
path

// validation
required
preValidate
validate
isValid
error

// serialize
serialize
includeEmpty
exclude

Detail for each property is included below.

value

String, number, JS date object, or a boolean. Required.

The actual field value.

Default: empty string.

displayValue

String, number, JS date object, or a boolean. Optional.

A human friendly alternative display value.

Default: null.

checked

Boolean. Optional.

The checked property for use with checkboxes if desired.

Default: null.

snapshot

String, number, JS date object, or a boolean. Optional.

A snapshot of the value property. Useful for undos.

Default: empty string.

disabled

Boolean. Optional.

Whether the field is disabled.

Default: false.

type

String / Enum. Optional.

The field type. One of 'number', 'text', 'boolean', or 'multiLine'.

Default: 'text'.

label

String. Optional.

The field label.

Default: Proper case of the field name. e.g. 'firstName' -> 'First name'.

placeholder

String. Optional.

A placeholder value.

Default: null.

order

Number. Optional.

A number that can be referenced to set the order of non-array fields.

Default: null.

path

String. Set automatically.

A convenience property providing the full path to the field.

foo: {
  bar: [
    {
      baz: {
        value: 8
      }
    }
  ]
}

console.log(baz.path) // foo.bar.0.baz

Default: automatic.

required

Boolean. Optional.

Whether a field is required.

Default: true.

preValidate

Function or Boolean. Optional.

A function that is called during the pre-validate phase of handleSubmit. Signature is:

preValidate({ value, field, fields })

where value is the field's value, field is the full field with all properties, and fields is all fields with their properties.

Returns the parsed value.

Default: A function that trims fields of type 'text' and 'multiLine', parses fields of type 'number' to JS numbers, and converts instances of JS Date to ISO strings.

Set to false to skip the pre-validation step.

validate

Function or Boolean. Optional.

A function that is called after pre-validation during handleSubmit. Signature is:

validate({ value, field, fields })

where value is the field's value, field is the full field with all properties, and fields is all fields with their properties.

Returns an object with the following properties:

isValid: <Boolean>, // Whether the field is valid.
value: <String> or <Number>, // The field value
error: <String>, An error message, if applicable.

Default: A function that checks for empty strings if required is set to true and for numbers if type is set to number.

Set to false to skip the validation step.

isValid

Boolean. Optional.

Whether or not the field passed validation.

Default: true.

error

String. Optional.

An error message. Set automatically during validation as appropriate.

Default: null.

serialize

Function. Optional.

A function that is called after validation during handleSubmit. Signature is:

serialize({ value, field, fields })

where value is the field's value, field is the full field with all properties, and fields is all fields with their properties.

Returns a serialized value.

Default: A function that returns the value (following pre-validate and validate steps).

includeEmpty

Boolean. Optional.

Whether to include the field, even when empty, when the form is serialized.

Default: false.

exclude

Boolean. Optional.

Whether to exclude a field when the form is serialized.

Default: false.

Field structure

Fields can have nested or array structures:

const fields = {
  basics: {
    fruit: {
      value: 'banana',
      label: 'Fruit',
    },
    recipients: [
      {
        person: {
          firstName: {
            value: 'Joe',
            label: 'First name',
          }
        }
      },
      {
        person: {
          firstName: {
            value: 'Bill',
            label: 'First name',
          }
        }
      }
    ]
  },
  order: {
    orderNumber: {
      value: '2453',
      label: 'Order number',
    },
  }
};

In this case, the input name for the first key would be basics.fruit, and the input name for the first recipient would be basics.recipients[0].person. See the examples in the demo folder for reference.

You can nest as deeply as you want. The one requirement is that the leaf object should hold your actual field properties (e.g. value, label, etc.).

Submitting the form

As referenced previously, when you call handleSubmit, all fields pass through a three step process:

  1. pre-validation (clean up the inputs and put them into a validation ready format)
  2. validation (ensure inputs match what's expected)
  3. serialization (prepare the data for submission to a server / persistence layer)

Each field comes with its own preValidate, validate, and serialize functions out of the box. You can override these functions with your own preValidate and/or validate functions, or opt out of pre-validation, validation, or both by passing false to the applicable property.

The default preValidate function does the following:

  • Removes any extra space.
  • Converts dates to ISO strings (using date.toISOString()).
  • Parses inputs of type number to actual JavaScript numbers.

The default validate function rejects empty strings if the input is required, and verifies the user passed a numeric value if the field type is number.

Once the form passes through the validation step, handleSubmit will serialize the form. This serialization process will (i) call a custom serialize function for a given field if you've defined one, and (ii) strip all metadata and unnecessary field properties in preparation for submission to the server. i.e. this:

firstName: {
  value: 'Foo',
  placeholder: 'John',
  error: null
},

becomes this:

{firstName: 'Foo'}

Again, you can pass your own function for preValidate, validate, and/or serialize. The signature for all three functions is:

preValidate / validate / serialize({ value <string>, field, <object>, fields <object> }) value is the input value. field is the full field object for the current field. fields is the full fields object (i.e. all the fields).

preValidate should return a clean value. validate should return the following object:

{
  isValid: <Boolean>,
  value: <String> or <Number>,
  error: null or <String>
}

serialize should return the serialized value.

handleSubmit has several options. The signature for the function is as follows:

handleSubmit({
  fields <Array of Objects>
  preValidate <Boolean>,
  validate <Boolean>,
  schema <Joi Validation Schema>
})

fields should be an array of the format:

[
  {
    path: 'lastName'
  },
  ...
]

If you pass in fields, only those fields will be pre-validated, validated, and serialized.

If preValidate is set to false, pre-validation will be skipped. Same with validate.

Validating with Joi

You can use Joi schemas to validate your inputs. Pass in a schema and that schema's validate method will be run against a serialized version of the form fields. Any Joi errors will be set on the appropriate field's error property.

See JoiValidation for an example.

Return Values

handleSubmit returns an object with the following properties:

{
  isValid <Boolean>,
  values <Object>,
  errors <Object>
}

isValid notes whether there were any validation errors.

values are the seralized values from the form.

errors is an object with two properties: fieldErrors, errors for individual fields, and generalErrors, errors resulting from a Joi schema validation run that don't apply to individual fields.

Modifying fields

handleChange should take care of your general onChange handlers. Simply pass handleChange to your input's onChange property and useForm will take care of the rest.

useForm also provides several other functions and properties when you need to handle selects or more complicated inputs, or you need more control over form state:

setFields

Set an array of field properties at specified paths.

Signature:

setFields([
  {
    path,
    value,
  },
])

where path is a path to the field property you want to set (e.g. foo.bar.baz.value) and value is the value you want to set the property to.

You can set any number of fields and you can set any field property (e.g. value, displayValue, checked, error, etc.).

See WithSelects for an example of how setFields can help you manage a select element and Undo for an example of how setFields can help you implement undo functionality.

replaceFields

Replace all fields at the specified key(s) with new fields.

Signature:

replaceFields(fields)

where fields is a fields object that contains the new fields. Note that the fields will replace each top level key that matches. So if you have an existing set of fields like so:

{
  employee: {
    firstName: {
      value: 'John'
    },
    lastName: {
      value: 'Smith'
    }
  }
}

and you call:

replaceFields({
  employee: {
    firstName: {
      value: 'Bill'
    },
    lastName: {
      value: 'Jenkins'
    }
  }
})

The employee key, and everything below it will be replaced with the new fields object you pass in.

See WithArrays for an example of how replaceFields can help you add and remove elements in an array of inputs.

setResetPoint

Set a reset point for later resets.

Signature:

setRestPoint(fields)

where fields is an optional fields object. If you don't pass in fields, a reset point will be created with the current form state. If you do pass in a fields object, those fields will be used to set the reset point.

See ResetForm for an example of how setResetPoint can help you reset a form.

reset

Reset a form. If a reset point has been set via setResetPoint, then the form will be reset to that state, else it will be reset to its initial state.

Signature:

reset()

This function takes no arguments.

hasChanged

A boolean value that indicates if any part of the form has changed.

Testing

Run the unit tests

npm run test:unit

Run the component tests (uses Cypress)

npm run test:component
3.0.2

9 months ago

3.1.0

7 months ago

3.0.1

11 months ago

3.0.0

11 months ago

2.0.0

1 year ago

1.0.0

2 years ago