Socket
Socket
Sign inDemoInstall

formula-one

Package Overview
Dependencies
4
Maintainers
4
Versions
44
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    formula-one

Strongly-typed React form state management


Version published
Weekly downloads
3.6K
increased by4.78%
Maintainers
4
Install size
490 kB
Created
Weekly downloads
 

Readme

Source

formula-one

formula-one is a library which makes it easier to write type-safe forms with validations and complex inputs.

Yarn Test

A minimal example, with no validation

Edit the working example on CodeSandbox

// @flow strict

import React from "react";
import {Form, Field, ObjectField, FeedbackStrategies} from "formula-one";

type Person = {
  name: string,
  age: string,
  faction: "Empire" | "Rebels",
};

const EMPTY_PERSON: Person = {
  name: "",
  age: "",
  faction: "Empire",
};

export default function SimpleExample() {
  return (
    <div className="App">
      <Form
        initialValue={EMPTY_PERSON}
        onSubmit={person => console.log("Submitted", person)}
      >
        {(link, onSubmit) => (
          <ObjectField link={link}>
            {links => (
              <>
                <Field link={links.name}>
                  {(value, errors, onChange) => (
                    <label>
                      <div>Name</div>
                      <input
                        type="text"
                        value={value}
                        onChange={e => onChange(e.target.value)}
                      />
                    </label>
                  )}
                </Field>
                <Field link={links.age}>
                  {(value, errors, onChange) => (
                    <label>
                      <div>Age</div>
                      <input
                        type="text"
                        onChange={e => onChange(e.target.value)}
                        value={value}
                      />
                    </label>
                  )}
                </Field>
                <Field link={links.faction}>
                  {(value, errors, onChange) => (
                    <label>
                      <div>Faction</div>
                      <select
                        onChange={e => onChange(e.target.value)}
                        value={value}
                      >
                        <option value="Empire">Empire</option>
                        <option value="Rebels">Rebels</option>
                      </select>
                    </label>
                  )}
                </Field>

                <div>
                  <button onClick={onSubmit}>Submit</button>
                </div>
              </>
            )}
          </ObjectField>
        )}
      </Form>
    </div>
  );
}

Philosophy

formula-one helps you write forms in React by managing the state of your form and ensuring your inputs are the right type. It does this by introducing a new abstraction, called a Field. A Field wraps some value and provides a way to render and edit that value. A simple Field might wrap a string, which displays and edits its value through an <input type="text">. A more complex value, such as a date and time might be displayed as an ISO 8601 string and be edited through a calendar input.

Fields are specified using the <Field> component, which wraps your input using a render prop. It provides the value, errors, onChange, and onBlur handlers, which should be hooked up to your input.

Individual Fields are aggregated into objects and arrays using the <ObjectField> and <ArrayField> components. These components enable you to build forms with multiple fields.

In formula-one, all of the form's state is held in the <Form> component, and communicated to its internal Fields via opaque props called links. These links contain all of the data and metadata used to render an input and its associated errors.

Validations

Simple Validation

formula-one provides an api for specifying validations on Fields. Each Field exposes a validation props, which has the type T => Array<string> for a Field of type T. Each string represents an error message, and the empty array indicates no errors.

An example of a Field<string> which doesn't allow empty strings:

Edit the working example on CodeSandbox

function noEmptyStrings(s: string): Array<string> {
  if (s === "") {
    return ["Cannot be empty"];
  }
  return [];
}

<Field link={link} validation={noEmptyStrings}>
  {(value, errors, onChange) => (
    <>
      <label>
        <div>Name</div>
        <input
          type="text"
          value={value}
          onChange={e => onChange(e.target.value)}
        />
        {errors ? (
          <ul className="error">
            {errors.map(e => (
              <li key={e}>{e}</li>
            ))}
          </ul>
        ) : null}
      </label>
    </>
  )}
</Field>;

When to show errors

In addition to tracking errors and validating when inputs change, formula-one tracks metadata to help you decide whether you should show errors to your user. <Form> allows you to specify a strategy for when to show errors.

Some base strategies are exported as fields on the FeedbackStrategies object. Here is a table of the strategies and their behavior.

Strategy identifierStrategy Behavior
FeedbackStrategies.AlwaysAlways show errors (default)
FeedbackStrategies.TouchedShow errors for fields which have been touched (changed or blurred)
FeedbackStrategies.BlurredShow errors for fields which have been blurred
FeedbackStrategies.ChangedShow errors for fields which have been changed
FeedbackStrategies.ClientValidationSucceededShow errors for fields which have had their validations pass at any time in the past
FeedbackStrategies.PristineShow errors when the form has not been modified
FeedbackStrategies.SubmittedShow errors after the form has been submitted

These simple strategies can be combined by using the and, or, and not functions also on the FeedbackStrategies object, as follows:

import {FeedbackStrategies} from "formula-one";
const {Changed, Blurred, Submitted, or, and} = FeedbackStrategies;
const strategy = or(and(Changed, Blurred), Submitted);

Multiple validations for a single <Field>

To specify multiple validations for a single field, simply run the validations in sequence and serialize their errors into a single array.

Edit the working example on CodeSandbox

function validate(s: string): Array<string> {
  return [noEmptyStrings, mustHaveNumbers, noLongStrings].flatMap(validation =>
    validation(s)
  );
}

Validations on aggregations of Fields

Both <ObjectField> and <ArrayField> allow a validation to be specified. You can use the <ErrorsHelper> component to extract the errors from the link.

Complex inputs

Even inputs which are complex (e.g. a datepicker) can be wrapped in a <Field> wrapper, but validations are tracked at the field level, so you won't be able to use formula-one to track changes and validations below the field level. For example, you can't represent a validation error for just the day part of a date if you only have a single <Field> wrapping a datepicker. Instead, the error will be associated with the entire date.

Common use cases

Arrays in forms

Often, you may want to edit a list of items in a form. formula-one exposes an aggregator called <ArrayField>, which allows you to manipulate a list of Fields.

For example, imagine you have a form for a person, who has a name, but also some number of pets, who each have their own name.

Edit the working example on CodeSandbox

type Person = {
  name: string,
  pets: Array<{
    name: string,
  }>,
};

const emptyPerson: Person = {
  name: "",
  pets: [],
};

export default function () {
  return (
    <div className="App">
      <Form
        initialValue={emptyPerson}
        onSubmit={p => console.log("Submitted", p)}
      >
        {(link, onSubmit) => (
          <ObjectField link={link}>
            {links => (
              <>
                <Field link={links.name}>
                  {(value, errors, onChange) => (
                    <label>
                      <div>Name</div>
                      <input
                        type="text"
                        onChange={e => onChange(e.target.value)}
                        value={value}
                      />
                    </label>
                  )}
                </Field>
                <ArrayField link={links.pets}>
                  {(links, {addField}) => (
                    <ul>
                      {links.map((link, i) => (
                        <ObjectField key={i} link={link}>
                          {link => (
                            <Field link={link.name}>
                              {(value, errors, onChange) => (
                                <li>
                                  Pet #{i + 1}
                                  <input
                                    type="text"
                                    value={value}
                                    onChange={e => onChange(e.target.value)}
                                  />
                                </li>
                              )}
                            </Field>
                          )}
                        </ObjectField>
                      ))}
                      {links.length === 0 ? "No pets :(" : null}
                      <button
                        onClick={() => addField(links.length, {name: ""})}
                      >
                        Add pet
                      </button>
                    </ul>
                  )}
                </ArrayField>
                <div>
                  <button onClick={onSubmit}>Submit</button>
                </div>
              </>
            )}
          </ObjectField>
        )}
      </Form>
    </div>
  );
}

<ArrayField> exposes both an array of links to the array elements, but also an object containing mutators for the array:

  • addField(index: number, value: E): Add a field at a position in the array
  • addFields(spans: $ReadOnlyArray<Span<E>>): Add multiple fields to an array
  • removeField(index: number): Remove a field at a position in array
  • filterFields(predicate: (item: E, index: number) => boolean): Remove multiple fields from an array
  • modifyFields({insertSpans: $ReadOnlyArray<Span<E>>, filterPredicate: (item: E, index: number) => boolean}): Simultaneously add and remove fields from an array
  • moveField(fromIndex: number, toIndex: number): Move a field in an array (preserves metadata for the field)

where Span<E> is a range to be inserted at an index: [number, $ReadOnlyArray<E>].

Form in a modal

Oftentimes, when you need to wrap a component which has a button you will use for submission, you can simply wrap that component with your <Form> element. The <Form> does not render any elements, so it will not affect your DOM hierarchy.

Example:

<Form>
  {(link, handleSubmit) => (
    <Modal buttons={[<button onClick={handleSubmit}>Submit</button>]}>
      <MyField link={link}>...</MyField>
    </Modal>
  )}
</Form>

Custom changes

Sometimes a change in one field has to be reflected in another field. <ObjectField> and <ArrayField> have a prop customChange to allow this. It will be called when a child Field changes, and by returning a non-null result you can override the whole Currently, no metadata is preserved (all fields are marked changed, blurred, and touched) if a customChange function is used.

The API is:

// Override nextValue by returning a non-null result
customChange: <T>(prevValue: T, nextValue: T) => null | T;

Edit the working example on CodeSandbox

const SHIPS = {
  "X-Wing": {faction: "Rebels", name: "X-Wing"},
  "Y-Wing": {faction: "Rebels", name: "Y-Wing"},
  "TIE Fighter": {faction: "Empire", name: "TIE Fighter"},
  "TIE Defender": {faction: "Empire", name: "TIE Defender"},
};

type Person = {
  name: string,
  age: string,
  faction: "Empire" | "Rebels",
  ship: string,
};

function ensureFactionShipConsistency(
  prevPerson: Person,
  person: Person
): null | Person {
  const ship = SHIPS[person.ship];
  if (person.faction !== ship.faction) {
    // person's ship is inconsistent with their faction: need an update
    if (prevPerson.ship !== person.ship) {
      // ship changed; update faction to match
      return {...person, faction: ship.faction};
    } else if (prevPerson.faction !== person.faction) {
      // faction changed; give them a ship from their new faction
      const newShip = Object.keys(SHIPS).find(
        x => SHIPS[x].faction === person.faction
      );
      return {...person, ship: newShip};
    } else {
      throw new Error("unreachable");
    }
  } else {
    return null;
  }
}

export default function CustomChange() {
  return (
    <div className="App">
      <Form
        initialValue={EMPTY_PERSON}
        onSubmit={person => console.log("Submitted", person)}
      >
        {(link, onSubmit) => (
          <ObjectField link={link} customChange={ensureFactionShipConsistency}>
            {links => (
              <>
                <Field link={links.faction}>{/*...*/}</Field>
                <Field link={links.ship}>{/*...*/}</Field>

                <div>
                  <button onClick={onSubmit}>Submit</button>
                </div>
              </>
            )}
          </ObjectField>
        )}
      </Form>
    </div>
  );
}

External validation

Oftentimes, you will want to show errors from an external source (such as the server) in your form alongside any client-side validation errors. These can be passed into your <Form> component using the externalErrors prop.

These errors must be in an object with keys representing the path to the field they should be associated with. For example, the errors:

const externalErrors = {
  "/": "User failed to save!",
  "/email": "A user with this email already exists!",
};

could be used in this form:

<Form externalErrors={externalErrors}>
  {(link, handleSubmit) => (
    <>
      <ObjectField link={link}>
        {links => (
          <>
            <StringField link={links.name} />
            <StringField link={links.email} />
          </>
        )}
      </ObjectField>
      <ErrorsHelper link={link}>
        {({shouldShowErrors, flattened}) =>
          shouldShowErrors ? (
            <ul className="form-level-error">
              {flattened.map(errorMessage => (
                <li key={errorMessage}>{errorMessage}</li>
              ))}
            </ul>
          ) : null
        }
      </ErrorsHelper>
      <button onClick={handleSubmit}>Submit</button>
    </>
  )}
</Form>

Advanced usage

Additional information in render prop

Additional information is available in an object which is the last argument to the <Form>, <ObjectField>, <ArrayField>, and <Field> components' render props. This object contains the following information:

keytypedescription
touchedbooleanWhether the field has been touched (blurred or changed)
changedbooleanWhether the field has been changed
blurredbooleanWhether the field has been blurred
shouldShowErrorsbooleanWhether errors should be shown according to the current feedback strategy
unfilteredErrors$ReadOnlyArray<string>All validation errors for the current field. (This differs from the errors argument in <Field>, since the errors argument in <Field> will be empty if shouldShowErrors is false)
validbooleanWhether the field (and its children) pass their validations (NOTE: only client errors are considered!)
asyncValidationInFlightbooleanWhether there is an asynchronous validation in progress for this field
valueTThe current value for this field. (This will always match the value argument to <Field>)

An example of how these data could be used:

<Form onSubmit={handleSubmit}>
  {(link, handleSubmit, {valid}) => (
    <>
      <Field link={link}>
        {(value, errors, onChange, onBlur, {changed}) => (
          <label>
            Name
            <input
              type="text"
              value={value}
              onChange={onChange}
              onBlur={onBlur}
            />
            {changed ? "(Modified)" : null}
          </label>
        )}
      </Field>
      <button disabled={!valid} onClick={() => handleSubmit()}>
        Submit
      </button>
    </>
  )}
</Form>

Multiple submission buttons

Sometimes, you need to have multiple submission buttons and need to know which button was clicked in your onSubmit prop callback. This can be achieved by passing additional information as an argument to the handleSubmit argument to your <Form>'s render prop. This argument will be passed to your onSubmit prop callback as a second argument. If your onSubmit prop callback is typed to make this extra data mandatory, the inner handleSubmit callback will require that data.

Example:

function handleSubmit(value: User, saveOrSubmit: "save" | "submit") {
  if (saveOrSubmit === "save") {
    // ...
  } else if (saveOrSubmit === "submit") {
    // ...
  }
}

<Form onSubmit={handleSubmit}>
  {(link, handleSubmit) => (
    <>
      <UserField link={link} />
      <div>
        <button onClick={() => handleSubmit("save")}>Save</button>
        <button onClick={() => handleSubmit("submit")}>Submit</button>
      </div>
    </>
  )}
</Form>;

Submitting forms externally

It is easy to submit a formula-one form using the handleSubmit argument provided to <Form>'s render prop, but sometimes you need to submit a <Form> from outside. This is possible using the submit() method available on <Form> along with a React ref to that <Form> element. This submit() method can also receive additional user-specified information, as stated above.

function handleSubmit(value) {
  /* ... */
}

class MyExternalButtonExample extends React.Component<Props> {
  form: null | React.Element<typeof Form>;

  constructor(props: Props) {
    super(props);

    this.form = null;
    this.handleSubmitClick = this.handleSubmitClick.bind(this);
  }

  handleSubmitClick() {
    if (this.form != null) {
      this.form.submit();
    }
  }

  render() {
    return (
      <div>
        <Form
          ref={f => {
            this.form = f;
          }}
          onSubmit={handleSubmit}
        >
          {link => <UserField link={link} />}
        </Form>
        <button onClick={this.handleSubmitClick}>Submit</button>
      </div>
    );
  }
}

Frequently Asked Questions

Is it possible to reset a form to a new initial state?

Normally, after the initial render, a <Form> component will ignore changes to its initialState prop. This is necessary because the <Form> component tracks metadata in addition to the any updates to the value of the form, and it must maintain that data as the user edits the form. If you want to start over with a new initial state and count all fields as unchanged and unblurred, you can provide a key prop to your <Form>. When this key changes, React will discard the state of your form and <Form> will be initialized with the provided initialState.

FAQs

Last updated on 23 Jan 2023

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc