Skip to content

Commit

Permalink
Merge pull request #9 from atmina/discriminatev2
Browse files Browse the repository at this point in the history
feat: `$discriminate` with runtime check (v2.0.0)
  • Loading branch information
mvarendorff2 authored Feb 21, 2024
2 parents 8c821b1 + a2839cd commit 77e0241
Show file tree
Hide file tree
Showing 16 changed files with 2,931 additions and 3,729 deletions.
30 changes: 0 additions & 30 deletions .eslintrc.js

This file was deleted.

24 changes: 12 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
name: build

on:
- push
- pull_request
- push
- pull_request

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn
- run: yarn install
- run: yarn build
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
cache: yarn
- run: yarn install
- run: yarn build
30 changes: 15 additions & 15 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
name: Publish to NPM

on:
release:
types: [created]
release:
types: [created]

jobs:
publish-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
registry-url: https://registry.npmjs.org/
cache: yarn
- run: yarn install
- run: yarn publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
publish-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
registry-url: https://registry.npmjs.org/
cache: yarn
- run: yarn install
- run: yarn publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
24 changes: 12 additions & 12 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
name: test

on:
- push
- pull_request
- push
- pull_request

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn
- run: yarn install
- run: yarn test
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
cache: yarn
- run: yarn install
- run: yarn test
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# build
lib/
dist/

# dependencies
node_modules/

# editor
.idea/
.idea/
25 changes: 25 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
node_modules
build
dist
storybook-static
.next
.turbo
.yarn

package.json

# Lockfiles
package-lock.json
pnpm-lock.yaml
yarn.lock

# Generated files
generated
__generated__
*.generated*

# IDE
.idea
.vscode

.DS_Store
102 changes: 59 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ yarn add @atmina/formbuilder
`FormBuilder` exposes a single hook `useFormBuilder` which is mostly compatible with `useForm` from `react-hook-form`.
It contains an additional member, `fields`, which represents an alternative, object-oriented API on top of React Hook
Form. Each field in the form data can be accessed as a property, including nested fields. The field can be called as a
function to [register](https://react-hook-form.com/api/useform/register/) an input. It also exposes RHF functions via
function to [register](https://react-hook-form.com/api/useform/register/) an input. It also exposes RHF functions via
field-level methods, e.g. `$setValue`. These methods are prefixed with `$` to prevent potential conflicts with form
data members.

```tsx
import { useFormBuilder } from '@atmina/formbuilder';
import {useFormBuilder} from '@atmina/formbuilder';

interface Address {
state: string;
Expand All @@ -31,43 +31,51 @@ interface Address {
}

const App = () => {
const {fields, handleSubmit} = useFormBuilder<{name: string, address: Address}>();

const {fields, handleSubmit} = useFormBuilder<{
name: string;
address: Address;
}>();

const handleFormSubmit = handleSubmit((data) => {
console.log(data);
console.log(data);
});

return (
<form onSubmit={handleFormSubmit}>
<input {...fields.name()} />
<input {...fields.address.city()} />
{/* etc. */}
</form>
);


}
};
```

## Fields

You can create components that encapsulate a single (typed) field by accepting a `FormBuilder<T>` prop where `T` is
You can create components that encapsulate a single (typed) field by accepting a `FormBuilder<T>` prop where `T` is
the type of the field. We like to call this `on` or `field`, but you are free to name it however you like.

```tsx
import { FC } from "react";
import {FC} from 'react';

const TextField: FC<{on: FormBuilder<string>, label: string}> = ({on: field}) => {
return <div>
<label>
<span>{label}</span>
<input type="text" {...field()} />
</label>
<button type="button" onClick={() => field.$setValue(getRandomName())}>
Randomize
</button>
</div>
}
const TextField: FC<{on: FormBuilder<string>; label: string}> = ({
on: field,
}) => {
return (
<div>
<label>
<span>{label}</span>
<input type='text' {...field()} />
</label>
<button
type='button'
onClick={() => field.$setValue(getRandomName())}
>
Randomize
</button>
</div>
);
};
```

The field component would be used like this:
Expand All @@ -86,15 +94,17 @@ You can create components which encapsulate a group of related fields, such as a
composition, letting you piece together complex data structures and adding a lot of reusability to your forms.

```tsx
import { FC } from "react";
import {FC} from 'react';

const AddressSubform: FC<{field: FormBuilder<Address>}> = ({field}) => {
return <div>
<TextField label="State" field={field.state} />
<TextField label="City" field={field.city} />
{/* etc. */}
</div>
}
return (
<div>
<TextField label='State' field={field.state} />
<TextField label='City' field={field.city} />
{/* etc. */}
</div>
);
};
```

## Field arrays
Expand Down Expand Up @@ -127,27 +137,34 @@ wrap the primitive in an object, or use a controller (`$useController`) to imple

For more information, see the React Hook Form docs on [`useFieldArray`](https://react-hook-form.com/docs/usefieldarray).

## Discriminating unions
## Discriminated unions

In case of a form that contains fields with object unions, the `$discriminate()` function may be used to narrow the type
using a specific member like this:

```tsx
import { FC } from 'react';

type DiscriminatedForm =
| { __typename: 'foo'; foo: string; }
| { __typename: 'bar'; bar: number; }

const DiscriminatedSubform: FC<{field: FormBuilder<DiscriminatedForm>}> = ({field}) => {
const fooForm = field.$discriminate('__typename', 'foo');

return <input {...fooForm.foo()} />;
import {FC} from 'react';

type DiscriminatedForm =
| {__typename: 'foo'; foo: string}
| {__typename: 'bar'; bar: number};

const DiscriminatedSubform: FC<{field: FormBuilder<DiscriminatedForm>}> = ({
field,
}) => {
const [typename, narrowed] = field.$discriminate('__typename');

switch (typename) {
case 'foo':
return <input {...narrowed.foo()} />;
case 'bar':
// ...
}
};
```

> [!IMPORTANT]
> `$discriminate` currently does **not** perform any runtime checks, it's strictly used for type narrowing at this time.
This returns the current value of the discriminator as well as a `FormBuilder` that is automatically narrowed when
the discriminator is checked, for example in a `switch` or `if` block.

## Compatibility with `useForm`

Expand All @@ -156,7 +173,6 @@ Currently, `useFormBuilder` is almost compatible with `useForm`. This means you
`useFormBuilder`. However, future versions of the library may see us diverging further from `useForm` in an effort to
streamline this API and increase its type-safety.


## License

MIT
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @type {import('eslint').Linter.FlatConfig[]}
*/
module.exports = [
...require('@atmina/linting/eslint/recommended'),
require('@atmina/linting/eslint/react'),
];
5 changes: 2 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
preset: 'ts-jest',
testEnvironment: 'jsdom'
};
Loading

0 comments on commit 77e0241

Please sign in to comment.