Validate forms
Rules sit on each field, messages can be set once for a form or the whole app, async checks run only when the value is otherwise valid, and the same rules check a request on the server. Every example on this page was type-checked and run against oks-ui 1.5.0.
Rules on fields
Give a field a name and a validation object inside a Form. rules lists what the value must satisfy, and message replaces the default text for one rule. The form checks a field when it loses focus and every field on submit, and onSubmit runs only when all of them pass.
import { Form, FormFieldSet } from "oks-ui/form-field-set";
import { Button } from "oks-ui/button";
<Form onSubmit={(data) => save(data)}>
<FormFieldSet type="email" name="email" label="Email"
validation={{ rules: { required: true, email: true } }} />
<FormFieldSet type="password" name="password" label="Password"
validation={{ rules: { required: true, strongPassword: { minLength: 10 } } }} />
<FormFieldSet type="password" name="confirm" label="Repeat password"
validation={{
rules: { required: true, matchField: "password" },
message: { matchField: "The passwords do not match" },
}} />
<Button type="submit">Create account</Button>
</Form>The built-in rules, grouped by what they check:
- Presence:
required, andrequiredIf, which makes a field required only when another field matches ({ field: "plan", equals: "business" }). - Text:
minLength,maxLength,pattern,email,url(http and https only unless you list other protocols),alpha,alphanumeric,json. - Numbers:
numeric,integer,min,max. - Choices and lists:
oneOf,minItems,maxItems. On a list, the text rules check every item. - Dates:
minDateandmaxDate, given as an ISO date, aDateor"now". - Other fields and files:
matchField,strongPassword,accept,maxFileSize,maxFiles.
Async checks, such as a taken username
A custom rule may return a Promise. It runs only after the field’s other rules pass, so a value that is already wrong is never sent to your server. Return true to pass, or a message string to fail.
<FormFieldSet
type="text"
name="username"
label="Username"
validation={{
rules: {
required: true,
minLength: 3,
custom: async (value) => ((await isTaken(String(value))) ? "That username is taken" : true),
},
}}
/>Checked on its own with validateFieldAsync, the value "taken" gives { custom: "That username is taken" }, and "ab" gives { minLength: "Must be at least 3 characters" } without calling the async check at all.
In a Form, the check runs on blur and on submit, only the answer for the latest value counts, and onSubmit waits for it. If the check throws (a network error), the field shows “This value could not be checked. Please try again.” and the submit is blocked. useFormContext() exposes isValidating if you want to show a spinner.
Messages for a form or the whole app
A form’s messages prop replaces the default text for every field in it. A message can be a function of the rule’s argument, which is what a translation needs.
<Form onSubmit={save} messages={{ required: "Bitte ausfüllen", minLength: (min) => `Mindestens ${min} Zeichen` }}>
<FormFieldSet type="text" name="name" label="Name" validation={{ rules: { required: true, minLength: 2 } }} />
</Form>For the whole app, call setValidationMessages once. It returns a function that puts the previous messages back. With LocaleProvider, put translated messages in labels.validation instead.
import { setValidationMessages } from "oks-ui/validation";
setValidationMessages({ email: "Enter an email address like name@example.com" });The order, strongest first: a field’s own message, the form’s messages, LocaleProvider’s labels.validation, setValidationMessages, then the defaults.
Rules of your own
registerRule adds a rule that any schema can use by name, and returns a function that removes it. Declare its argument so a schema that uses it still type-checks.
import { registerRule } from "oks-ui/validation";
declare module "oks-ui/validation" {
interface CustomValidationRules {
slug: boolean;
}
}
registerRule(
"slug",
(value) => typeof value !== "string" || value === "" || /^[a-z0-9-]+$/.test(value),
"Use lowercase letters, numbers and hyphens",
);Import from "oks-ui" instead and declare the module "oks-ui". Built-in names cannot be replaced.
The same rules on the server
oks-ui/validation imports nothing from React, so a Server Action, a route handler or a plain Node.js script can use it. parseForm validates and converts in one step: each field’s type turns "12" into 12 and "on" into true, and only the schema’s keys are kept, so a field the schema does not know never gets through.
import { defineSchema, parseForm, type InferData } from "oks-ui/validation";
const postSchema = defineSchema({
title: { type: "string", rules: { required: true, maxLength: 120 } },
slug: { type: "string", rules: { required: true, slug: true } },
views: { type: "number", rules: { min: 0, integer: true } },
published: { type: "boolean" },
tags: { type: "array", each: { type: "string", rules: { maxLength: 30 } }, rules: { maxItems: 5 } },
seo: { type: "object", fields: { canonical: { type: "string", rules: { url: true } } } },
});
type Post = InferData<typeof postSchema>;
export async function POST(request: Request) {
const result = await parseForm(await request.json(), postSchema);
if (!result.ok) return Response.json({ errors: result.errors }, { status: 400 });
const post: Post = result.data;
// ...save post
}Sent a good request with an extra field, it returns the converted data without the extra field:
// input: { title: "Hello", slug: "hello-world", views: "12", published: "on",
// tags: ["react"], seo: { canonical: "https://example.com/hello" }, isAdmin: true }
{ ok: true, data: { title: "Hello", slug: "hello-world", views: 12, published: true,
tags: ["react"], seo: { canonical: "https://example.com/hello" } } }Sent a bad one, it returns the first error per field, keyed by path for nested values:
{ ok: false, errors: {
"title": "This field is required",
"slug": "Use lowercase letters, numbers and hyphens",
"views": "Please enter a whole number",
"tags.0": "Must be no more than 30 characters",
"seo.canonical": "Please enter a valid URL",
} }parseFormSync is the same for schemas without async rules. To get every failing message per field instead of the first, pass { allErrors: true }.
Schemas stored as JSON
A schema saved in a database or written by a plugin may contain a typo. By default an unknown rule is skipped with a warning in development, which would leave the field unchecked. Check stored schemas with parseSchema, which throws an error that names the problem, and pass { strict: true } on the server so an unknown rule is an error rather than a skipped check.
import { parseSchema, validateForm } from "oks-ui/validation";
const schema = parseSchema(JSON.parse(stored));
// a rule spelled "requried" throws:
// oks-ui validation: field "a" uses an unknown rule "requried". Check the spelling,
// or add it with registerRule() before checking the schema.
validateForm(data, schema, { strict: true, allErrors: true });
// { email: ["Please enter a valid email address", "Must be at least 5 characters"] }Limits
- Validation in the browser is for the person filling in the form. Always check the request again on the server; the same schema makes that one call.
setValidationMessagesandregisterRulechange module state that everything in the process shares. A server that answers in several languages at once should passmessagesper call.- Patterns written by people you do not trust can be very slow on some inputs. Only let trusted people define them.
- Everything above is in 1.5.0 and later. See What’s new in 1.5.