Validation tools for React built on react-waitables
This package provides tools for building efficiently executed validators that depend on bindings and/or waitables. This is useful:
The following example demonstrates attaching a validator to a string
binding, which is updated by a TextInput
component (code for TextInput
is included in CodeSandbox).
The validator's state automatically changes as a user interacts with the field. In this case the validator is very simple, checking that the text field isn't empty. Below the input, the page dynamically reflects:
import React, { useCallback } from 'react';
import { BindingsConsumer, resolveTypeOrDeferredType, useBinding } from 'react-bindings';
import { checkStringNotEmpty, useValidator } from 'react-validatables';
import { WaitablesConsumer } from 'react-waitables';
import { TextInput } from './TextInput';
export const App = () => {
const value = useBinding(() => '', { id: 'value', detectChanges: true });
const valueValidator = useValidator(value, () => checkStringNotEmpty("Shouldn't be empty"), { id: 'valueValidator' });
const onClear1Click = useCallback(() => value.set(''), [value]);
return (
<>
<div>
<TextInput value={value} />
<button onClick={onClear1Click}>Clear</button>
</div>
<div>
You entered:
<BindingsConsumer bindings={value}>{(value) => value}</BindingsConsumer>
</div>
<WaitablesConsumer dependencies={valueValidator}>
{(validator) => (
<>
<div>{`Valid: ${String(validator.isValid)}`}</div>
{!validator.isValid ? <div>{resolveTypeOrDeferredType(validator.validationError)}</div> : null}
</>
)}
</WaitablesConsumer>
</>
);
};
In the example above, the validator only depends on a single input value and only performs a single check. However, multiple inputs and multiple operations are supported in a single validator and validators can depend on other validators.
In the following example, we demonstrated stored values and validators (see CodeSandbox for more-complete example with UI) where:
const checkNamePart = (): ValidationChecker<string> => changeStringTrim(checkStringNotEmpty());
const firstName = useBinding(() => '', { id: 'firstName', detectChanges: true });
const firstNameValidator = useValidator(firstName, checkNamePart);
const lastName = useBinding(() => '', { id: 'lastName', detectChanges: true });
const lastNameValidator = useValidator(lastName, checkNamePart);
const nameValidator = useValidator([firstNameValidator, lastNameValidator], (validators) =>
checkAnyOf(validators, 'First name, last name, or both must be specified')
);
const age = useBinding<number | undefined>(() => undefined, { id: 'age', detectChanges: true });
const ageValidator = useValidator(age, () =>
preventUndefined(
[
checkNumberIsInteger("Fractional values aren't allowed"),
checkNumberGTE(0, 'Must be >= 0'),
checkNumberLT(200, 'Must be < 200')
],
'Age is required'
)
);
const formValidator = useValidators([nameValidator, ageValidator]);
As your use cases become more complex, you'll start to build reusable, composable validators.
There are many cases where validation logic needs to be dynamic. With react-validatables
, there are two main ways to introduce dynamism:
setDisabledOverride
The validator checker creation function is called every time validation is performed, so the rules you setup can be changed anytime.
Validators can be disabled using one or more of disabledUntil
, disabledWhile
, and/or disabledWhileUnmodifiedBindings
. When a validator is disabled, it it always considered to be valid. All of these options takes one or more bindings, so validators can be disabled/enabled very dynamically.
disabledWhileUnmodifiedBindings
helps create more friendly forms by, for example, not providing feedback on inputs that the user hasn't modified yet. Otherwise, in the common case, all fields would initially be in an error state, which isn't necessarily useful. Consider using disabledWhileUnmodifiedBindings
on all or most validators directly associated with inputs (see Final Validation section below as well).
If you need to override the automatically-calculated behavior, validators expose setDisabledOverride
, which can be used to forcibly enable or disable a validator or to clear the override.
In the following example, we use disabledWhileUnmodifiedBindings
so that firstNameValidator
is disabled until firstName
is modified. firstName
is usually modified by calling set
.
const firstName = useBinding(() => '', { id: 'firstName', detectChanges: true });
const firstNameValidator = useValidator(firstName, makeRequiredStringChecker, { disabledWhileUnmodified: firstName });
In addition to interactive validation, we often need "final" validation before, for example, submitting data to a server.
During interactive validation, we often have disabledWhileUnmodifiedBindings
associated with inputs. However, if a user tries to submit a form with incomplete data, where they've accidentally skipped a field, for example, we then want to make sure we give clear feedback at that point. The finalizeValidation
utility is used for these cases and to generally wait for validation to finish.
You may also want to use useValidators
which is a shorthand for combining multiple validators together.
const formValidator = useValidators([firstNameValidator, lastNameValidator]);
const onDoneClick = () =>
finalizeValidation(formValidator, {
fieldBindings: { firstName, lastName },
onValid: ({ firstName, lastName }) => { … }
});
finalizeValidation
generally:
triggerChangeListeners
, which in turn resets the associated validatorsonValid
or onInvalid
It returns a function that can be used to cancel validation, if desired, and also a promise for the result.
This package provides basic functionality for string and number validation and, more importantly, provides tools for building your own performant validators.
For extending these capabilities, be sure to checkout our API Docs to get a more-complete picture of the building blocks.
Examples of extensions others might add support for are:
BigNumber
DateTime
(Luxon)The API using the following prefixes for naming conventions:
checkStringNotEmpty()
changeStringTrim(checkStringNotEmpty())
allowNull(checkStringNotEmpty())
preventNull(checkStringNotEmpty())
selectValue(Math.random(), checkNumberGT(0.5))
Thanks for checking it out. Feel free to create issues or otherwise provide feedback.
Be sure to check out our other TypeScript OSS projects as well.