React Form Ctl is a simple and type-safe way to handle form values and validation with both React and React Native.
It is inspired by Angulars FormControl
and/or FormBuilder
You can view a live demo of it using this CodeSandbox link
- No runtime dependencies, only requires React as a peer dependency
- Fully type-safe
- Localization ready (i18n)
- Simple API that will be familiar to Angular developers and easy to learn for React developers
- All state handling is done for you
npm install react-form-ctl
# or
yarn add react-form-ctl
A quick example can be as easy as implementing a hook with some initial data and spreading the given properties onto a input element:
import {useFormControl} from 'react-form-ctl';
// Inside component
const {controls} = useFormControl<FormData>({
name: ['John'],
// ...
});
return <>
<input
type="text"
{...controls.name.inputProps()}
/>
{/* ... */}
</>
There are a number of input helpers available:
inputProps
-> for HTMLinput
snumberInputProps
-> for HTMLinput
s of type numbercheckboxProps
-> for HTML checkboxinput
srnInputProps
-> for React NativeTextInput
srnNumberInputProps
-> for React NativeTextInput
s of type number
Although if you want more granular control, you can do all the state changing by yourself, using the exposed properties:
import {useFormControl, Validators} from 'react-form-ctl';
// Inside component
const {controls} = useFormControl<FormData>({
name: ['John', [Validators.required, Validators.minLength(3)]],
// ...
});
return <>
<input
type="text"
value={controls.name.value}
onChange={(e) => controls.name.setValue(e.target.value)}
onBlur={() => controls.name.markTouched()}
/>
{/* ... */}
</>
In case you want to write a more specific error message for different errors, it is recommended to supply a Map with your error messages like in the following example. Note that this will only be applied to the first error supplied by the first failing Validator.
This Error-Map is also a good chance to bring your localized form data in.
import {useFormControl, extError, Validators, ErrorMappings} from 'react-form-ctl';
const {controls} = useFormControl<FormData>({
name: ['John', [Validators.required, Validators.minLength(5)]],
// ...
});
const errorMap: ErrorMappings = {
required: () => 'Field is required',
minLength: ({length, expectedLength}) => `Minimum Length: ${length}/${expectedLength}`,
default: () => 'Unknown error' // Default mapping fires, when no mapping is given for another error
// If no default mapping is set, a runtime exception may be thrown for unknown exceptions
};
return <>
{/* input field */}
{/* Check if the property contains errors (invalid)
+ also check if the property has been modified (edited or touched) */}
{ controls.name.invalid && (controls.name.touched || controls.name.dirty) && <div>
{extError(errorMap, controls.name.error)}
</div>}
</>
Alternatively, you can also get all errors manually and implement your own error handling like so:
import {useFormControl, Validators} from 'react-form-ctl';
const {controls} = useFormControl<FormData>({
name: ['John', [Validators.required, Validators.minLength(5)]],
// ...
});
return <>
{/* input field */}
{ controls.name.errors?.required && <div>
Field is required
</div>}
{ controls.name.errors?.minLength && <div>
Minimum Length: {controls.name.errors.minLength.length}/{controls.name.errors.minLength.expectedLength}
</div>}
</>
There are a number of Validators already included:
- required
- requiredTrue
- minLength
- maxLength
- numeric
- min
- max
- regex
- if (can execute validators based on a condition)
If you need more validation types (email, phone number, ...), we recommend using the Validator.js library. Check their documentation for more information about the available validators.
npm i validator && npm i -D @types/validator
# or
yarn add validator && yarn add -D @types/validator
You can then implement your own validators and pass them to the Validators array:
import {useFormControl, Validators, InternalState} from 'react-form-ctl';
import isEmail from 'validator/lib/isEmail';
// A simple custom validator
const nameNotBlacklisted = Validators.create((value: string) => {
if(['Max', 'Anna'].includes(value)) {
return {
name: 'nameNotBlacklisted',
found: value
};
}
});
// You can also create parametrized custom validators
const isExactAge = Validators.createParametrized((age: number) => {
return Validators.create((value: number) => {
if (value !== age) {
return {
name: 'isExactAge',
expected: age
};
}
});
});
// You can also check against other form values in your validator
const passwordRepeatMatches = Validators.create(
(passwordRepeat: string, state: InternalState<FormData>) => {
if (passwordRepeat !== state.password.value) {
return {
name: 'passwordRepeatMatches',
expected: passwordRepeat
};
}
});
// You can use the Validator.js library to write your own custom validators
const validateEmail = Validators.create((email: string) => {
if (!isEmail(email)) {
return {
name: 'validateEmail',
};
}
});
// You can use the "if" Validator to create conditional Validators
const isSomethingTrue = ...;
const requiredIfCondition = Validators.if(() => isSomethingTrue, [Validators.required])
// Then use them like other validators inside your Validators array
const {controls} = useFormControl<FormData>({
name: ['John', [nameNotBlacklisted]],
age: [21, [isExactAge(42), requiredIfCondition]],
password: [''],
passwordRepeat: ['', [passwordRepeatMatches]],
email: ['[email protected]', [validateEmail]]
// ...
}, [isSomethingTrue]); // Needs to pass dependency array, because of the "if" validator
// Don't forget to also write custom error handlers if you want
const errorMap: ErrorMappings = {
nameNotBlacklisted: ({found}) => `Name is blacklisted: ${found}`,
isExactAge: ({expected}) => `Expected age of ${expected}`,
passwordRepeatMatches: () => `Password does not match`,
validateEmail: () => `Email invalid`,
default: () => 'Unknown error'
};
You will get back an object containing information about the general state of the form and also a more detailed information about each form field:
type State = {
controls: {
[FieldName: string]: { // For each passed field, you get an entry
// in this object with detailed information about it
// properties are described down below
}
};
value: FormData; // Get the input data as an object
setValue: (value: FormData) => void; // Update the whole form state at once
reset: () => void; // Reset the form to the initial values
valid: boolean; // If the form passed all Validators
invalid: boolean; // The opposite of the above
touched: boolean; // If the form data was set once
dirty: boolean; // If the form data was updated once
tod: boolean; // Shorthand for touched || dirty
}
For each passed field, you will get back the following:
type FieldState = {
value: FieldType; // The value of the field
setValue: (value: FieldType) => void; // Callback to set the value of the field
resetValue: (value: FieldType) => void; // Callback to set the value of the field and setting touched = false & dirty = false
valid: boolean; // If the value passes all Validators
invalid: boolean; // The opposite of the above
touched: boolean; // If the value was set once
markTouched: (value?: boolean) => void; // Set the value to a desired touched state (if no parameter is given, it is set to touched = true)
dirty: boolean; // If the value was updated once
markDirty: (value?: boolean) => void; // Set the value to a desired dirty state (if no parameter is given, it is set to dirty = true)
touchedOrDirty: boolean; // Shorthand for touched || dirty
error?: {
name: string; // name of the error (e.g. 'required')
// more custom error properties
};
errors?: {
[ErrorName: string]: { // the property name is the name of the error (e.g. required)
name: string; // name of the error (e.g. 'required')
// more custom error properties
}
};
}
Dirty means that the value of a property has updated once. Touched means that the input element has been focused once.
A common use-case for those properties is error-handling. An error for a value should mostly only be shown, if the value is invalid AND was already changed once, or the input field has been focused and unfocused again. To implement this, you would have to write:
if (somefield.invalid && (somefield.touched || somefield.dirty)) { /* ... */ }
Because this is so common, react-form-ctl
has implemented a small shortcut for
that:
if (somefield.invalid && somefield.touchedOrDirty) { /* ... */ }