@kaliber/forms

2.1.2 • Public • Published

Forms

A set of utilities to help you create forms in React.

Motivation

Creating ad-hoc forms using React hooks is quite easy, but there is one thing that is a bit hard to do: preventing to render the complete form on each keystroke.

Another motivation is to make form handling within our applications more consistent.

Installation

yarn add @kaliber/forms

Usage

Please look at the example for more advanced use-cases.

import { useForm, useFormField } from '@kaliber/forms'
import { required, email } from '@kaliber/forms/validation'

const validationErrors = {
  required: 'This field is required',
  email: 'This is not a valid email',
}

export function Basic() {
  const { form: { fields }, submit } = useForm({
    // provide initial values to populate the form with
    initialValues: {
      name: '',
      email: '',
    },
    // create the form structure, fields are essentially their validation functions
    fields: {
      name: required,
      email: [required, email],
    },
    // handle form submit
    onSubmit: handleSubmit,
  })

  return (
    <form onSubmit={submit}>
      <TextInput label='Name' field={fields.name} />
      <TextInput label='Email' field={fields.email} />
      <button type='submit'>Submit</button>
    </form>
  )

  function handleSubmit(snapshot) {
    // note that the snapshot can still be invalid
    console.log(snapshot)
  }
}

function TextInput({ label, field }) {
  const { name, state, eventHandlers } = useFormField(field)
  const { value = '', error, showError } = state
  return (
    <>
      <div>
        <label htmlFor={name}>{label}</label>
        <input id={name} type='text' {...{ name, value }} {...eventHandlers} />
      </div>
      {showError && <p>{validationErrors[error.id]}</p>}
    </>
  )
}

Reference

See the example for use cases

Hooks

import { ... } from '@kaliber/forms'

useForm

Defines a form.

const {
  form, // 'object' field containing the form
  submit, // handler that can be used to submit the form
  reset, // handler that can be used to reset the form
} = useForm({
  fields, // form structure
  initialValues, // (optional) initial form values
  validate, // (optional) validation for the complete form
  onSubmit, // called when the form is submitted
  formId, // (optional) a custom id, can be useful when multiple forms are placed on the same page
})
Input
fields An object with the shape: { [name: string]: FormField }.
initialValues An object with the shape: { [name: keyof fields]: ValueFor<fields[name]> }
validate One of Validate or Array<Validate>
onSubmit A function that accepts a Snapshot
FormField One of BasicField, ArrayField or ObjectField
BasicField One of null, Validate or Array<Validate>
ArrayField Created with array(fields) or array(validate, fields)
ObjectField Created with object(fields) or object(validate, fields)
Validate A function with the following shape: `(x, { form, parents }) => falsy
ValueFor<BasicField> Value can be anything and depends on the value passed to onChange
ValueFor<ArrayField> Value is an array with objects mirroring the fields of that array
ValueFor<ObjectField> Value is an object mirroring the fields of that object
Snapshot An object with the following shape: { invalid, value, error }
Output
form An object with the shape: { fields: { [name: string]: FormField } }. Note that form is an ObjectField
submit A function that can be used as onSubmit handler
reset A function that can be used to reset the form
FormField One of BasicField, ArrayField or ObjectField
BasicField Can be used with the useFormField hook
ArrayField Can be used with the `useArrayFormField hook
ObjectField Can be used with the `useObjectFormField hook

useFormField

Subscribes to state changes in the form field and provides the event handlers for form elements.

const {
  name, // the fully qualified name of the form field
  state, // 'object' that contains the form field state
  eventHandlers, // 'object' that contains handlers which can be used by form elements
} = useFormField(field)
Output
name The fully qualified name of the form field
state An object
- value The value of the field
- error The validation error for the field
- isSubmitted Indicates if the form was submitted
- isVisited Indicates if the field has been visited (had focus)
- hasFocus Indicates if the field currently has focus
- invalid The same as !!error
- showError Handy derived boolean to determine when to show an error
eventHandlers An object
- onBlur Handler for onBlur events
- onFocus Handler for onFocus events
- onChange handler for onChange events, accepts DOM event or value

useNumberFormField

Specialized version of useFormField that converts the value to a a number if possible.

useBooleanFormField

Specialized version of useFormField that uses the checked state of the event target as value.

useArrayFormField

Subscribes to state changes in the form field and provides helpers for the array field.

const {
  name, // the fully qualified name of the form field
  state, // 'object' that contains the form field state
  helpers, // 'object' that contains handlers that can be used to manipulate the array field
} = useArrayFormField(field)
Output
name The fully qualified name of the form field
state An object
- children The child fields (these are object type fields)
- error The validation error for the field
- isSubmitted Indicates if the form was submitted
- invalid The same as !!error
- showError Handy derived boolean to determine when to show an error
helpers An object
- add Handler to add a field, accepts an initialValue for the child field
- remove Handler to remove a field, accepts the child field

useObjectFormField

Subscribes to state changes in the form field and provides the fields of the object.

const {
  name, // the fully qualified name of the form field
  state, // 'object' that contains the form field state
  fields, // 'object' containing the fields
} = useObjectFormField(field)
Output
name The fully qualified name of the form field
state An object
- error The validation error for the field
- isSubmitted Indicates if the form was submitted
- invalid The same as !!error
- showError Handy derived boolean to determine when to show an error
fields An object containing the fields

useFormFieldSnapshot

Subscribes to the state of a field (or form).

const snapshot = useFormFieldSnapshot(form)
Output
snapshot An object
- invalid Boolean indicating whether the field is invalid
- error One of BasicError, ObjectError or ArrayError
- value The value of the field
BasicError The result of the validation function
ObjectError An object with the shape: { self, children } where self is a BasicError and children an object with errors
ArrayError An object with the shape: { self, children } where self is a BasicError and children an array with errors

useFormFieldValue

Subscribes to the value of a field (or form).

const value = useFormFieldValue(field)
Output
value The value of the field.

useFormFieldsValues

Subscribes to the values of multiple fields (or forms).

const values = useFormFieldsValues(fields)
Output
values An array with the values of the fields.

Schema

import { ... } from '@kaliber/forms'

array

Used to create an array form field.

array(validationOrFields, fields)

Has two signatures:

array(validation, fields)
array(fields)

If you want to use heterogeneous arrays (different types) you can use a function instead of a fields object:

array(initialValue => ({
  _type: required,
  ...(
    initialValue._type === 'content' ? { text: required } :
    initialValue._type === 'image' ? { image: required } :
    null
  )
}))

When rendering the array field you can render a different component based on the value of the field:

const { state: { children }, helpers } = useArrayFormField(field)

return (
  <>
    {children.map(field => {
      const { _type } = field.value.get()
      return (
        _type === 'content' ? <ContentForm key={field.name} {...{ field }} /> :
        _type === 'image' ? <ImageForm key={field.name} {...{ field }} /> :
        null
      )
    })}
    <button type='button' onClick={_ => helpers.add({ _type: 'content' })}>Add content</button>
    <button type='button' onClick={_ => helpers.add({ _type: 'image' })}>Add image</button>
  </>
)

object

Used to create an object form field.

object(validationOrFields, fields)

Has two signatures:

object(validation, fields)
object(fields)

Validation

Validation functions have this shape: (value, { form, parents }) => falsy | { id, params }

Input
value The value of the form field
form The value of the complete form
parents An array with the values of the parents (when using object or array fields)
Output
id An identifier to translate the error into something for people
params Parameters useful for constructing the validation error

We provide a few commonly used validation functions.

import { ... } from '@kaliber/forms/validation'
required Reports when the value is 'falsy' and not 0 or false.
optional An alias for null, it's there for consistency and readability.
number Reports if the value can not be converted to a number.
min and max Reports if the value is outside of the given value.
minLength and maxLength Reports if the length of the value is outside of the given value.
email Reports if the value does not vaguely look like an email address.
error Utility to create an error object.

Components

When you want to conditionally render or set some props based on your current form state, you should avoid the use of useFormFieldSnapshot or useFormFieldValue in your form root, since that will re-render your entire form with each change. Rather you should create a specialised component and make the values available through a render prop.

Because this is such a common usecase, we provide several of these components.

FormFieldValue

Props
render A function with the following shape: `value => React.ReactNode
field The field whose value is used as the value argument when calling render.
Example
<FormFieldValue field={fields.subscribeToNewsletter} render={value => (
  value && <TextInput label='Email' field={fields.email} />
)}>

FormFieldsValues

Props
render A function with the following shape: `values => React.ReactNode
fields The fields whose values are used as the values argument when calling render.
Example
<FormFieldValue field={[fields.firstName, fields.lastName]} render={([firstName, lastName]) => (
  firstName && lastName && <Greeting>{firstName} {lastName}</Greeting>
)}>

FormFieldValid

Props
render A function with the following shape: `valid => React.ReactNode
field The field whose validity state is used as the valid argument when calling render.
Example
<FormFieldValid field={form} render={valid => (
  <Button type="submit" disabled={!valid}>Verstuur</Button>
)}>

Missing feature?

If the library has a constraint that prevents you from implementing a specific feature (outside of the library) start a conversation.

If you think a feature should be added to the library because it solves a specific use case, discuss it to determine if the complexity of introducing it is worth it.

Other libraries

Existing libraries have one or more of the following problems:

  • Too big / too complex
  • Not following the React philosophy
  • Too much boilerplate for simple forms
  • Validation results are
    • hard to translate
    • difficult to use in combination with accessibility principles
  • Different guiding principles and trade-offs

Guiding principles

  • Static form structure
  • No visual components
  • Clean DSL for creating the form structure
  • No async validation
  • Minimal / small
  • Translatable validation results
  • Conscious complexity / usefulness / commonality trade-offs

Disclaimer

This library is intended for internal use, we provide no support, use at your own risk. It does not import React, but expects it to be provided, which @kaliber/build can handle for you.

This library is not transpiled.

Package Sidebar

Install

npm i @kaliber/forms

Weekly Downloads

0

Version

2.1.2

License

MIT

Unpacked Size

39.8 kB

Total Files

17

Last publish

Collaborators

  • melissabos18
  • janbru
  • albertkaliber
  • jerryduijm
  • hjsielcken
  • kaliber-owner
  • daveykropf
  • eecolor
  • larsvankleef