Skip to content

Latest commit

 

History

History
434 lines (331 loc) · 14.9 KB

File metadata and controls

434 lines (331 loc) · 14.9 KB

Validation

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.

<form validate> — Form-Level Validation

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).

Form Attributes

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)

$form Context API

Inside any <form validate>, the $form object is available in expressions.

Properties

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)

Methods

Method Description
$form.reset() Resets dirty/touched state, clears pending validators, calls native form.reset(), and re-validates

Per-Field State ($form.fields.<name>)

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>

Field Validation Rules

validate Attribute

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" />

Built-in Validators

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.

Field Attributes

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

Validation Triggers (validate-on)

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.


Conditional Validation (validate-if)

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.


Error Display

Inline Error Messages

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>

Custom Error Messages

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:

  1. error-{rule} attribute (e.g., error-required)
  2. error attribute (generic)
  3. Built-in default message

Error Templates

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")

Error Class

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).


Exposed Field State (as)

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>

Custom Validators

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>

Validator Function Signature

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
});

Auto-Disable Submit Buttons

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>

Standalone Field Validation

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).


Submit Handling

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>

Complete Example

<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 ←