Skip to content
form-atomsPublic

Repository files navigation

@form-atoms/list-atom

The Jotai 👻 form-atoms list extension.

npm install @form-atoms/list-atom jotai-effect
Bundlephobia NPM Version Code coverage Storybook

Features

  • 🏎️ Performant rendering. Each list item is rendered with a stable key derived from Jotai atom.
  • 🎮 Easy controls. The <List.Item> provides add, remove, moveUp and moveDown actions for each item.
  • 🧩 Compound JSX Components. Use the <List.Add />, or <List.Empty /> instead of manual hooks.
  • 🔑 Scoped field names. The fields are dynamically indexed enabling serialization to FormData.
  • 🐢 Lists all the way down. Render lists within lists within lists... with the <List.Of>.

Quick start 🎨 Demo

import { formAtom, useForm, fieldAtom, InputField } from "form-atoms";
import { listAtom, createList } from "@form-atoms/list-atom";

const envVars = listAtom({
  name: "envVars",
  fields: () => ({
    variable: fieldAtom({ name: "variable", value: "" }),
    value: fieldAtom({ name: "value", value: "" }),
  }),
});

const form = formAtom({ envVars });

const { List } = createList(envVars);

export const Form = () => {
  const { submit } = useForm(form);

  return (
    <form onSubmit={submit(console.log)}>
      <List
        initialValue={[
          { variable: "PACKAGE_NAME", value: "form-atoms" },
          { variable: "APP_URL", value: "https://jotai.org" },
        ]}
      >
        <List.Item>
          {({ fields, remove }) => (
            <>
              <InputField atom={fields.variable} component="input" />
              <InputField atom={fields.value} component="input" />
              <button type="button" onClick={remove}>
                Remove
              </button>
            </>
          )}
        </List.Item>
        <List.Add>
          {({ add }) => (
            <button type="button" onClick={() => add()}>
              Add variable
            </button>
          )}
        </List.Add>
      </List>
    </form>
  );
};

Table of contents

Atoms Description
listAtom() An atom that represents a list of form fields in a form. It manages state for the list, including the name, value, errors, dirty, validation and empty state.
Hooks Description
useListActions() A hook that returns a add, remove & move actions, that can be used to interact with the list atom state.
useList() A hook that returns the list items ready to be rendred together with the list actions.
Components Description Demo
createList(listAtom) A function to create components bound to the listAtom.
<List> A component to initialize the listAtom via initialValue prop. 🎨
<List.Add> Adds new or initialized items to the list. 🎨
<List.Empty> Render children only when the list has no items. Also accepts a render prop with isEmpty, count, isFull, and max. 🎨
<List.Item> Iterate and render each of the list items. 🎨
<List.Of> Render a nested list within a <List.Item>. 🎨

List atoms

listAtom()

An atom that represents a list of form fields in a form. It manages state for the list, including the name, value, errors, dirty, validation, count and empty state.

Arguments

Name Type Required? Description
config ListAtomConfig<Fields, Value> Yes The initial state and configuration of the field.

ListAtomConfig

export type ListAtomConfig<Fields extends FormFields, Value> = {
  /**
   * Optionally provide a name for the field that will be added
   * prefixed to inner fields
   * E.g. list name "contacts" and field name "email"
   * will have scoped name for the 4th item "contacts[3].email"
   */
  name?: string;
  /**
   * The initial array of values of the list
   */
  value: Value[];
  /**
   * A function to initialize the fields for each of the initial values.
   */
  fields: (value: Value) => Fields;
  /**
   * Error message the listAtom will have, when its items have nested errors.
   * It will be one of errors returned by the `useFieldErrors()` hook.
   */
  invalidItemError?: string;
  /**
   * A function that validates the value of the field any time
   * one of its atoms changes. It must either return an array of
   * string error messages or undefined. If it returns undefined,
   * the validation is "skipped" and the current errors in state
   * are retained.
   */
  validate?: (state: {
    /**
     * A Jotai getter that can read other atoms
     */
    get: Getter;
    /**
     * The current value of the field
     */
    value: Value;
    /**
     * The dirty state of the field
     */
    dirty: boolean;
    /**
     * The touched state of the field
     */
    touched: boolean;
    /**
     * The event that caused the validation. Either:
     *
     * - `"change"` - The value of the field has changed
     * - `"touch"` - The field has been touched
     * - `"blur"` - The field has been blurred
     * - `"submit"` - The form has been submitted
     * - `"user"` - A user/developer has triggered the validation
     */
    event: ValidateOn;
  }) => void | string[] | Promise<void | string[]>;
};

Returns

An extended FieldAtom:

export type ListAtom<Fields extends FormFields, Value> = ExtendFieldAtom<
  Value[],
  {
    /**
     * An atom holding the total count of items in the list.
     */
    count: Atom<number>;
    /**
     * An atom indicating whether the list is empty.
     */
    empty: Atom<boolean>;
    /**
     * An atom holding the maximum number of items allowed in the list.
     */
    max: PrimitiveAtom<number>;
    /**
     * An atom indicating whether the list has reached its maximum capacity.
     */
    full: Atom<boolean>;
    /**
     * A splitAtom() instance from jotai/utils.
     * It handles adding, removing and moving of items in the list.
     * @internal
     */
    _splitList: WritableAtom<
      SplitListItem<Fields>[],
      [SplitAtomAction<ListItemForm<Fields>>],
      void
    >;
    /**
     * An atom holding the list of forms of each item.
     * @internal
     */
    _formList: WritableAtom<
      ListItemForm<Fields>[],
      [typeof RESET | SetStateAction<ListItemForm<Fields>[]>],
      void
    >;
    /**
     * An atom holding the fields of the internal formAtom of each item.
     * @internal
     */
    _formFields: Atom<Fields[]>;
    buildItem(): ListItemForm<Fields>;
  }
>;

Hooks

useListActions()

A hook that returns a add, remove & move actions, that can be used to interact with the list atom state. Moreover you can get recently aded item's fields with the getItemFields.

Arguments

Name Type Required? Description
listAtom ListAtom<Fields extends FormFields, Value> Yes The atom that stores the list's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

export type UseListActions<Fields extends FormFields, Value> = {
  /**
   * Removes the item from the list.
   *
   * @param item - An item from the listAtom's splitList array.
   */
  remove: (item: SplitListItem<Fields>) => void;
  /**
   * Appends a new item to the list by default, when no 'before' position is used.
   * Optionally the item can be initialized, with the 'fields' argument.
   *
   * @param before - An item from the listAtom's splitList array.
   * @param value - A custom list item value.
   * @returns The created ListItemForm<Fields>
   */
  add: (
    before?: SplitListItem<Fields> | undefined,
    value?: Value | undefined,
  ) => ListItemForm<Fields>;
  /**
   * Moves the item to the end of the list, or where specified when the 'before' is defined.
   *
   * @param item - A splitList item to be moved.
   * @param before - A splitList item before which to place the moved item.
   */
  move: (
    item: SplitListItem<Fields>,
    before?: SplitListItem<Fields> | undefined,
  ) => void;
  /**
   * Retrieves the fields of a given list item form.
   *
   * @param listItem - A list item form from which to get the fields.
   * @returns The fields of the specified list item form.
   */
  getItemFields: (listItem: ListItemForm<Fields>) => Fields;
};

useList()

A hook that returns the list items ready to be rendred together with the list actions.

Arguments

Name Type Required? Description
listAtom ListAtom<Fields extends FormFields, Value> Yes The atom that stores the list's state
options UseAtomOptions No Options that are forwarded to the useAtom, useAtomValue, and useSetAtom hooks

Returns

export type UseList<Fields extends FormFields, Value> = UseListActions<
  Fields,
  Value
> & {
  /**
   * Resolved value from the list.empty atom.
   */
  isEmpty: boolean;
  /**
   * Resolved value from the list.count atom.
   */
  count: number;
  items: {
    /**
     * The item from the internal splitList.
     */
    item: SplitListItem<Fields>;
    /**
     * Stable React key prop derived from atom id.
     */
    key: string;
    /**
     * The form fields of the current item.
     */
    fields: Fields;
    /**
     * A function to remove the current item from the list.
     */
    remove: () => void;
    /**
     * A helper function to move the item to the previous index in the list.
     */
    moveUp: () => void;
    /**
     * A helper function to move the item to the next index in the list.
     */
    moveDown: () => void;
  };
};

createList(listAtom)

Create a compound List components:

import { createList } from "@form-atoms/list-atom";

const { List } = createList(myListAtom);

// Usage:
// List.Add
// List.Empty
// List.Item
// List.Of

Returns

export type ListComponents<Fields extends FormFields> = {
  /**
   * A component to initialize the listAtom value.
   */
  List: FunctionComponent<ListProps<FormFieldValues<Fields>>> & {
    /**
     * A component to control adding new or initialized items to the list.
     */
    Add: FunctionComponent<AddButtonProps<Fields>>;
    /**
     * A component which renders children only when the list is empty.
     */
    Empty: FunctionComponent<EmptyProps>;
    /**
     * A component to iterate and render each of the list items.
     */
    Item: FunctionComponent<ItemProps<Fields>>;
    /**
     * A component to create these ListComponents for a nested listAtom within a <List.Item>
     */
    Of: <Fields extends FormFields>(props: ListOfProps<Fields>) => ReactNode;
  };
};

<List>

🎨 Storybook

Props

Name Type Required? Description
children ReactNode Yes A react nodes
initialValue Value[] No A value to initialize the listAtom
max number No The maximum number of items allowed in the list.
store AtomStore No A Jotai store

<List.Add>

🎨 Storybook

Props

Name Type Required? Description
children (props: AddChildrenProps) => JSX.Element No A render prop

Children Props

type AddChildrenProps<Fields extends FormFields> = {
  /**
   * An action to append a new item to the end of the list.
   * @param fields optionaly set the items initial value.
   * @returns The created ListItemForm<Fields>
   */
  add: (value?: FormFieldValues<Fields>) => ListItemForm<Fields>;
  /**
   * Retrieves the fields of a given list item form.
   *
   * @param listItem - A list item form from which to get the fields.
   * @returns The fields of the specified list item form.
   */
  getItemFields: (listItem: ListItemForm<Fields>) => Fields;
};

<List.Empty>

🎨 Storybook

Props

Name Type Required? Description
children ReactNode No Content to render when the list is empty
children (props: EmptyChildrenProps) => ReactNode No A render prop

Children Props

type EmptyChildrenProps = {
  /**
   * Indicates whether the list has no items.
   */
  isEmpty: boolean;
  /**
   * Total count of items in the list.
   */
  count: number;
  /**
   * Indicates whether the list has reached its maximum capacity.
   */
  isFull: boolean;
  /**
   * The maximum number of items allowed in the list.
   */
  max: number;
};

<List.Item>

🎨 Storybook

Props

Name Type Required? Description
children (props: ListItemProps) => JSX.Element Yes A render prop

Children Props

type ListItemProps<Fields extends FormFields> = {
  /**
   * The fields of the current item, as created with the listAtom's `fields` config function.
   */
  fields: Fields;
  /**
   * The item from the internal splitList.
   */
  item: SplitListItem<Fields>;
  /**
   * The index of the current item.
   */
  index: number;
  /**
   * Total count of items in the list.
   */
  count: number;
  /**
   * Append a new item to the list.
   * When called with the current item, it will prepend it.
   * @returns The created ListItemForm<Fields>
   */
  add: (
    before?: SplitListItem<Fields>,
    value?: FormFieldValues<Fields>,
  ) => ListItemForm<Fields>;
  /**
   * Removes the current item from the list.
   */
  remove: () => void;
  /**
   * Moves the current item one slot up in the list.
   * Supports carousel - the first item will become the last.
   */
  moveUp: () => void;
  /**
   * Moves the current item one slot down in the list.
   * Supports carousel - the last item will become the first.
   */
  moveDown: () => void;
};

<List.Of>

🎨 Storybook

Props

Name Type Required? Description
atom ListAtom<Fields> Yes A listAtom for which to create nested compound components
children (props: Components) => JSX.Element Yes A render prop with a List compound component bound to the listAtom

Let's see what's in this listAtom

atom in atoms

LICENSE

MIT

Releases

Packages

Used by

Contributors

Languages