The Validation module is included in NoJS Elements. When using the CDN, it is auto-installed — no separate registration is needed. For ESM usage, call
NoJS.use(NoJSElements)once.
Add validate to a <form> to enable declarative form validation. Creates a $form context object that tracks validity, dirty/touched state, errors, and values for all named fields inside the form.
<!-- Basic validated form -->
<div state="{ name: '', email: '' }">
<form validate on:submit.prevent="post('/api/register', $form.values)">
<input name="name" model="name" validate="required" placeholder="Name" />
<span if="$form.fields.name?.touched && $form.fields.name?.error"
bind="$form.fields.name.error"
style="color: red"></span>
<input name="email" model="email" validate="required|email" placeholder="Email" />
<span if="$form.fields.email?.touched && $form.fields.email?.error"
bind="$form.fields.email.error"
style="color: red"></span>
<button type="submit">Register</button>
</form>
</div>Native browser validation popups are disabled automatically (novalidate is set on the form).
| Attribute | Type | Default | Description |
|---|---|---|---|
validate |
boolean attr | required | Enables form-level validation and creates $form context |
validate-on |
string | "input change focusout" |
Space-separated list of events that trigger validation (form-wide default) |
error-class |
string | — | CSS class applied to invalid fields (form-wide default, overridden per-field) |
Inside any <form validate>, the $form object is available in expressions.
| Property | Type | Description |
|---|---|---|
$form.valid |
boolean | true when all fields pass validation. Reflects real validity regardless of touched/dirty state |
$form.dirty |
boolean | true when any field value has changed |
$form.touched |
boolean | true when any field has been focused and blurred |
$form.submitting |
boolean | true during the submit event handler (auto-resets on next frame) |
$form.pending |
boolean | true when any async validators are in-flight |
$form.errors |
object | Map of field name to error message — only includes errors for interacted (touched or dirty) fields |
$form.values |
object | Map of field name to current value. Checkbox fields map to true/false |
$form.firstError |
string | null | The first error message (among interacted fields), or null |
$form.errorCount |
number | Number of interacted fields with errors |
$form.fields |
object | Per-field state objects (see below) |
| Method | Description |
|---|---|
$form.reset() |
Resets dirty/touched state, clears pending validators, calls native form.reset(), and re-validates |
Each named field gets a state object accessible via $form.fields.<fieldName>:
| Property | Type | Description |
|---|---|---|
valid |
boolean | true when this field passes all rules |
dirty |
boolean | true when this field's value has changed |
touched |
boolean | true when this field has been focused and blurred |
error |
string | null | The highest-priority error message, or null |
value |
any | Current field value |
<div state="{}">
<form validate>
<input name="email" model="email" validate="required|email" />
<!-- Show error only after interaction -->
<span if="$form.fields.email?.touched && !$form.fields.email?.valid"
bind="$form.fields.email.error"
style="color: red"></span>
<!-- Disable submit while invalid -->
<button type="submit" disabled="!$form.valid">Save</button>
<!-- Show overall form state -->
<p if="$form.firstError" bind="'Error: ' + $form.firstError"></p>
<p bind="'Errors: ' + $form.errorCount"></p>
</form>
</div>Add to <input>, <textarea>, or <select> to define validation rules. Rules are pipe-separated.
<input name="email" validate="required|email" />
<input name="age" type="number" validate="required|min:18|max:120" />
<input name="website" validate="url" />
<input name="code" validate="required|custom:productCode" />| Rule | Arguments | Error message | Description |
|---|---|---|---|
required |
— | "Required" |
Value must not be empty or whitespace |
email |
— | "Invalid email" |
Must match user@domain.tld pattern |
url |
— | "Invalid URL" |
Must be a valid URL (via new URL()) |
min |
min:N |
"Minimum value is N" |
Numeric value must be >= N |
max |
max:N |
"Maximum value is N" |
Numeric value must be <= N |
custom |
custom:name |
(from validator) | Calls a custom validator registered via NoJS.validator() |
In addition, native HTML5 validation attributes (required, minlength, maxlength, pattern, type="email", type="url", min, max, step) are also checked via the browser's ValidityState API. NoJS-specific validators take priority over native ones when both detect the same rule.
| Attribute | Type | Default | Description |
|---|---|---|---|
validate |
string (pipe-separated) | — | Validation rules (e.g., "required|email") |
validate-on |
string | (inherits from form) | Space-separated events: "input", "focusout", "blur", "submit" |
validate-if |
expression | — | Expression that must be truthy for this field to be validated |
error |
string | "#templateId" |
— | Generic error message or template reference |
error-{rule} |
string | "#templateId" |
— | Error message or template for a specific rule (e.g., error-required="Name is required") |
error-class |
string | (inherits from form) | CSS class applied when this field is invalid |
as |
string | — | Exposes the field state as a named variable in context (e.g., as="emailField" makes emailField.valid, emailField.error available) |
name |
string | required | Field name — used as key in $form.fields, $form.values, and $form.errors |
Control when validation runs and errors become visible.
<!-- Form-wide: validate on blur only -->
<form validate validate-on="focusout">
<input name="email" validate="required|email" />
</form>
<!-- Per-field override -->
<form validate>
<input name="name" validate="required" validate-on="input" />
<input name="email" validate="required|email" validate-on="focusout" />
<input name="code" validate="required" validate-on="submit" />
</form>| Trigger | Behavior |
|---|---|
input |
Validates on every keystroke |
change |
Validates when the field value is committed (e.g., select change, checkbox toggle, date picker close) |
focusout / blur |
Validates when the field loses focus |
submit |
Validates only on form submission |
The default triggers are input, change, and focusout. When validate-on is set on the form, it applies to all fields unless overridden per-field.
Regardless of the validate-on setting, $form.valid always reflects the real-time validity of the form (for submit button state). The trigger only controls when error messages and error-class become visible.
Skip validation for fields based on a reactive expression.
<div state="{ hasAddress: false }">
<form validate>
<label>
<input type="checkbox" model="hasAddress" name="hasAddress" />
Add shipping address
</label>
<input name="street" model="street" validate="required"
validate-if="hasAddress" placeholder="Street" />
<input name="city" model="city" validate="required"
validate-if="hasAddress" placeholder="City" />
<button type="submit">Order</button>
</form>
</div>When validate-if evaluates to falsy, the field is treated as valid and excluded from $form.errors.
The simplest approach: use $form.fields to show errors inline.
<form validate>
<input name="email" model="email" validate="required|email" />
<span if="$form.fields.email?.touched && $form.fields.email?.error"
bind="$form.fields.email.error"
class="error"></span>
</form>Override the default message per-field or per-rule.
<input name="email"
validate="required|email"
error="Please enter a valid email"
error-required="Email is required"
error-email="That doesn't look like an email address" />Error resolution priority:
error-{rule}attribute (e.g.,error-required)errorattribute (generic)- Built-in default message
Reference a <template> for structured error rendering.
<input name="email" validate="required|email" error="#email-error" />
<template id="email-error">
<div class="field-error">
<span bind="$error"></span>
</div>
</template>Inside error templates, these variables are available:
$error— the error message string$rule— the failing rule name (e.g.,"required","email")
Add a CSS class to invalid fields for styling.
<!-- Form-wide error class -->
<form validate error-class="border-red">
<input name="name" validate="required" />
<input name="email" validate="required|email" />
</form>
<!-- Per-field override -->
<form validate error-class="border-red">
<input name="name" validate="required" error-class="bg-red-50 border-red" />
</form>The error class is applied only after the field has been interacted with (touched or dirty).
Use the as attribute to expose a field's validation state as a named variable.
<form validate>
<input name="email" model="email" validate="required|email" as="emailField" />
<!-- Use the named variable -->
<span if="emailField?.touched && !emailField?.valid"
bind="emailField.error"></span>
<span if="emailField?.valid">Valid!</span>
</form>Register custom validation functions via NoJS.validator(). This API remains in NoJS Core and works with the Elements validation module.
<script>
NoJS.validator('phone', (value) => {
if (!value) return null; // let "required" handle empty
if (!/^\+?[\d\s\-()]{7,}$/.test(value)) return 'Invalid phone number';
return null; // null = valid
});
NoJS.validator('matchField', (value, fieldName, allValues) => {
if (value !== allValues[fieldName]) return `Must match ${fieldName}`;
return null;
});
</script>
<form validate>
<input name="phone" validate="required|custom:phone" />
<input name="password" type="password" model="password" />
<input name="confirm" type="password" validate="required|custom:matchField:password" />
</form>NoJS.validator(name, (value, ...args, allValues) => {
// value — the field value (string)
// args — arguments after : in the rule (e.g., "min:5" → args = ["5"])
// allValues — object of all field values in the form { fieldName: value }
//
// Return:
// null or true → valid
// string → error message
});Submit buttons (<button> without type="button" and <input type="submit">) are automatically disabled when the form is invalid or has pending async validators.
<form validate>
<input name="email" validate="required|email" />
<!-- This button is auto-disabled until the form is valid -->
<button type="submit">Submit</button>
</form>To opt out for a specific button, give it an explicit disabled expression:
<button type="submit" disabled="!$form.valid || isLoading">Submit</button>Fields with validate can also be used outside a <form validate> for simple inline validation.
<div state="{ code: '' }">
<input model="code" validate="required" error="#code-err" />
<template id="code-err">
<span class="error" bind="err.message"></span>
</template>
</div>In standalone mode, validation runs on input events. The error template receives err.message (note: err, not $error).
The validate directive sets $form.submitting = true during the submit event, marks all fields as touched (making all errors visible), and re-validates. After the event handler completes, $form.submitting resets to false on the next animation frame.
<div state="{}">
<form validate on:submit.prevent="post('/api/submit', $form.values)">
<input name="name" model="name" validate="required" />
<input name="email" model="email" validate="required|email" />
<button type="submit">
<span hide="$form.submitting">Submit</span>
<span show="$form.submitting">Submitting...</span>
</button>
</form>
</div><div state="{ showAddress: false }">
<form validate error-class="border-red-500" on:submit.prevent="post('/api/register', $form.values)">
<h3>Account</h3>
<div>
<label>Name</label>
<input name="name" model="name" validate="required"
error-required="Please enter your name" />
<span if="$form.fields.name?.touched && $form.fields.name?.error"
bind="$form.fields.name.error" class="text-red-500"></span>
</div>
<div>
<label>Email</label>
<input name="email" model="email" type="email" validate="required|email"
error-required="Email is required"
error-email="Please enter a valid email" />
<span if="$form.fields.email?.touched && $form.fields.email?.error"
bind="$form.fields.email.error" class="text-red-500"></span>
</div>
<div>
<label>Age</label>
<input name="age" model="age" type="number" validate="required|min:18|max:120" />
<span if="$form.fields.age?.touched && $form.fields.age?.error"
bind="$form.fields.age.error" class="text-red-500"></span>
</div>
<h3>Shipping</h3>
<label>
<input type="checkbox" model="showAddress" name="showAddress" />
Add shipping address
</label>
<div show="showAddress">
<input name="street" model="street" validate="required"
validate-if="showAddress" placeholder="Street" />
<input name="city" model="city" validate="required"
validate-if="showAddress" placeholder="City" />
</div>
<div>
<p if="$form.firstError" bind="$form.firstError" class="text-red-500"></p>
<button type="submit">Register</button>
<button type="button" on:click="$form.reset()">Reset</button>
</div>
</form>
</div>Previous: Drag and Drop ←