Overview

A validator is (value, signal?) => string | null | Promise<…>. Built-ins are factories: call them to get the function. Message is always the last optional argument.

On formAtom / useForm, validators must be sync. On schema / validateObject, they may return a Promise and receive an AbortSignal — see Async Validation.

This page lists everything re-exported from lib/index.ts. Additional factories exist under lib/Mutables/form/validators.ts but are not public until exported.

Where validators run

Context
Async?
Signal
formAtom Sync only Promise throws
validateObject / schema Async OK Second arg AbortSignal

Not public (source only)

Present in lib/Mutables/form/validators.ts but omitted from lib/index.ts: isUrl, isGoodPassword / isStrongPassword / isStrongestPassword, hasCapitalLetter / hasLowercaseLetter / hasNumber / hasSpecialCharacter, isFloat / isInteger / isPositive / isNegative, isGreaterThan / isLessThan / isBetween / isDateBetween, date helpers (isAfter, isBefore, isPast, isFuture, isInTheLast24Hours, …), and all check* boolean utilities. Standalone toFloat / toValidNumber / toFloatString are also source-only — use FormAtom methods.

Public catalog (playground)

isEmail, isPattern, min/max length, numeric bounds, isOneOf, withMessage.

import {
html, formAtom, validateAll, renderApp,
isRequired, isEmail, isPattern, minLength, maxLength,
isMin, isMax, isOneOf, withMessage,
} from "mates";

const App = () => {
const email = formAtom("", [isRequired(), isEmail("Need a real email")]);
const handle = formAtom("", [
isPattern(/^[a-z]+$/, "lowercase letters only"),
minLength(3),
maxLength(12),
]);
const age = formAtom(0, [isMin(18), isMax(120)]);
const role = formAtom("", [isOneOf(["admin", "editor", "viewer"])]);
const bio = formAtom("", [withMessage(minLength(8), "Tell us a bit more")]);
const msg = formAtom("");

const hint = (fa) =>
fa.dirty && fa.errors[0]
? html`<span class="tag tag-danger">${fa.errors.join(" · ")}</span>`
: "";

return () => html`
<div class="m-col m-gap">
<label class="m-col m-gap-xs">Email ${hint(email)}
<input .value=${email()} @input=${(e) => email.set(e.target.value)} />
</label>
<label class="m-col m-gap-xs">Handle ${hint(handle)}
<input .value=${handle()} @input=${(e) => handle.set(e.target.value)} />
</label>
<label class="m-col m-gap-xs">Age ${hint(age)}
<input type="number" .value=${age()} @input=${(e) => age.set(+e.target.value)} />
</label>
<label class="m-col m-gap-xs">Role ${hint(role)}
<select .value=${role()} @change=${(e) => role.set(e.target.value)}>

Custom validators

Any (value) => string | null works alongside factories. Return null when valid.

import { html, formAtom, isRequired, minLength, renderApp } from "mates";

const notAdmin = (value) =>
value.toLowerCase() === "admin" ? "Username is taken" : null;

const slugOk = (value) =>
/^[a-z0-9-]+$/.test(value) ? null : "Use lowercase letters, numbers, and dashes";

const App = () => {
const username = formAtom("admin", [isRequired(), minLength(3), notAdmin, slugOk]);

return () => html`
<div class="m-col m-gap">
<label class="m-col m-gap-xs">
Username
<input .value=${username()} @input=${(e) => username.set(e.target.value)} />
</label>
<div class="m-flex m-flex-wrap m-gap-sm m-items-center">
<span class="tag ${username.isValid ?"tag-success" : "tag-danger"}">
isValid ${username.isValid}
</span>
${username.dirty && username.errors[0]
? html`<span class="tag tag-danger">${username.errors[0]}</span>`
: ""}
</div>
</div>
`;
};

renderApp(App, document.getElementById("app"));