Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: $discriminate with runtime check (v2.0.0) #9

Merged
merged 12 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading