@noire/form v0.0.4
@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:
- 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 :
onSubmit(formResult, controller)
formResult
: An object where each key matches thename
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>
);
}