Validation
Ooko provides synchronous, typed validation for reactive form models without owning or mutating their values.
Typed model rules
Pass a reactive object and a partial rule map to useOokoValidation. Each rule receives the value associated with its key, and rule() only accepts keys configured in that map.
import { reactive } from "vue";
import { OokoValidator, useOokoValidation } from "ooko-ui/validation";
const user = reactive({
age: undefined as number | undefined,
city: "",
name: "",
});
const validator = useOokoValidation(user, {
age: (age) => ({
valid: age === undefined || age >= 18,
level: "error",
message: "You must be an adult.",
}),
name: [OokoValidator.required(), OokoValidator.minLength(3)],
});A rule returns an OokoValidationResult:
interface OokoValidationResult<TMeta = unknown> {
valid: boolean;
level: "info" | "warning" | "error";
message: string;
meta?: TMeta;
}Failed error results make the validator invalid. Failed info and warning results remain visible feedback but do not block submission. Successful results are not displayed.
API reference
Creates typed synchronous validation bindings for a reactive value object.
function useOokoValidation<TValues extends object, const TRules extends OokoValidationRules<TValues>>(
values: TValues,
rules: TRules,
): UseOokoValidationReturn<Extract<keyof TRules, string>>Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
values | TValues | yes | undefined | Reactive object whose current field values are validated. |
rules | TRules | yes | undefined | Rules keyed by fields in the value object. |
Returns
| Member | Type | Description |
|---|---|---|
rule | <TKey extends Extract<keyof TRules, string>>(key: TKey) => OokoValidationBinding | Returns the binding for one configured field key. |
check | () => boolean | Reveals every configured field and reports aggregate validity. |
getResults | () => readonly OokoValidationResult[] | Returns all current validation results without revealing them. |
isValid | () => boolean | Reports aggregate validity without revealing feedback. |
reset | () => void | Hides feedback for every configured field. |
Related types
| Type | Definition |
|---|---|
OokoValidationBinding | interface OokoValidationBinding { /** Failed results currently visible to the user. */ readonly visibleResults: ComputedRef<readonly OokoValidationResult[]>; /** Reveals feedback and reports whether blocking rules pass. */ check: () => boolean; /** Reports whether blocking rules pass without revealing feedback. */ isValid: () => boolean; /** Hides previously revealed feedback. */ reset: () => void; } |
OokoValidationController | interface OokoValidationController { /** Reveals every configured field and reports aggregate validity. */ check: () => boolean; /** Returns all current validation results without revealing them. */ getResults: () => readonly OokoValidationResult[]; /** Reports aggregate validity without revealing feedback. */ isValid: () => boolean; /** Hides feedback for every configured field. */ reset: () => void; [ookoValidationResolverKey]: (name: string) => OokoValidationBinding | undefined; } |
OokoValidationLevel | type OokoValidationLevel = "error" | "info" | "warning"; |
OokoValidationResult | interface OokoValidationResult<TMeta = unknown> { valid: boolean; level: OokoValidationLevel; message: string; meta?: TMeta; } |
OokoValidationRule | type OokoValidationRule<TValue, TMeta = unknown> = (value: TValue) => OokoValidationResult<TMeta>; |
OokoValidationRules | type OokoValidationRules<TValues extends object> = Partial<{ [TKey in Extract<keyof TValues, string>]: OokoValidationRule<TValues[TKey]> | readonly OokoValidationRule<TValues[TKey]>[]; }>; |
OptionalNumber | type OptionalNumber = number | null | undefined; |
OptionalText | type OptionalText = string | null | undefined; |
UseOokoValidationReturn | interface UseOokoValidationReturn<TRuleKey extends string> extends OokoValidationController { /** Returns the binding for one configured field key. */ rule: <TKey extends TRuleKey>(key: TKey) => OokoValidationBinding; } |
Related exports
| Export | Type | Default message | Description |
|---|---|---|---|
OokoValidator.accepted | { (message?: string): OokoValidationRule<boolean | null | undefined>; (message: string | undefined, value: boolean | null | undefined): OokoValidationResult; } | "This field must be accepted." | Requires a value to be strictly true. |
OokoValidator.email | { (message?: string): OokoValidationRule<OptionalText>; (message: string | undefined, value: OptionalText): OokoValidationResult; } | "Enter a valid email address." | Requires a string to match the HTML email-address syntax. |
OokoValidator.integer | { (message?: string): OokoValidationRule<OptionalNumber>; (message: string | undefined, value: OptionalNumber): OokoValidationResult; } | "This field must be an integer." | Requires an integer number. |
OokoValidator.max | { (maximum: number, message?: string): OokoValidationRule<OptionalNumber>; (maximum: number, message: string | undefined, value: OptionalNumber): OokoValidationResult; } | This field must be at most ${maximum}. | Requires a finite number less than or equal to the maximum. |
OokoValidator.maxLength | { (maximum: number, message?: string): OokoValidationRule<OptionalText>; (maximum: number, message: string | undefined, value: OptionalText): OokoValidationResult; } | This field must contain at most ${maximum} characters. | Requires a string to contain at most the given number of UTF-16 code units. |
OokoValidator.min | { (minimum: number, message?: string): OokoValidationRule<OptionalNumber>; (minimum: number, message: string | undefined, value: OptionalNumber): OokoValidationResult; } | This field must be at least ${minimum}. | Requires a finite number greater than or equal to the minimum. |
OokoValidator.minLength | { (minimum: number, message?: string): OokoValidationRule<OptionalText>; (minimum: number, message: string | undefined, value: OptionalText): OokoValidationResult; } | This field must contain at least ${minimum} characters. | Requires a string to contain at least the given number of UTF-16 code units. |
OokoValidator.oneOf | { <T>(allowed: readonly T[], message?: string): OokoValidationRule<(T extends string ? string : T extends number ? number : T extends boolean ? boolean : T) | null | undefined>; <T>(allowed: readonly T[], message: string | undefined, value: NoInfer<(T extends string ? string : T extends number ? number : T extends boolean ? boolean : T) | null | undefined>): OokoValidationResult; } | "Choose one of the allowed values." | Requires strict equality with one of the allowed values. |
OokoValidator.pattern | { (expression: RegExp, message?: string): OokoValidationRule<OptionalText>; (expression: RegExp, message: string | undefined, value: OptionalText): OokoValidationResult; } | "This field does not match the expected format." | Requires a string to match the supplied regular expression. |
OokoValidator.required | { (message?: string): OokoValidationRule<unknown>; (message: string | undefined, value: unknown): OokoValidationResult; } | "This field is required." | Requires a non-empty value and rejects whitespace-only strings. |
OokoValidator.sameAs | { <T>(expected: T | (() => T), message?: string): OokoValidationRule<(T extends string ? string : T extends number ? number : T extends boolean ? boolean : T) | null | undefined>; <T>(expected: T | (() => T), message: string | undefined, value: NoInfer<(T extends string ? string : T extends number ? number : T extends boolean ? boolean : T) | null | undefined>): OokoValidationResult; } | "This field must match the expected value." | Requires strict equality with a value or the current result of a getter. |
OokoValidator.url | { (message?: string): OokoValidationRule<OptionalText>; (message: string | undefined, value: OptionalText): OokoValidationResult; } | "Enter a valid HTTP or HTTPS URL." | Requires an absolute HTTP or HTTPS URL. |
Built-in rule behavior
All built-in rules return an error-level result. Every helper accepts a custom message.
All helpers can evaluate a standalone value by appending it after the message argument. Pass undefined as the message to use the default; explicitly passing an undefined value still runs the standalone check rather than returning a builder.
const requiredRule = OokoValidator.required("Enter a name.");
const ruleResult = requiredRule(user.name);
const standaloneRequired = OokoValidator.required("Enter a name.", user.name);
const standaloneLength = OokoValidator.minLength(3, "Keep typing.", user.name);
const standaloneEmail = OokoValidator.email(undefined, "person@example.com");
const standaloneAge = OokoValidator.min(18, undefined, user.age);
const standaloneAcceptance = OokoValidator.accepted(undefined, false);Presence, formats, and comparisons
requiredrejectsnull,undefined, empty strings, and whitespace-only strings. It still accepts0andfalse; useacceptedwhen the value must be strictlytrue.- Every other rule except
acceptedacceptsnull,undefined, and empty strings where its value type permits them. Combine withrequiredto enforce presence. Whitespace-only strings are not empty, and values are never trimmed or coerced. - Length rules use JavaScript string length (UTF-16 code units), as the existing
minLengthcontract does. Minimum and maximum bounds are inclusive. - Numeric rules accept numbers only.
minandmaxrejectNaNand infinities;integeradditionally rejects fractional numbers. emailchecks one address using the HTML email syntax, including local domains such asperson@localhost. It does not check mailbox existence or support display names and comma-separated address lists.urlparses an absolute HTTP(S) URL without DOM access. Relative URLs, other protocols, whitespace, and backslashes fail. URL validation is not a destination allowlist.patternpreserves the supplied regular expression's flags and matching semantics. Use anchors when the entire value must match. Repeated checks withgoryare deterministic and do not modify the original expression'slastIndex.oneOfaccepts readonly lists and uses strict equality without coercion. An empty list rejects every non-empty value.sameAsalso uses strict equality; objects are compared by reference.- Pass a getter to
sameAsto track another reactive field. Passing a value captures that value when the builder is created. Functions are interpreted as getters.
const credentials = reactive({ password: "", confirmation: "", role: "" });
const credentialsValidator = useOokoValidation(credentials, {
confirmation: [OokoValidator.required(), OokoValidator.sameAs(() => credentials.password, "Passwords must match.")],
role: OokoValidator.oneOf(["admin", "member"] as const),
});Changing credentials.password re-evaluates the confirmation rule even if confirmation itself has not changed. Feedback stays hidden until blur or check(), as for other rules.
Field and form integration
Pass a typed binding directly to a control:
<OInput v-model="user.name" label="Name" name="name" :validation="validator.rule('name')" />Alternatively, pass the controller to OForm. OInput and OTextarea resolve their binding from their name. A direct field binding takes precedence over the form binding.
<OForm :validation="validator">
<OInput v-model="user.name" label="Name" name="name" />
</OForm>Custom feedback is first revealed on blur, form submission, or validator.check(), then updates as the reactive model changes. Controlled error feedback takes presentation precedence over custom and native feedback.
State ownership and SSR
Each composable call owns independent state. It performs no browser work, persistence, or external synchronization and is deterministic during SSR and hydration.
Source organization
The public imports remain ooko-ui and ooko-ui/validation. Implementation modules are private and separated by responsibility:
src/composables/useOokoValidation.ts: reactive controller and per-field visibility.src/validation/OokoValidation.types.ts: public validation types.src/validation/OokoValidator.ts: the built-in rule catalog.src/validation/rules/: presence, text, format, number, and comparison rules, with colocated tests.src/validation/context.ts: private form/controller protocol.
Each rule family is small enough to read without region markers. The rule catalog has no Vue runtime dependency, and importing the composable alone does not include the built-in rules.