another-use-form-hook
A React hook 🎣 for easy form handling
Table of Contents
Installation
This should be installed as one of your project dependencies
:
yarn add another-use-form-hook
or
npm install --save another-use-form-hook
NOTE: another-use-form-hook
only works with react >=16.8, since it is a hook.
Usage
This hook is intended to give a full solution for handling forms. From interdependent field value validations (meaning if a field value is dependent on other field value), to submitting the form, and providing information about when the UI should be unresponsive (loading of some kind of async-like operation), in addition to notification "hooks" to be able to inform the users the most efficient way.
To retrieve props for an input field, you have the following options:
- Using the
form.inputs.{inputType}('name')
input prop generator function (inputTypes
is one of these) - Using
form.fields.{name}.{value|error}
and form.handleChange
functions
NOTE: The example below is available live at CodeSandbox
Let's see a complex example to understand how it works:
import React from "react";
import ReactDOM from "react-dom";
import useForm from "another-use-form-hook";
import "./styles.css";
import { addDays, isAfter, differenceInDays, parseISO } from "date-fns";
const isValidEmail = email => /\w*@\w*\.\w*/.test(email);
const TODAY = new Date();
const App = () => {
const form = useForm({
initialState: {
email: "",
arrival: TODAY,
departure: TODAY
},
validators: (fields, isSubmitting) => ({
email: isSubmitting
? isValidEmail(fields.email)
: typeof fields.email === "string",
arrival: isAfter(parseISO(fields.arrival), TODAY),
departure: isAfter(addDays(parseISO(fields.departure), 1), TODAY),
minOneNight: isSubmitting
? differenceInDays(
parseISO(fields.departure),
parseISO(fields.arrival)
) >= 1
: true
}),
onNotify: (type, reason) => {
switch (type) {
case "submitError":
console.error("Form could not be submitted: ", reason);
break;
case "submitSuccess":
console.info("Form has been submitted.", reason);
break;
case "validationErrors":
console.warn(
"The following problems occurred while validating: ",
reason
);
break;
default:
break;
}
},
onSubmit: async ({ fields, setLoading, notify }) => {
try {
setLoading(true);
const response = await new Promise(resolve => {
setTimeout(() => {
console.log("Submitting: ", fields);
resolve(fields);
}, 1000);
});
notify("submitSuccess", response);
} catch (error) {
notify("submitError", error.message);
} finally {
setLoading(false);
}
}
});
return (
<form onSubmit={form.handleSubmit}>
{/* option 1 (control all the props with a one-liner)*/}
<fieldset>
<legend>Option 1</legend>
<label htmlFor="email">
{form.fields.email.error ? "Invalid" : ""} email
</label>
<input {...form.inputs.email("email")} />
<label htmlFor="departure">
{form.fields.arrival.error ? "Invalid" : ""} arrival
</label>
<input
{...form.inputs.date("arrival")}
// You can override props by simply defining them last
onChange={e => form.handleChange(e, ["minOneNight"])}
/>
</fieldset>
{/* option 2 specify id, type, name, value props manually*/}
<fieldset>
<legend>Option 2</legend>
<label htmlFor="arrival">
{form.fields.arrival.error ? "Invalid" : ""} arrival
</label>
<input
type="date"
id="arrival"
name="arrival"
value={form.fields.arrival.value}
onChange={e => form.handleChange(e, ["minOneNight"])}
/>
<label htmlFor="departure">
{form.fields.departure.error ? "Invalid" : ""} departure
</label>
<input
type="date"
id="departure"
name="departure"
value={form.fields.departure.value}
onChange={e => form.handleChange(e, ["minOneNight"])}
/>
</fieldset>
{/* also from option 1 */}
<button {...form.inputs.submit()} disabled={form.loading}>
Submit
</button>
</form>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
Documentation
useForm
useForm(useFormParams: UserFormParams): UseForm
UseFormParams
InitialState
An object containing the default value of every field in a form.
Example:
useForm({
initialState: {
email: "",
name: "",
address: "",
age: 0
}
})
If name
is defined, you can refer to initialStates.{name}
in formProviderProps.
Example:
<FormProvider
initialStates={{
login: {
email: "email@example.com",
password: ""
}
}}
>
const form = useForm({name: "login"})
console.log(form.fields.email.value)
Validators
This function is invoked before onChange and onSubmit. The former only runs the validations for the changed fields, while the latter runs it on the whole form. For convenience, it is also returned from useForm
.
function validators(fields: object, isSubmitting: boolean): Validations
Validations
An object containing boolean
expressions. Each input field must have at least a corresponding property in this object, but you can define custom ones as well.
Example:
{
email: submitting
? isValidEmail(fields.email)
: typeof fields.email === "string"
}
You can also look at the live example.
SubmitCallback
Invoked when handleSubmit is called and there were no validation issues.
function onSubmit(onSubmitParams: OnSubmitParams): void
NotifyCallback
Invoked if there is a validation error when calling handleChange
or handleSubmit
. Can be manually triggered on onSubmit
by calling notify
.
type NotifyType = "validationErrors" | "submitError" | "submitSuccess"
function notify(type: NotifyType, reason: any): void
name | type | description |
---|
type | string | Type of notification |
reason | any | When type is validationErrors , it is a list of field names, Otherwise you set it to whatever you want. |
Example:
Look at the live example.
UseFormReturn
FieldValuesAndErrors
Validated field values and their errors.
Example:
const form = useForm({initialState: {
email: "email@example.com",
age: -2
}})
console.log(form.fields)
console.log(form.hasErrors)
ChangeHandler
You can call this two ways. Either pass an event as the first argument, or a partial fields
object. With the latter, you can change multiple values at the same time. E.g.: resetting the form after submit, or any other reason you might have.
function handleChange(event: React.FormEvent, validations: string[]): void
function handleChange(fields: object, validations: string[]): void
name | type | description |
---|
event | React.FormEvent | Standard event. Using target.{name|value|checked} to infer the intention |
fields | object | Pass a partial fields object, if you want to change multiple values at the same time |
validations | string[] | Which validators you would like to run. If omitted, only validators with the same event/field name will be run |
Example:
Look at the live example.
SubmitHandler
Call to submit the form. Before onSubmit is invoked, validators is run for every form field. If there were any errors, notify is invoked with type
being validationErrors
, and reason
a list of form field names.
function handleSubmit(): void
InputPropGenerators
An object, containing properties with the same name as the HTML input types, with some minor differences.
For convenience, since datetime-local
contains a hyphen (-) character, it is also exposed as datetimeLocal
, to overcome the need of "
characters, when accessing it.
I.e.:
const form = useForm()
form.inputs.datetimeLocal == form.inputs["datetime-local"]
In addition to the standard input types, there is a select
type also available.
Each property is a function:
function inputType(name: string, options: InputTypeOptions): InputPropGeneratorsReturn
Example:
For examples of all types, you can check this test suite
InputTypeOptions
An optional object
name | type | description |
---|
value | string | Usually, when using radio buttons, values are static. (Each button in the same group must have different values) |
generateProps | function | Provides name , value and error that can be used to generate additional props. Useful, if you want to avoid using form.fields |
formName | string | If the input type is submit , it can be used to override the name of the form being submitted. |
Example:
const form = useForm()
console.log(form.fields.radio.value)
return (
<radiogroup>
<input {...inputs.radio('radio', { value: 'option-1' })}/>
<input {...inputs.radio('radio', { value: 'option-2' })}/>
{/*You can do it the "traditional" way also*/}
<input
{...inputs.radio('radio')}
value='option-3'
/>
</radiogroup>
)
const form = useForm()
return(
<div>
<input
{...form.inputs.email("emailField", {
generateProps: ({error}) => ({
className: error ? "email-error" : "email",
})
})}
/>
{/*Tip: if your custom Input component takes an error prop, you can try this: */}
<Input {...form.inputs.email("emailField", {generateProps: _ => _})}/>
{/* This will spread error to Input as well.*/}
{/*Or here is a more complex example for a custom Input component*/}
<Input
{...form.inputs.email("emailField", {
generateProps: ({error, name}) => ({
error,
label: error ? `Invalid ${name}` : name,
placeholder: `Type ${name}`
})
})}
/>
</div>
)
InputPropGeneratorsReturn
An object that can be spread on a React input like element.
name | type | description |
---|
name | string | The name of a field. Must be one of the properties in initialState |
id | string | By default, same as name . If input type is radio , it is the same as value , to avoid problems in radio groups where id would be the same |
value | any | The value of the field |
onChange | function | See onChange |
checked | boolean | If input type is checkbox , it is the same as value |
onClick | function | If input type is submit , it is onSubmit |
Example:
const form = useForm()
return (
<div>
<label htmlFor="emailField">E-mail</label>
<input {...form.inputs.email("emailField")}/>
{/* This is the same as */}
<input
name="emailField"
id="emailField"
type="email"
onChange={form.handleChange}
value={form.fields.emailField.value}
/>
</div>
)
FormProvider
function FormProvider(formProviderProps: FormProviderProps): JSX.Element
FormProviderProps
name | type | description |
---|
initialStates | object | Single place to define all initialState for every form. See InitialState |
validators | object | Single place to define all validators for every form. See Validators |
onSubmit | function | Same as onSubmit |
onNotify | function | Same as onNotify |
children | ReactElement | The element you would like to wrap with FormProvider |
getForms
function getForms() : Forms
Forms
An object containing all the forms' current values in FormProvider. Same shape as initialStates
.
LICENSE
MIT