0.0.4 • Published 3 years ago

@noire/form v0.0.4

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

@noire/form

Lightweight form library for React.

Installation

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 />.

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

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 :

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.

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

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 and tailwindcss for styling, but any classes or simply using template strings would work just as fine.

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

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

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 in order to obtain the state when the user decides to submit the form by pressing a submit button. It uses a single ref 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:

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 :

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.

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>
  );
}
0.0.4

3 years ago

0.0.3

3 years ago

0.0.2

3 years ago

0.0.1

3 years ago