npm package: @noire/form
Description: Form library for React
Author: Alexandre Clark
Latest version: 0.0.4

# `@noire/form`

Lightweight form library for React.

## Installation

```shell
yarn add @noire/form
```

## Usage

This library exposes four components to quickly build a form :

* `<Form />`
* `<Input />`
* `<Button />`
* `<ErrorText />`

**Basic example**

Here's the simplest use case, which should look almost exactly the same as a form you would normally build using `<form />`, `<input />` and `<button />`.

```tsx
function App() {
  return (
    <Form name="login" onSubmit={(form) => console.log(form)}>
      <Input type="email" name="email" placeholder="Email" />
      <ErrorText for="email" />
      <Input type="password" name="password" placeholder="password" />
      <ErrorText for="password" />
      <Button>Login</Button>
    </Form>
  );
}
```

Let's go over the advanced features that makes this library worth it.

### Validation

`<Input />` accepts a `validator` prop which let's you define a validation to be executed when the form will be submitted.

To help building validators, we expose a `useValidator` hook that fills the most common use cases.

**Example**

```tsx
import {useValidator} from '@noire/form';

const passwordValidator = useValidator({
  min: 7,
  max: 15,
  required: true,
  message: ({min, max, required}) => {
    if (!required) return 'Password required';
    if (!min) return 'Password should be at least 7 characters';
    if (!max) return 'Password should be 15 characters maximum';
    return 'Password invalid';
  },
});

function App() {
  return (
    <Form name="login">
      <Input name="password" validator={passwordValidator} />
    </Form>
  );
}
```

The current properties that validators accepts are the following :

```ts
export interface ValidatorProperties {
  pattern?: string;
  required?: boolean;
  min?: number;
  max?: number;
  email?: boolean;
  hasNumber?: boolean;
  hasLowercase?: boolean;
  hasUppercase?: boolean;
}
```

Using the `pattern` property, you should be able to handle most complex use cases, but if you want to create your own validator, we expose the `FormValidator` interface.

### Loading

By default `<Form />` will expose to all other components when a form is submitting. If a promise is returned by the `onSubmit` handler, we will wait for the promise to resolve. This is useful when you want to keep a loading state during the form validation, but also during the `onSubmit` callback where you might call an API an do all sorts of async operations.

**Example**

This example mimicks an API call using `setTimeout`.

```tsx
function App() {
  return (
    <Form name="login" onSubmit={() => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve({});
        }, 1000);
        });
      }}>
      <Button>Login</Button>
    </Form>
  );
}
```

If you need to know in another component that the form is submitting, you can use the `useForm` hook.

**Example**

```tsx
import {useForm} from '@noire/form';

function Banner() {
  const {submitting} = useForm();

  if (submitting) {
    return <p>Loading...</p>
  }

  return null;
}
```

A common use case during loading is to apply styles to inputs or buttons when submitting the form. To make it easier to handle differrent styles for our `<Button />` and `<Input />` components, we expose `className getters` that have access to a `submitting` value. We'll discuss more about styling below.

### Styling

To make styling easier, `<Input />` and `<Button />` accepts enhanced `className` prop that are essentially `className getters`. This prop let's you decide between passing just a `string` or passing a callback to compute a string from it.

**Example**

In this example we define special classes for the `disabled` and `error` states. This example also uses [classnames](https://www.npmjs.com/package/classnames) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss) for styling, but any classes or simply using template strings would work just as fine.

```tsx
import React, {useCallback} from 'react';
import classNames from 'classnames';
import {Form, Input, InputStyles} from '@noire/form';

function App() {
  const inputStyles = useCallback<InputStyles>(
    ({disabled, error}) =>
      classNames(
        'p-1 px-2 rounded-lg text-black',
        disabled && 'bg-gray-400',
        error && 'border-red-500 border-2',
      ),
    [],
  );

  return (
    <Form name="login">
      <Input name="email" className={inputStyles} />
    </Form>
  );
}
```

### Modularity

One of the great things about this library is that instead of having a big form hook with all the submission and validation logic at the same place (like most other form libraries), it let's you split and compose your fields. This makes it much easier to reuse logic and reduces the amount of code you have to write.

**Example: Reusing validators**

```tsx
const usePasswordValidator = () => useValidator({
  min: 7,
  max: 15,
  required: true,
  message: ({min, max, required}) => {
    if (!required) return 'Password required';
    if (!min) return 'Password should be at least 7 characters';
    if (!max) return 'Password should be 15 characters maximum';
    return 'Password invalid';
  },
});

function Signup() {
  const passwordValidator = usePasswordValidator();

  return (
    <Form name="signup">
      <Input name="password" validator={passwordValidator} />
    </Form>
  );
}

function UpdateUser() {
  const passwordValidator = usePasswordValidator();

  return (
    <Form name="updateUser">
      <Input name="password" validator={passwordValidator} />
    </Form>
  );
}
```

**Example: Reusing subforms**

```tsx
function UserSubform() {
  return (
    <>
      <Input name="email" />
      <Input name="username" />
      <Input name="firstName" />
      <Input name="lastName" />
      <Input name="password" type="password" />
      <Input name="confirmPassword" type="password" />
    </>
  )
}

function Signup() {
  return (
    <Form name="signup" onSubmit={(user) => createUser(user)}>
      <UserSubform />
    </Form>
  );
}

function UpdateUser() {
  return (
    <Form name="updateUser" onSubmit={(user) => updateUser(user)}>
      <UserSubform />
    </Form>
  );
}
```

## API

### `<Form />`

`<Form />` is the core of this library. It uses a React context provider to expose the _whole state_ of a form to the other `@noire/form` components and accepts a `onSubmit` handler that will be called with the value of each fields.

One important thing to note is that `<Form />` doesn't track any of the values from each of the `<Input />` component render as one of its children. Instead, this library uses [uncontrolled components](https://reactjs.org/docs/uncontrolled-components.html) in order to obtain the state when the user decides to submit the form by pressing a submit button. It uses a single [ref](https://reactjs.org/docs/glossary.html#refs) on a `<form />` component, which returns all of the form values.

The tradeoffs of uncontrolled vs controlled components is pretty well documented at this point and I suggest reading both articles if you're not familiar with the subject:
* https://reactjs.org/docs/uncontrolled-components.html
* https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/

By not having the values of each field in our `<Form />`, we can avoid a lot of unneccessary re-renders without having to write too much code.

**onSubmit**

The `onSubmit` handler has the following type signature :

```ts
onSubmit(formResult, controller)
```

* `formResult` : An object where each key matches the `name` of an `<Input />`
* `controller` : An object which exposes internal form APIs to interact with your form.

**Example**

Let's take our login form example again. `onSubmit` will always return an object containing each of our `<Input />`. We can then create an typescript interface to ensure better typings.

```ts
interface LoginForm {
  email: string;
  password: string;
}

function App() {
  return (
    <Form name="login" onSubmit={(formResult: LoginForm, controller) => console.log(form)}>
      <Input name="email" />
      <Input name="password" />
    </Form>
  );
}
```