react-lightweight-forms
is a simple to use, lightweight forms API based on React hooks. It only provides the functional aspects of forms. It's up to you to develop your UI as you see fit, and simply integrate your UI with the hooks.
This library supports the following:
- Form state management
- Form field validations
- Form submission
- Form field state management
- Doesn't require you to develop functional React UI components - you can continue to use your class based components
ThoroughlySomewhat unit tested 😃- Supports asynchronous validations on blur, on change, and on submit
- Supports standard HTML inputs such as:
- Text inputs (input type text, textareas)
- Radio button groups
- Checkboxes
- Multi-selects
- Supports custom components
- Supports custom validations
- Supports yup-based schema validations
- Inputs support pristine and visited state
- Dynamically add or remove form fields to/from existing forms
- Easy to get started!
See my multi-part article series for background. I took it upon myself to develop this forms library as a part of learning a bit about React hooks since I believe that hooks are a better way to develop React components.
See complete demo of all features.
This is not an NPM package at this time. Two install options you can:
npm install --save https://github.com/shanplourde/react-hooks-form-util#master
- Copy the source code under
src/components/form
into your project and run with it
Call useForm
and pass the form id along with your form's initial state.
useForm
returns your form's current state under theinputValues
object.useForm
returnsgetFormProps
, which you need to expand onto yourform
tag- Your
onSubmit
gets called by theuseForm
hook, via thegetFormProps
that you expand onto your form tag
import { useForm } from "form/use-form";
const { inputValues, getFormProps } = useForm({
id: "settingsForm",
initialState: {
firstName: "George",
lastName: "OfTheJungle",
email: "george@thejungle.com",
custom: "custom",
agreeToTerms: false,
comments: "",
favouriteFlavour: "",
favouriteColours: ["red", "green"],
cookiesPerDay: null,
preferredDate: null
}
});
console.log(inputValues.firstName); // George
///
const onSubmit = async ({ evt, inputValues }) => {
await sleep(2000);
console.log("onSubmit was called", inputValues);
};
///
<form {...getFormProps({ onSubmit })}>
useForm
returns an API that you can use to configure your form's inputs.
- Call
api.addInput
to define your input - Call
api.addRadioGroup
to add a radio button group (this will probably be rolled intoapi.addInput
). - Expand
getInputProps
onto your inputs
import { useForm } from "form/use-form";
const { inputValues, api } = useForm({
id: "settingsForm",
initialState: {
firstName: "George",
lastName: "OfTheJungle",
email: "george@thejungle.com",
custom: "custom",
agreeToTerms: false,
comments: "",
favouriteFlavour: "",
favouriteColours: ["red", "green"],
cookiesPerDay: null,
preferredDate: null
}
});
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName
});
//
<div className="field-group">
<label htmlFor={firstNameInput.id}>
First name {JSON.stringify(inputUiState.firstName)} --{" "}
{JSON.stringify(formValidity.firstName)} *
</label>
<input type="text" {...firstNameInput.getInputProps()} />
- Call
api.removeInput
to remove an existing input field - Removing an input field removes all state associated with the input, such validation state, input value, etc.
- The demo/dynamic-form example shows how to add and remove form fields dynamically to an already rendered form
/// Add your inputs, say an input with id = firstName
api.removeInput("firstName");
- Pass a
validators
array toapi.addInput
- See
validators.js
for out of the box validations (currentlyrequired
,email
,mustBeTrue
,schema
) - For each validator, specify the
when
array, which indicates when validators fire. Validators can be firedonBlur
,onSubmit
, andonChange
- Validity state is not set for form fields that don't specify validity
- Validity state is only set once a validation has run
useForm
returns aformValidity
object. Each key is an input id. Each value is an array of validation errors
import { validators, validateInputEvents } from "validators";
const { required, email } = validators;
const { onBlur, onSubmit, onChange } = validateInputEvents;
const { formValidity, inputValues, api } = useForm({
...
});
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [{ ...required, when: [onChange, onSubmit] }]
});
const lastNameInput = api.addInput({
id: "lastName",
value: inputValues.lastName,
validators: [{ ...required, when: [onBlur, onSubmit] }]
});
const emailInput = api.addInput({
id: "email",
value: inputValues.email,
validators: [
{ ...required, when: [onBlur, onSubmit] },
{
...email,
when: [onBlur, onSubmit]
}
]
});
//
console.log('formValidity', formValidity, formValidity.lastName)
- Import
createValidator
and pass an object with avalidationFn
property, representing your validation function. Your validation function receives the input value - Pass an
error
property, which is the key that you can use to determine what validation errors triggered for a given input
import { createValidator, validateInputEvents } from "validators";
const { onBlur, onSubmit } = validateInputEvents;
const { formValidity, inputValues, api } = useForm({
...
});
const customValidator = createValidator({
validateFn: async ({ value }) =>
await new Promise(resolve => {
setTimeout(() => {
resolve((value || "").length > 8);
}, 5000);
}),
error: "CUSTOM_ASYNC_ERROR"
});
const customInput = api.addInput({
id: "custom",
value: inputValues.custom,
validators: [{ ...customValidator, when: [onBlur, onSubmit] }]
});
console.log(formValidity.custom); // returns validity state
- When creating a custom validator with
createValidator
,validateFn
receives ainputValues
argument that allows you to compare a form field against all other values in the current form
import { createValidator, validateInputEvents } from "validators";
const { onBlur, onSubmit } = validateInputEvents;
const emailInput = api.addInput({
id: "email",
value: inputValues.email,
validators: [
{ ...required, when: [onBlur, onSubmit] },
{
...email,
when: [onBlur, onSubmit]
}
]
});
const confirmEmailValidator = createValidator({
validateFn: ({ value, inputValues }) =>
value === inputValues.email;
},
error: "EMAILS_DO_NOT_MATCH"
});
const confirmEmailInput = api.addInput({
id: "confirmEmail",
value: inputValues.confirmEmail,
validators: [{ ...confirmEmailValidator, when: [onBlur, onSubmit] }]
});
- Nothing else needs to be done for asynchronous validations, they're supported right out of the box
- The custom validator below is one example
const customValidator = createValidator({
validateFn: async ({ value }) =>
await new Promise(resolve => {
setTimeout(() => {
resolve((value || "").length > 8);
}, 5000);
}),
error: "CUSTOM_ASYNC_ERROR"
});
- Schema validations are supported with the yup schema validation library
- Schema validations are configured at the field level, meaning that you have fine-grained control over when schema validations run ( i.e. onBlur, onSubmit, or using the reward early validate late pattern)
- Checkout the code below.
mySchema
is our schema definition. We pass this touseForm
, and then use the predefinedschema
validator. In addition, we get to specify thewhen
property for our schema validations
import { object, string } from "yup";
const { schema } = validators;
const mySchema = object({
firstName: string()
.required()
.min(3),
lastName: string()
.required()
.length(10),
email: string().email()
});
const { api } = useForm({
id: "kitchenSinkForm",
initialState,
validationSchema: contactSchema
});
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [
{
...schema,
when: [
{
eventType: onChange,
evaluateCondition: evaluateConditions.rewardEarlyValidateLate
},
onBlur,
onSubmit
]
}
]
});
- When defining your
when
condition for validators, you can choose from the eventsonBlur
,onChange
, andonSubmit
- In addition, you can define your own custom expression with
evaluateCondition
that decides if your validator should trigger - Predefined evaluateConditions:
evaluateConditions.rewardEarlyValidateLate
: Returns true if theformValidity
for the given input is false
- evaluateCondition functions receive
{ id, inputValues, formValidity }
asid
is the given input's idinputValues
are the given form's input valuesformValidity
is the current form's input validity
import { validators, validateInputEvents, evaluateConditions } from "validators";
const { required, email } = validators;
const { onBlur, onSubmit, onChange } = validateInputEvents;
const { formValidity, inputValues, api } = useForm({
...
});
// Example of using the stock evaluateConditions.rewardEarlyValidateLate
// evaluator with the onChange event:
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [
{
...required,
when: [
{
eventType: onChange,
evaluateCondition: evaluateConditions.rewardEarlyValidateLate
},
onBlur,
onSubmit
]
}
]
});
// Example of using a custom evaluateCondition
// evaluator with the onChange event. evaluateCondition
// returns true, so the validation is always evaluated
// onChange
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [
{
...required,
when: [
{
eventType: onChange,
evaluateCondition: ({ id, inputValues, formValidity }) => true
},
onBlur,
onSubmit
]
}
]
});
- useForm does not block form submits if the form is in an invalid state
- This allows you to either submit the form on error, prevent submission, or check if critical validations passed before submission, for example
- All validations are executed before your custom
onSubmit
form is invoked formValidity
and uiState.isValid are set before your customonSubmit
is executed
const { getFormProps, inputValues, uiState, formValidity } = useForm({
id: "settingsForm",
initialState: {
firstName: "George",
lastName: "OfTheJungle"
///
}
});
const handleOnSubmit = async ({ evt, inputValues }) => {
await sleep(2000);
console.log("sample-form onSubmit, inputValues", inputValues);
if (uiState.isValid || formValidity.firstName.isValid) {
// Guess we're ok with just first name being valid, let's submit our form
}
};
<form {...getFormProps({ onSubmit: handleOnSubmit })} />;
useForm
returns auiState
property that allows you to track the following:isValidating
: is form getting validatedisSubmitting
: is form getting submittedisValid
: is form valid
A submit button could therefore be disabled, or a modal overal perhaps displayed, if the form was being validated or submitted.
<div className="input-footer">
<button type="submit" disabled={uiState.isSubmitting || uiState.isValidating}>
Save
</button>
{uiState.isSubmitting || (uiState.isValidating && <p>Submitting...</p>)}
</div>
useForm
's api.addInput
returns an input with a uiState
property. You can use this to get the input's visited and pristine state.
visited
is set to true whenever an input receives focus.
pristine
is set to false whenever an input's value changes from its original value. pristine
can be set to false
, then true
should the user change the input value back to its original value.
const firstNameInput = api.addInput({
id: "firstName",
value: inputValues.firstName,
validators: [{ ...required, when: [onChange, onSubmit] }]
});
console.log(firstNameInput.uiState); // { visited: false, pristine: true }
- useForm doesn't handle custom onSubmit errors, you'll have to handle these
- When a custom validation throws an error, the validator's state
is set to
isValue = true
, and a property calledundeterminedValidations
is set that includes the validation name (error key), and error details
- Each blur / change event on an input requests new asynchronous validations
- However if an input's value is the same, the new validation request is ignored, allowing the original validation request to complete. This should optimize the user experience
- In the future, this may be an option that you can opt out of, in case you need an asynchronous validation that depends on other form field values
- Each blur / change event on an input requests new asynchronous validations
- When the latest input value is different from the previous, a new asynchronous validation request is kicked off
- Previously ran validation requests are ignored, but any asynchronous activity they are performing is not cancelled
Feel free to open an issue to raise questions, bugs, suggestions, etc.