2.1.2 • Published 2 years ago

@kaliber/forms v2.1.2

Weekly downloads
11
License
MIT
Repository
github
Last release
2 years ago

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

npm.io

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
fieldsAn object with the shape: { [name: string]: FormField }.
initialValuesAn object with the shape: { [name: keyof fields]: ValueFor<fields[name]> }
validateOne of Validate or Array<Validate>
onSubmitA function that accepts a Snapshot
FormFieldOne of BasicField, ArrayField or ObjectField
BasicFieldOne of null, Validate or Array<Validate>
ArrayFieldCreated with array(fields) or array(validate, fields)
ObjectFieldCreated with object(fields) or object(validate, fields)
ValidateA function with the following shape: (x, { form, parents }) => falsy | { id, params }
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
SnapshotAn object with the following shape: { invalid, value, error }
Output
formAn object with the shape: { fields: { [name: string]: FormField } }. Note that form is an ObjectField
submitA function that can be used as onSubmit handler
resetA function that can be used to reset the form
FormFieldOne of BasicField, ArrayField or ObjectField
BasicFieldCan be used with the useFormField hook
ArrayFieldCan be used with the `useArrayFormField hook
ObjectFieldCan 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
nameThe fully qualified name of the form field
stateAn object
- valueThe value of the field
- errorThe validation error for the field
- isSubmittedIndicates if the form was submitted
- isVisitedIndicates if the field has been visited (had focus)
- hasFocusIndicates if the field currently has focus
- invalidThe same as !!error
- showErrorHandy derived boolean to determine when to show an error
eventHandlersAn object
- onBlurHandler for onBlur events
- onFocusHandler for onFocus events
- onChangehandler 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
nameThe fully qualified name of the form field
stateAn object
- childrenThe child fields (these are object type fields)
- errorThe validation error for the field
- isSubmittedIndicates if the form was submitted
- invalidThe same as !!error
- showErrorHandy derived boolean to determine when to show an error
helpersAn object
- addHandler to add a field, accepts an initialValue for the child field
- removeHandler 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
nameThe fully qualified name of the form field
stateAn object
- errorThe validation error for the field
- isSubmittedIndicates if the form was submitted
- invalidThe same as !!error
- showErrorHandy derived boolean to determine when to show an error
fieldsAn object containing the fields

useFormFieldSnapshot

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

const snapshot = useFormFieldSnapshot(form)
Output
snapshotAn object
- invalidBoolean indicating whether the field is invalid
- errorOne of BasicError, ObjectError or ArrayError
- valueThe value of the field
BasicErrorThe result of the validation function
ObjectErrorAn object with the shape: { self, children } where self is a BasicError and children an object with errors
ArrayErrorAn 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
valueThe value of the field.

useFormFieldsValues

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

const values = useFormFieldsValues(fields)
Output
valuesAn 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
valueThe value of the form field
formThe value of the complete form
parentsAn array with the values of the parents (when using object or array fields)
Output
idAn identifier to translate the error into something for people
paramsParameters useful for constructing the validation error

We provide a few commonly used validation functions.

import { ... } from '@kaliber/forms/validation'
requiredReports when the value is 'falsy' and not 0 or false.
optionalAn alias for null, it's there for consistency and readability.
numberReports if the value can not be converted to a number.
min and maxReports if the value is outside of the given value.
minLength and maxLengthReports if the length of the value is outside of the given value.
emailReports if the value does not vaguely look like an email address.
errorUtility 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
renderA function with the following shape: value => React.ReactNode | null. Will render the return value, or null in case this value is undefined.
fieldThe 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
renderA function with the following shape: values => React.ReactNode | null, where values is an array. Will render the return value, or null in case this value is undefined.
fieldsThe 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
renderA function with the following shape: valid => React.ReactNode | null. Will render the return value, or null in case this value is undefined.
fieldThe 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.

2.1.2

2 years ago

2.1.1

3 years ago

2.1.0

4 years ago

2.0.0

5 years ago

1.2.1

5 years ago

1.2.0

5 years ago

1.1.1

5 years ago

1.1.0

5 years ago

1.0.5

5 years ago

1.0.4

5 years ago

1.0.3

5 years ago

1.0.2

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago