Skip to content

Commit

Permalink
feat(listField): Refactor list field (#117)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: listAtom extracted to `@form-atoms/list-atom` which is now a required peer Dependency for `listField()`
BREAKING CHANGE: The `builder` config property from `listField` was renamed to `fields`.
BREAKING CHANGE: The `builder` config property no longer accepts `FieldAtom` as return type. `FormFields` must be returned.
  • Loading branch information
MiroslavPetrik authored Mar 6, 2024
1 parent 4a7e626 commit 13f5745
Show file tree
Hide file tree
Showing 48 changed files with 502 additions and 3,538 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# [4.1.0-next.1](https://github.com/form-atoms/field/compare/v4.0.16...v4.1.0-next.1) (2024-03-06)


### Features

* **listField:** Refactor list field ([#117](https://github.com/form-atoms/field/issues/117)) ([8bc738f](https://github.com/form-atoms/field/commit/8bc738fbd4048c581dd7f1bc8ec845e6dab2ce22)), closes [#116](https://github.com/form-atoms/field/issues/116)

## [4.0.16](https://github.com/form-atoms/field/compare/v4.0.15...v4.0.16) (2024-03-06)


Expand Down
66 changes: 23 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<div align="center">
<img width="180" style="margin: 32px" src="./form-atoms-field.svg">
<h1>Atomic Form Fields for React</h1>
<h1>@form-atoms/field</h1>
</div>

Declarative & headless form fields build on top of [`jotai & form-atoms`](https://github.com/form-atom/form-atoms).
A `zod`-powered [`fieldAtoms`](https://github.com/form-atoms/form-atoms?tab=readme-ov-file#fieldatom) with pre-configured schemas for type & runtime safety.

```
yarn add jotai form-atoms @form-atoms/field
npm install jotai jotai-effect form-atoms @form-atoms/field zod
```

<a aria-label="NPM version" href="https://www.npmjs.com/package/%40form-atoms/field">
Expand All @@ -16,27 +16,14 @@ yarn add jotai form-atoms @form-atoms/field
<img alt="Code coverage" src="https://img.shields.io/codecov/c/gh/form-atoms/field?style=for-the-badge&labelColor=24292e">
</a>

## Motivation
## Features

`form-atoms` is the 'last-mile' of your app's form stack. It has layered, bottom-up architecture with clear separation of concerns.
We provide you with stable pre-fabricated UI fields, while you still can go one level down and take the advantage of form primitives to develop anything you need.
- [x] **Well-typed fields** required & validated by default
- [x] **Initialized field values**, commonly with `undefined` empty value
- [x] **Optional fields** with schema defaulting to `z.optional()`
- [x] **Conditionally required fields** - the required state can depend on other jotai atoms.

To contrast it with formik or react-hook-form, our form state thanks to `jotai` lives outside of the react tree, so you never lose it when the component unmounts.
Moreover, jotai's external state unlike redux-form has compact API with 'atom-local reducer' and automatic dependency tracking leading to unmatched rendering performance.

![architecture](./architecture.png)

### Key differences to other form libraries

#### No 'dotted keypath' access

Some libraries use `path.to.field` approach with field-dependent validation or when reading field at other place. We don't need such paths, as fields can be moved arround in regular JavaScript variables, as they are jotai atoms in reality.

#### Persistent form state by default

With others libraries you often lose form state when your component or page unmounts. Thats because the rendered form hook maintains the store. If the library provides a contextual API, you can opt-in into the persistence, so form state lives even when you unmount the form.

`form-atoms` on the other hand keeps the form state until you clear it, because it lives in jotai atoms. This way, you don't have to warn users about data loss if they navigate out of filled & unsubmitted form. Instead you can display 'continue where you left off' message when they return to the form.
The fields are integrated with the following components:

#### Atomic Components

Expand All @@ -45,7 +32,7 @@ a clickable label which focuses the respective input, or a custom indicator whet

We take care of these details in atomic 'low-level' components like `PlaceholderOption`, `FieldLabel` and `RequirementIndicator` respectively.

#### Generic Native Components
#### Generic Components

With other form libraries you might find yourself repeatedly wiring them into recurring scenarios like checkbox multi select or radio group.
We've created highly reusable generic components which integrate the native components.
Expand All @@ -57,37 +44,30 @@ Lastly to capture a list of objects, you will find the [ListField](https://form-

## Docs

Checkout [our Storybook docs](https://form-atoms.github.io/field/) and for additional primitives see the [`form-atoms` docs](https://github.com/form-atoms/form-atoms).

## Fields
See [Storybook docs](https://form-atoms.github.io/field/)

For well-known field types we export data type specific `fieldAtom` constructors. These come with
pre-defined empty value of `undefined` and a specific zod validation schema.
Similarly to `zod` schema fields, by default all the fieldAtoms are required.

### Usage
### Quick start

```tsx
import { numberField, stringField, Select } from "@form-atoms/field";
import { fromAtom, useForm } from "form-atoms";
import { textField, numberField, stringField, Select } from "@form-atoms/field";
import { fromAtom, useForm, Input } from "form-atoms";
import { z } from "zod";
import { NumberField } from "@form-atoms/flowbite"; // or /chakra-ui

const height = numberField();
const age = numberField({ schema: z.number().min(18) }); // override default schema
const character = stringField().optional(); // make field optional

const personForm = formAtom({ height, age, character });
const personForm = formAtom({
name: textField(),
age: numberField({ schema: z.number().min(18) }); // override default schema
character: stringField().optional(); // make field optional
});

export const Form = () => {
const { submit } = useForm(personForm);
const { fieldAtoms, submit } = useForm(personForm);

return (
<form onSubmit={submit(console.log)}>
<NumberField field={height} label="Height (in cm)" />
<NumberField field={age} label="Your age (min 18)" />
<InputField atom={fieldAtoms.name} label="Your Name" />
<InputField atom={fieldAtoms.age} label="Your age (min 18)" />
<Select
field={character}
field={fieldAtoms.character}
label="Character"
options={["the good", "the bad", "the ugly"]}
getValue={(option) => option}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},
"devDependencies": {
"@emotion/react": "^11.11.3",
"@form-atoms/list-atom": "^1.0.5",
"@picocss/pico": "^2.0.6",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^11.1.0",
Expand Down Expand Up @@ -134,6 +135,7 @@
]
},
"peerDependencies": {
"@form-atoms/list-atom": "^1",
"form-atoms": "^3",
"jotai": "^2",
"jotai-effect": "^0",
Expand Down
23 changes: 9 additions & 14 deletions src/Intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { Meta, Title } from "@storybook/blocks";
style={{ margin: "24px auto" }}
src="./form-atoms-field.svg"
/>
<Title>Atomic Form Fields for React</Title>
<Title>@form-atoms/field</Title>
</div>

## About

Declarative & headless form fields build on top of <a href="https://jotai.org/" target="_blank">Jotai</a>.
A `zod`-powered [fieldAtoms](https://github.com/form-atoms/form-atoms?tab=readme-ov-file#fieldatom) with pre-configured schemas for type & runtime safety.

<div>
<a
Expand All @@ -36,15 +36,13 @@ Declarative & headless form fields build on top of <a href="https://jotai.org/"
## Installation

```bash
npm install jotai jotai-effect form-atoms @form-atoms/field
npm install jotai jotai-effect form-atoms @form-atoms/field zod
```

## Features

- ✅ Pre-configured field atoms, notably two and three state checkbox, multiselect and more.
- ✅ Support for required and optional fields with pre-cofigured field schemas with `zod`.
- ✅ Headless components for an accessible field label & errors.
- ✅ Full-featured & headless List component.
- ✅ RadioControl to switch between selected fields.
- ✅ Hooks to control the file field/input.
- ✅ ...plus everything from <a href="https://github.com/jaredLunde/form-atoms#features" target="_blank">form-atoms</a>.
Expand Down Expand Up @@ -79,19 +77,17 @@ npm install jotai jotai-effect form-atoms @form-atoms/field
| | |
| ------------------------------------------------------------------------- | -------------------------------------------------------------- |
| [FieldErrors](?path=/docs/components-fielderrors--docs) | A headless component providing the field errors. |
| [FieldLabel](?path=/docs/components-fieldlabel--docs) | A headless component with accessible label. |
| [PlaceholderOption](?path=/docs/components-placeholderoption--docs) | A special `<option>` to be rendered in an empty `<Select>`. |
| [RequirementIndicator](?path=/docs/components-requirementindicator--docs) | Displays an indicator whether a field is required or optional. |

#### Native Generic Components

| | |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [CheckboxGroup](?path=/docs/components-checkboxgroup--docs) | Select multiple values from a list of generic options via `<input type="checkbox">`. |
| [List](?path=/docs/components-list--docs) | An advanced headless component to control the [listField()](?path=/docs/fields-listField--docs). |
| [MultiSelect](?path=/docs/components-multiselect--docs) | Select multiple values via `<select multiple>`. |
| [RadioGroup](?path=/docs/components-radiogroup--docs) | Select a generic option via `<input type="radio">`. |
| [Select](?path=/docs/components-select--docs) | Select a generic option via `<select>`. |
| | |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [CheckboxGroup](?path=/docs/components-checkboxgroup--docs) | Select multiple values from a list of generic options via `<input type="checkbox">`. |
| [MultiSelect](?path=/docs/components-multiselect--docs) | Select multiple values via `<select multiple>`. |
| [RadioGroup](?path=/docs/components-radiogroup--docs) | Select a generic option via `<input type="radio">`. |
| [Select](?path=/docs/components-select--docs) | Select a generic option via `<select>`. |

### Hooks

Expand All @@ -101,7 +97,6 @@ npm install jotai jotai-effect form-atoms @form-atoms/field
| [useCheckboxFieldProps](?path=/docs/hooks-usecheckboxfieldprops--docs) | Adapts fields having `boolean` values to controlled checkbox inputs. |
| [useDateFieldProps](?path=/docs/hooks-useDateFieldprops--docs) | Adapts fields having `Date` values to controlled date inputs. |
| [useFilesFieldProps](?path=/docs/hooks-useFilesfieldprops--docs) | Adapts fields having `File[]` values to controlled `input[type=file]`. |
| [useListField](?path=/docs/hooks-useListField--docs) | Manages a `listField()`. |
| [useNumberFieldProps](?path=/docs/hooks-usenumberfieldprops--docs) | Adapts fields having `number` values to controlled numeric inputs. |
| [useOptions](?path=/docs/hooks-useoptions--docs) | A data hook to evaluate which of option(s) is(are) active with respect to a field. |
| [useRequiredProps](?path=/docs/hooks-userequiredprops--docs) | Provides the `required` prop for input based on field optionality. |
Expand Down
29 changes: 0 additions & 29 deletions src/atoms/_useFieldInitialValue.ts

This file was deleted.

35 changes: 35 additions & 0 deletions src/atoms/extendAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Atom, Getter, PrimitiveAtom, atom } from "jotai";

export const extendAtom = <
T extends PrimitiveAtom<any>,
E extends Record<string, unknown>,
>(
baseAtom: T,
makeAtoms: (
cfg: T extends Atom<infer Config> ? Config : never,
get: Getter,
) => E,
) => {
const extended = atom(
(get) => {
const base = get(baseAtom);
return {
...base,
...makeAtoms(
base as T extends Atom<infer Config> ? Config : never,
get,
),
};
},
(get, set, update: T extends Atom<infer Config> ? Config : never) => {
set(baseAtom, { ...get(baseAtom), ...update });
},
);

if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
baseAtom.debugPrivate = true;
extended.debugLabel = baseAtom.debugLabel;
}

return extended;
};
34 changes: 0 additions & 34 deletions src/atoms/extendFieldAtom.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export * from "./list-atom";
export * from "./upload-atom";
export * from "./_useFieldInitialValue";
2 changes: 0 additions & 2 deletions src/atoms/list-atom/index.ts

This file was deleted.

Loading

0 comments on commit 13f5745

Please sign in to comment.