npm package: @react-noui/create-form
Description: React form context/provider
Author: taystack
Latest version: 1.0.2

# create-form
[![gzip size](https://img.badgesize.io/https://unpkg.com/react-noui/create-form?compression=gzip&amp;style=flat-square)](https://unpkg.com/react-noui/create-form)
[![npm version](https://img.shields.io/npm/v/react-noui/create-forms.svg?style=flat-square)](https://www.npmjs.com/package/react-noui/create-form)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)

create-form is a declarative, typescript-driven form control library. create-form allows you to quickly apply setters/getters/validators within atomic, props-less components by following React's Context API.

This README was generated by [anansi](https://github.com/ntucker/anansi/tree/master/packages/generator-js#readme).

## Installation
```bash
yarn add @react-noui/create-form
```

# Create form

This library employs the factory pattern when creating the data-layer for your form controls.
If you are familiar with `React.createContext` then you will be familiar with how to use this library.

## Controlled

### `const MyForm = createForm<T>(options? = {})`
```typescript
import { createForm } from '@react-noui/create-form';

type Login = {
  email: string;
  password: string;
  session: boolean;
}

const LoginForm = createForm<Login>()
// -> { Context, Provider }
```

### Options
#### Props
##### `options.props: Record<K = keyof T, DetailedHTMLProps<HTMLInputElement>>`
These props map directly to the desired field. `Props` can be any semantic `HTMLInputElement` propery.
```typescript
const LoginForm = createForm<Login>({
  props: {
    email: {
      type: 'email',
      placeholder: 'Account email',
    },
    password: {
      type: 'password',
      placeholder: 'Account password',
    },
  },
});
```

#### Validation
##### `options.validate: Record<K = keyof T, (value: T[K], values: T) => string | undefined>`
Provide custom validation methods upon receiving changes to key `K` of provided type `T`.
The keys allowed in your `validate` option must match the keys you provided from type `T`,
but are not required.

For the `LoginForm` example, we can provide options to validate `email | password`:
```typescript
const LoginForm = createForm<Login>({
  validate: {
    email: (value) => !value.length ? 'Email cannot be empty' : undefined,
    password: (value) => !value.length ? 'Password cannot be empty' : undefined,
  },
});
```

## Uncontrolled
If you don't want to use controlled inputs, you can create an uncontrolled form. Most of the controlled APIs do not exist for uncontrolled forms due to the nature of uncontrolled inputs.
Also, `options.validate` is not possible because `onChange` events are no longer run against the individual inputs.

### `const MyFormUncontrolled = createFormUncontrolled<T>(options? = {})`
```typescript
import { createFormUncontrolled } from '@react-noui/create-form';
const LoginForm = createForm<Login>({
  props: {...}
})
// -> { Context, Provider }
```

# Hooks
## Controlled
### `useForm(LoginForm)`
Short-cut access to the `LoginForm` APIs. This should be composed within `LoginForm.Provider`.
```typescript
function LoginFormConsumer() {
  const form = useForm(LoginForm)
  // ...
}
```
|**attribute**|**type**|**effect**|
|---|---|---|
|`form.reset(field)`|`(form[FIELD]) => void`|Resets `form[FIELD]` values, errors, files, etc. Resets `form[FIELD].current`to the `defaultValues[field.name]` for `field`.|
|`form.resetAll()`|`() => void`|Resets `form[FIELD]` values, errors, files, etc. Resets `form[FIELD].current`to the `defaultValues` for all fields.|
|`form.toJSON()`|`() => void`|Returns JSON format matching shape `T`|
|`form.toFormData()`|`() => FormData`|Obtain `FormData` for use with http request/fetch `Content-Type: application/x-www-form-urlencoded`.|
|`form.toURLSearchParams()`|`() => string`|Returns query string with url-encoded form fields|
|`form.options`|`{props: {...}, validate: {...}}`|Returns the `options` used in `createForm(options)`|

## Uncontrolled
### `useFormUncontrolled(LoginForm)`
Short-cut access to the **Uncontrolled** `LoginForm` APIs. This should be composed within `LoginForm.Provider`.
```typescript
function LoginFormConsumer() {
  const form = useFormUncontrolled(LoginForm)
  // ...
}
```
|**attribute**|**type**|**effect**|
|---|---|---|
|`form.options`|`{props: {...}}`|Returns the `options` used in `createFormUncontrolled(options)`|

## Provider `LoginForm.Provider`
This behaves like `React.createContext(...).Provider` such that access to `LoginForm` values can only be made from components within this provider.

```tsx
const LOGIN_DEFAULTS: Login = {
  email: '',
  password: '',
  session: false,
}
function LoginFormPage() {
  return (
    <LoginForm.Provider defaultValues={LOGIN_DEFAULTS}>
      {...components}
    </LoginForm.Provider>
  )
}
```

#### Provider prop `defaultValues: T`
Where `T extends Record<string, string | number | boolean>`. This library currently supports primitive values. `HTML` change events typically involve primitive values.

This will be the default values we use in our form as type `T`. In the example above, `T` is of type `Login`.

### `LoginForm.Context`
This behaves like `React.createContext(...)` such that access to the `LoginForm` APIs can be made.
```tsx
// Controlled inputs
function LoginFormEmailComponent() {
  const form = React.useContext(LoginForm.Context);
  return (
    <>
      <label htmlFor={form.email.name}>Email</label>
      <input
        id={form.email.name}
        name={form.email.name}
        value={form.email.value}
        onChange={(event) => form.set.email(event.target.value)}
      />
      {form.email.error && <div>{form.email.error}</div>}
    </>
  )
};
```

### `const field = useContext(MyForm.Context)[FIELD]`
Each field defined in `type T` is accessible via `form[FIELD]`
|**attribute**|**type**|**effect**|
|---|---|---|
|`[FIELD].default`|`T[keyof T]`|Default value provided by you within `<MyForm.Provider defaultValues={value} />`.|
|`[FIELD].current`|`T`|Current state of the form value.|
|`[FIELD].set(value: T[keyof T])`|`(value: T[keyof T]) => void`|Allows setting of `[FIELD].current` values for matching `[FIELD]`.|
|`[FIELD].error`|`string \| undefined`|Custom string will exist *if* you have the matching `options.validate[FIELD](value: T[keyof T])` defined.|
|`[FIELD].name`|`string`|Provides a random, unique value for use as a primary key for any given field.|
|`[FIELD].reset()`|`() => void`|Reset `[FIELD].current` state to `Provider.props.defaultValues[FIELD]`. Clears any files, reader data, etc.|
|`[FIELD].handleFileEvent(event: React.ChangeEvent<HTMLInputElement>)`|`() => void`|Allow files to be referenced for a file change event. `onChange={handleFileEvent}`. *(see side effects)|

#### Side effects

##### `[FIELD].handleFileEvent(event: React.ChangeEvent<HTMLInputElement>)`
This particular function will capture the `event.target.files` for use outside of the component in question.

# Example
## Controlled form
Basic login form with `POST fetch` using JSON payload.
```tsx
const LoginForm = createForm<Login>({
  validate: {
    email: (value) => value.length === 0 ? 'Cannot be empty' : undefined,
    password: (value) => value.length === 0 ? 'Cannot be empty' : undefined,
  },
  props: {
    email: {
      type: 'email',
      placeholder: 
    }
  },
})

function Login() {
  return (
    <LoginForm.Provider defaultValues={{ email: '', password: '', session: false }}>
      <LoginEmail />
      <LoginEmailError />
      <LoginPassword />
      <LoginPasswordError />
      <SubmitButton />
    </LoginForm.Provider>
  )
}
function LoginEmail() {
  const { email } = useForm(LoginForm);
  return <input {...email} />
}
function LoginEmailError() {
  const { errors } = useForm(LoginForm);
  return (errors.email ? <span>{errors.email}</span> : null)
}
function LoginPassword() {
  const { password } = useForm(LoginForm);
  return <input {...password} />
}
function LoginPasswordError() {
  const { errors } = useForm(LoginForm);
  return (errors.password ? <span>{errors.password}</span> : null)
}
function SubmitButton() {
  const form = useForm(LoginForm);
  const handleClick = useCallback(() => {
    fetch('/login', { method: 'post', body: JSON.stringify(form.toJSON()) })
  }, [form])
  return <button onClick={handleClick}>Login</button>
}
```


## Development

With [Visual Studio Code](https://code.visualstudio.com), simply press `F5` to start the development server and browser.

#### Run dev:

```bash
yarn start
```

#### Build prod:

`Ctrl+shift+B` in [Visual Studio Code](https://code.visualstudio.com)

```bash
yarn build
```

#### Run prod: (after build)

```bash
yarn start:server
```

#### Analyze production bundle sizes:

```bash
yarn build:analyze
```

#### Run with [React Profiler](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html):

```bash
yarn build:profile
```

#### Check Packages for duplicates or circular dependencies:

```bash
yarn pkgcheck
```


#### Run with [Storybook](https://storybook.js.org/):

```bash
yarn storybook
```


#### Share demo on Stackblitz

https://stackblitz.com/github/react-noui/create-form