Remix Validity State
remix-validity-state
is a small React form validation library that aims to embrace HTML input validation and play nicely with Remix primitives.
⚠️ Note: This library is in very much in an alpha stage. Feedback is welcome, however production usage is strongly discouraged.
Design Goals
This library is built with the following design goals in mind:
1. Leverage built-in HTML input validation attributes verbatim
What ever happened to good old <input required maxlength="30" />
? Far too often we reach for some custom implementation just to check that a value is not empty. Let’s use what we have readily available when we can! That way we don’t have to relearn something new. If you already know some of the HTML validation attributes…then you’re ready to use this library.
2. Share validations between client and server
Thanks to Remix
, this is finally much more straightforward than it has been in the past. But wait ?, aren’t we using DOM validations? We don’t have a DOM on the server?!? Don’t worry – in true Remix spirit, we emulate the DOM validations on the server.
3. Expose validation results via a ValidityState
-like API
We will need an API to explain the validation state of an input…good news – the web already has one! Let’s #useThePlatform
and build on top of ValidityState
.
4. Permit custom sync/async validations beyond those built into HTML
Congrats for making it to bullet 4 and not leaving as soon as we mentioned the super-simple HTML validations. Don’t worry – it’s not lost on me that folks need to check their email addresses for uniqueness in the DB. We’ve got you covered with custom sync/async validations.
5. Provide limited abstractions to simplify form markup generation
Semantically correct and accessible <form>
markup is verbose. Any convenient form library oughta provide some wrapper components to make simple forms easy. However, any form library worth it’s weight has to offer low level access to allow for true custom forms, and the ability to built custom abstractions for your application use-case. Therefore, any wrapper components will be little more than syntactix sugar on top of the lower level APIs.
Installation
> npm install --save remix-validity-state
Usage
Demo App
There’s a sample Remix app deployed to rvs.fly.dev that you can check out. This app source code is stored in this repository in the demo-app/
folder, so you can also run it locally via:
git clone [email protected]:brophdawg11/remix-validity-state.git
cd remix-validity-state/demo-app
npm ci
npm run dev
Getting Started
Define your form validations
In order to share validations between server and client, we define a single object containing all of our form field validations, keyed by the input names. Validations are specified using the built-in HTML validation attributes, exactly as you’d render them onto a JSX <input>
.
const formValidations = {
name: {
required: true,
maxLength: 50,
},
middleInitial: {
pattern: "^[a-zA-Z]{1}$",
},
lastName: {
required: true,
maxLength: 50,
},
emailAddress: {
type: "email",
required: true,
maxLength: 50,
},
};
This allows us to directly render these attributes onto our HTML inputs via something like <input name="firstName" {...formValidations.firstName} />
Provide your validations via FormContext
In order to make these validations easily accessible, we provide them via context that should wrap your underlying <form>
:
<FormContext.Provider value={{ formValidations }}>
{/* Your <form> goes in here */}
</FormContext.Provider
Render <Field>
Components inside your FormContext
<FormContext.Provider value={{ formValidations }}>
<Field name="firstName" label="First Name" />
<Field name="middleInitial" label="Middle Name" />
<Field name="lastName" label="Last Name" />
<Field name="emailAddress" label="Email Address" />
</FormContext.Provider>
The <Field>
component is our wrapper that handles the <label>
, <input>
, and real-time error display.
Wire up server-side validations
In Remix, your submit your forms to an action
which receives the FormData
. In your action, call validateServerFormData
with the formData
and your previously defined formValidations
:
export async function action({ request }) {
const formData = await request.formData();
const serverFormInfo = await validateServerFormData(
formData,
formValidations
);
if (!serverFormInfo.valid) {
// Uh oh - we found some errors, send them back up to the UI for display
return json({ serverFormInfo });
}
// Congrats! Your form data is valid - do what ya gotta do with it
}
Add your server action response to the FormContext
When we validate on the server, we may get errors back that we didn’t not catch during client-side validation (or we didn’t run because JS hadn’t yet loaded!). In order to render those, we can provide the response from validateServerFormData
to our FormContext
and it’ll be used internally. The serverFormInfo
also contains all of the submitted input values to be pre-populated into the inputs in a no-JS scenario.
export default function MyRemixRouteComponent() {
let actionData = useActionData();
return (
<FormContext.Provider
value={{
formValidations,
serverFormInfo: actionData?.serverFormInfo,
}}
>
<Field name="firstName" label="First Name" />
<Field name="middleInitial" label="Middle Name" />
<Field name="lastName" label="Last Name" />
<Field name="emailAddress" label="Email Address" />
</FormContext.Provider>
);
}
That’s it!
You’ve now got a real-time client-side validated form wired up with your rock-solid server validations!
Advanced Usages and Concepts
EnhancedValidityState
Internally, we use what we call an EnhancedValidityState
data structure which is the same format as ValidityState
, plus any additional custom validations. This looks like the following:
let enhancedValidityState = {
badInput: false, // currently unused
customError: false, // currently unused
rangeOverflow: false, // Did we fail 'max'?
rangeUnderflow: false, // Did we fail 'min'?
patternMismatch: false, // Did we fail 'pattern'?
stepMismatch: false, // Did we fail 'step'?
tooLong: false, // Did we fail 'maxlength'?
tooShort: false, // Did we fail 'minlength'?
typeMismatch: false, // Did we fail 'type'?
valueMissing: false, // Did we fail 'required'?
valid: true, // Is the input valid?
// Custom validations are appended directly in here as well!
uniqueEmail: false, // Did we fail the unique email check?
};
Custom Validations
Custom validations are implemented as a sync or async function returning a boolean, and you add them directly into your formValidations
object where you define HTML validations:
const formValidations: FormValidations = {
name: {
required: true,
maxLength: 50,
},
emailAddress: {
required: true,
maxLength: 50,
async uniqueEmail(value) {
let res = await fetch(...);
let data = await res.json();
return data.isUnique === true;
},
},
}
Error Messages
Basic error messaging is handled out of the box by <Field>
for built-in HTML validations. If you are using custom validations, or if you want to override the built-in messaging, you can provide custom error messages through the <FormContext>
. Custom error messages can either be a static string, or a function that receives the attribute value (built-in validations only), the input name, and the input value:
const errorMessages = {
valueMissing: "This field is required",
tooLong: (attrValue, name, value) =>
`The ${name} field can only be up to ${attrValue} characters, ` +
`but you have entered ${value.length}`,
uniqueEmail: (_, name, value) =>
`The email address ${value} is already taken`,
};
<FormContext.Provider value={{ formValidations, errorMessages }}>
...
</FormContext.Provider>;
useValidatedInput()
This is the bread and butter of the library – and <Field>
is really nothing more than a wrapper around this hook. Let’s take a look at what it gives you. The only required input is the input name
:
let { info, getInputAttrs, getLabelAttrs, getErrorsAttrs } = useValidatedInput({
name: "firstName",
});
The returned info
value is of the following structure:
interface InputInfo {
// Has this input been blur'd?
touched: boolean;
// Has this input value changed?
dirty: boolean;
// Validation state, 'idle' to start and 'validating' during any
// custom async validations
state: "idle" | "validating" | "done";
// The current validity state of our input
validity?: ExtendedValidityState;
// Map of ExtendedValiditystate field => error message for all current errors
errorMessages?: Record<string, string>;
}
validity
contains the current validation state of the input. Most notably validity.valid
, tells you if the input is in a valid state.
errorMessages
is present if the input is invalid, and contains the error messages that should be displayed to the user (keyed by the field in validity
):
{
tooLong: 'The email field can only be up to 50 characters, but you have entered 60',
uniqueEmail: 'The email address [email protected] is already taken',
}
getInputAttrs
, getLabelAttrs
, and getErrorsAttrs
are prop getters that allow you to render you own custom <input>
/<label>
elements and error displays, while handling all of the validation attrs, id
, for
, aria-*
, and other relevant attribute for your form markup.
Let’s look at an example usage:
<div>
<label {...getLabelAttrs()}>Email Address*</label>
<input {...getInputAttrs()} />
{info.touched && info.errorMessages ? (
<ul {...getErrorsAttrs()}>
{Object.values(info.errorMessages).map((msg) => (
<li key={msg}>? {msg}</li>
))}
</ul>
) : null}
</div>
useValidatedInput
can also be used instead of FormContext
context for formValidations
and serverFormInfo
if necessary:
let { info } = iuseValidatedInput({
name: "emailAddress",
formValidations,
serverFormInfo,
});
Or, you can pass field-specific error message overrides that will be merged into the errorMessages
provided by the FormContext
:
let { info } = useValidatedInput({
name: "emailAddress",
errorMessages: {
required: "Please provide an email address",
},
});
Styling
This library aims to be pretty hands-off when it comes to styling, since every use-case is so different. We expect most consumers will choose to create their own custom markup with direct usage of useValidatedInput
. However, for simple use-cases of <Field />
we expose a handful of stateful classes on the elements you may hook into with your own custom styles:
rvs-label
– added to the built-in<label>
elementrvs-label--touched
– present when the input has been blur’drvs-label--dirty
– present when the input has been changedrvs-label--invalid
– present when the input is invalidrvs-label--validating
– present when the input is processing async validations
rvs-input
– added to the built-in<input>
elementrvs-input--touched
– present when the input has been blur’drvs-input--dirty
– present when the input has been changedrvs-input--invalid
– present when the input is invalidrvs-input--validating
– present when the input is processing async validations
rvs-validating
– present on the<p>
tag that displays aValidating...
message during async validationrvs-errors
– added to the built-in errors list<ul>
element
Not Yet Implemented
Currently, this library has only supports simple <input>
elements. The following items are not currently supported, but are planned:
- Radio Buttons
- Checkboxes
- Select
- Textarea