Validation
Validation is how FrontAlign keeps form data clean before it ever reaches your server — and how it explains why an input failed, in language the user actually understands. Every form opted into the engine gets the same consistent behavior: native HTML5 constraints are extended with chainable parametric rules, custom regex patterns, and your own JavaScript validators, all evaluated client-side in real time, then reconciled with whatever JSON your server returns on submit.
There's no separate validation library to wire up, and no manual error <div> markup to maintain. Feedback elements are generated, positioned, and cleaned up automatically based on a single data-rule (or data-regex) attribute on the input itself.
This page covers:
- Rule-based validation — chainable, parametric rules like
min,max,email,match. - Custom regex — for patterns the built-in rules don't cover.
- Extending rules — registering your own validators via
Form.addRule(). - AJAX submission — with a live, working demo against a public test API.
How the Engine Works
Add fa-component="form" to any <form> element to opt in. The runtime automatically manages the form's lifecycle:
- Auto-Generated Feedback — You don't need to manually write
<div class="form-feedback">in your HTML. The engine creates and injects one below each field the first time it's needed, and removes/reuses it on subsequent validations. - Touched State (Blur & Live) — Fields are never validated the instant the page loads. A field is validated for the first time when it loses focus (blur). Once "touched", validation re-runs live on every input keystroke — so users aren't scolded before they've finished typing.
- Implicit Requirement — Any field carrying a
data-ruleattribute is treated as implicitlyrequired, even without the nativerequiredattribute. - Evaluation Order — Per field, the engine checks emptiness →
data-rule→data-regex, stopping at the first failure.data-regexis only evaluated if nodata-rulehas already failed — so the two are complementary fallbacks, not stacked checks. - Validation Styling — On submit, the form receives the
.is-validatedclass, native browser validation UI is suppressed, the first invalid field is auto-focused, and submission is blocked until every rule passes.
Rule-Based Validation (data-rule)
The core of FrontAlign validation is the data-rule attribute. Chain multiple rules with spaces, and pass parameters with a colon (:). Rules are evaluated left to right, and the first one that fails wins — its message is the one shown to the user.
<form fa-component="form" novalidate>
<div class="group">
<label class="form-label" for="v-username">Username</label>
<!-- Must be alphanumeric, min 4 chars, max 20 chars, no spaces -->
<input
class="form-input"
id="v-username"
type="text"
data-rule="alphanumeric min:4 max:20 no-spaces"
/>
</div>
<div class="group">
<label class="form-label" for="v-pass">Password</label>
<!-- Built-in strict password checking -->
<input
class="form-input"
id="v-pass"
type="password"
data-rule="password-strength"
/>
</div>
</form>
Available Rules
| Rule | Parameter Example | Description |
|---|---|---|
email | — | Validates standard email format. |
phone | — | Validates international phone numbers (strips spaces/dashes/parens before testing). |
number | — | Must be a valid numeric value (decimals allowed). |
integer | — | Must be a whole number (no decimals). |
alphanumeric | — | Only letters and numbers are allowed. |
url | — | Validates web addresses. |
password-strength | — | Requires 8+ chars, with at least one uppercase, one lowercase, and one number. |
no-spaces | — | Fails if the input contains any whitespace. |
Length & Value Rules
| Rule | Parameter Example | Description |
|---|---|---|
min | min:10 | Smart min: checks minimum value for numeric-looking input, or minimum length for text. |
max | max:100 | Smart max: checks maximum value for numeric-looking input, or maximum length for text. |
minlen | minlen:4 | Always checks string length, even if the value looks numeric (e.g. a zero-padded code). |
maxlen | maxlen:50 | Always checks string length, even if the value looks numeric. |
minval | minval:0 | Always checks the numeric value. Fails outright if the value isn't a valid number. |
maxval | maxval:99 | Always checks the numeric value. Fails outright if the value isn't a valid number. |
exact | exact:5 | String length must be exactly the specified number. |
Tip:
min/maxare convenient defaults that adapt to the value's shape. The moment that auto-detection becomes a liability — a string field that happens to hold only digits, like a postal code — switch to the explicitminlen/maxlen(always length) orminval/maxval(always numeric) pair.
Comparison & String Rules
| Rule | Parameter Example | Description |
|---|---|---|
match | match:#password | Value must exactly match the value of the target element (any valid CSS selector). |
contains | contains:abc | String must contain the target substring. |
startswith | startswith:https | String must start with the target substring. |
endswith | endswith:.com | String must end with the target substring. |
Custom Regex & Error Messages
When a pattern doesn't fit neatly into a reusable rule — a one-off SKU format, an internal ID scheme — reach for data-regex instead of registering a whole new rule for a single field. Override the default copy with data-required-msg and data-error-msg.
Note:
data-regexis a fallback, not an additional check. It only runs if the field has nodata-rulefailure already (see evaluation order above). A field can carry both attributes, but in practice you'll typically use one or the other per field.
<form fa-component="form" novalidate>
<div class="group">
<label class="form-label" for="v-custom">Product SKU</label>
<input
class="form-input"
id="v-custom"
type="text"
data-regex="^[A-Z]{3}-\d{4}$"
data-required-msg="A product SKU is required."
data-error-msg="Format must be 3 letters, a dash, then 4 digits (e.g. ABC-1234)."
required
/>
</div>
<button class="button is-primary" type="submit">Submit</button>
</form>
File Validation
File inputs get their own pair of rules — file-max and file-ext — plus automatic display text so users see what they've selected without any extra markup. Wrap the input in .group-file and add an element matching .upload-display inside that wrapper; FrontAlign keeps it in sync on every change event.
Note: Both file rules are no-ops on any field that isn't
type="file", and they pass automatically when no file is selected — pair them withrequiredif a file must be present.
<form fa-component="form" novalidate>
<div class="group-file">
<label class="form-label" for="v-file">Attachment</label>
<input
class="form-file"
id="v-file"
type="file"
name="attachment"
data-rule="file-max:5 file-ext:pdf,doc,docx"
data-error-msg="Only PDF or Word files up to 5MB are allowed."
/>
<div class="upload-display" data-default-text="No file selected">No file selected</div>
</div>
<button class="button is-primary" type="submit">Submit</button>
</form>
File Rules
| Rule | Parameter Example | Description |
|---|---|---|
file-max | file-max:5 | Maximum file size in megabytes. Fails if any selected file exceeds it — checked per file, so multi-file inputs are covered too. |
file-ext | file-ext:pdf,jpg,png | Comma-separated whitelist of allowed extensions (case-insensitive). Fails if any selected file's extension isn't in the list. |
Auto-Generated Selection Text
The .upload-display element inside .group-file is updated automatically whenever the file input's selection changes — no manual JS needed:
| Selection | Display Text |
|---|---|
| No files | The value of data-default-text on .group-file, or "No file selected". |
| One file | Selected: filename.ext |
| Multiple files | N items selected |
Extending Rules (JS API)
Register your own validation rules via Form.addRule(). Once registered, a custom rule works exactly like a built-in one — chainable, parametric, and usable from the data-rule attribute immediately.
import { Form } from './Form.js';
// Add a custom rule checking for a specific domain
Form.addRule('work-email', (val, param) => {
const domain = param || 'company.com';
return val.endsWith(`@${domain}`) ? true : `Must be a valid @${domain} email.`;
});
Warning: Registering a rule name that already exists overwrites the original silently in production and logs a console warning in development — useful for patching a built-in rule's behavior, but easy to do by accident with a generic name. Prefix custom rule names (e.g.
acme-tax-id) to avoid collisions with future built-in rules.
Built-in AJAX Submission
FrontAlign has a native fetch wrapper built directly into the form engine — no extra plugin required. Add data-ajax="true" to your form. Once client-side validation passes, the form automatically submits via fetch, manages a loading state on the submit button, guards against double submissions, enforces a request timeout, and dispatches custom events at every stage of the request lifecycle.
Configuration Attributes
| Attribute | Example | Default | Description |
|---|---|---|---|
data-ajax | data-ajax="true" | false | Required. Opts the form into AJAX submission once client-side validation passes. Without it, the form submits natively. |
action | action="/api/login" | current page URL | Standard HTML attribute. The endpoint the request is sent to. |
method | method="POST" | POST | Standard HTML attribute, read directly from the markup. If omitted, FrontAlign defaults to POST rather than the browser's native GET default. |
data-ajax-headers | | — | Optional JSON object of extra headers, merged into the request (CSRF tokens, auth headers, etc). Invalid JSON is ignored with a console warning — the request still proceeds. |
data-ajax-timeout | data-ajax-timeout="15000" | 30000 (30s) | Per-form request timeout in milliseconds. The request is aborted automatically if it's exceeded, firing fa.form.error with timeout: true. |
What Gets Sent Automatically
- Body — the form serialized as
FormData, so file inputs work without extra config. - Headers —
X-Requested-With: XMLHttpRequestandAccept: application/jsonare always included, merged with anything passed viadata-ajax-headers. - Re-entrancy guard — while a request is in flight, the submit button is disabled and any further submit on the same form is silently ignored — no duplicate requests from double-clicks or double-Enter.
- Loading state — the submit button receives the
.is-loadingclass for the duration of the request, and it's restored on success, error, or timeout.
Lifecycle Events
| Event | Fired when | event.detail |
|---|---|---|
fa.form.start | Right before the request is sent. | |
fa.form.success | Response received with a 2xx status. The form is reset and its validated/touched state is cleared automatically. | — data is the parsed JSON body, or null if the response wasn't valid JSON. |
fa.form.error | Non-2xx response, a network failure, or a timeout. | on a server error, or on a network failure/timeout. |
Live Demo: Submitting to a Real Endpoint
To see the full request lifecycle working end-to-end without standing up a backend, the demo below points at JSONPlaceholder — a free fake REST API. It always returns a real, valid JSON response; it just doesn't persist anything.
<form
fa-component="form"
id="ajax-signup"
data-ajax="true"
action="https://jsonplaceholder.typicode.com/posts"
method="POST"
novalidate
>
<div class="group">
<label class="form-label" for="demo-email">Email</label>
<input class="form-input" id="demo-email" name="email" type="email" data-rule="email" />
</div>
<div class="group">
<label class="form-label" for="demo-pass">Password</label>
<input class="form-input" id="demo-pass" name="password" type="password" data-rule="password-strength" />
</div>
<button class="button is-primary" type="submit">Create Account</button>
</form>
Try it: submit the form above with a valid email and an 8+ character password containing upper, lower, and numeric characters. JSONPlaceholder responds with
201 Created, FrontAlign treats that as success, resets the form, and the banner confirms the fake record's generatedid.
To see the error branch fire instead, point action at a path JSONPlaceholder doesn't recognize — it correctly responds with 404, which FrontAlign treats as a failed submission and routes to fa.form.error:
<form fa-component="form" data-ajax="true" action="https://jsonplaceholder.typicode.com/no-such-route" method="POST" novalidate>
...
</form>
Automatic Server-Side Field Errors
If your backend returns a 4xx/5xx JSON response shaped like the table below, FrontAlign automatically maps each error onto its matching field — no manual DOM work required. It reuses the same feedback-element generation as client-side rules and marks the element .from-server (see Server Feedback below).
| Response shape | Example | Behavior |
|---|---|---|
| | Message is shown next to the field matching [name="email"]. |
| | Only the first message in the array is displayed. |
The matched field is keyed by its name attribute, so make sure your inputs have one (e.g. name="email") in addition to id. Any response shape FrontAlign doesn't recognize simply skips auto-mapping; you can still read data from fa.form.error and build your own UI.
Server Feedback
Often you need to show errors that come back from the server (like "Username already taken").
Use .form-feedback.is-server combined with .with-error or .with-success.
Bonus: the FrontAlign runtime automatically clears these server messages the next time the user attempts to submit the form, ensuring fresh validation state.
<div class="group">
<label class="form-label" for="srv-user">Username</label>
<input class="form-input user-invalid" id="srv-user" type="text" value="john_doe" />
<!-- Server message. Will be automatically cleared on next form submit -->
<div class="form-feedback is-server with-error">This username is already registered.</div>
</div>
<div class="group">
<label class="form-label" for="srv-promo">Promo code</label>
<input class="form-input user-valid" id="srv-promo" type="text" value="WELCOME10" />
<div class="form-feedback is-server with-success">Code valid — 10% discount applied.</div>
</div>
Select, Radio & Checkbox Validation
The same data-rule, data-required-msg, and data-error-msg attributes work on <select>, radio groups, and checkboxes — but each control type has slightly different semantics worth knowing.
Select
A <select> carrying data-rule or the native required attribute is treated as implicitly required. The engine reads the selected option's value (not its label), so make sure your placeholder option has an empty value — that's what the emptiness check tests against.
<form fa-component="form" novalidate>
<div class="group">
<label class="form-label" for="v-country">Country</label>
<select
class="form-select"
id="v-country"
name="country"
data-rule="required"
data-required-msg="Please select your country."
>
<option value="">— Select a country —</option>
<option value="az">Azerbaijan</option>
<option value="tr">Turkey</option>
<option value="de">Germany</option>
</select>
</div>
<button class="button is-primary" type="submit">Submit</button>
</form>
Tip: The feedback element is injected immediately after the
<select>tag, just like with text inputs. If your select is wrapped in a custom styled container, make sure the container is the.groupelement so positioning stays consistent.
Radio Groups
For radio buttons, place data-rule="required" (or data-required-msg) on any one input in the group — FrontAlign detects all inputs sharing the same name and validates them as a unit. The feedback element is injected after the last radio in the group.
Each radio + label pair gets its own .group-radio.group-inline wrapper. Stack them inside a fieldset so they render as a tidy inline row, and FrontAlign's group-detection still works correctly because all inputs share the same name.
<form fa-component="form" novalidate>
<fieldset class="group">
<legend class="form-label">Preferred contact method</legend>
<div class="group-radio is-inline">
<input
class="form-radio"
type="radio"
name="contact"
value="email"
id="r-email"
data-rule="required"
data-required-msg="Please choose a contact method."
/>
<label class="form-label" for="r-email">Email</label>
</div>
<div class="group-radio is-inline">
<input class="form-radio" type="radio" name="contact" value="phone" id="r-phone" />
<label class="form-label" for="r-phone">Phone</label>
</div>
<div class="group-radio group-inline">
<input class="form-radio" type="radio" name="contact" value="sms" id="r-sms" />
<label class="form-label" for="r-sms">SMS</label>
</div>
</fieldset>
<button class="button is-primary" type="submit">Submit</button>
</form>
Note: Touched state for radio groups fires when focus leaves the entire group (i.e. the user tabs past the last radio), not on each individual input's blur — this prevents the error message from flashing while the user is still cycling through options with arrow keys.
Checkboxes
Single checkboxes (e.g. "I agree to the terms") use data-rule="required" to assert the box must be checked. The engine checks the checked property, not the value. Use .group-checkbox.group-inline to keep the input and its label aligned on the same line.
<form fa-component="form" novalidate>
<div class="group-checkbox group-inline">
<input
class="form-checkbox"
type="checkbox"
name="terms"
id="terms"
data-rule="required"
data-required-msg="You must accept the terms to continue."
/>
<label class="form-label" for="terms">
I agree to the <a href="/terms">Terms of Service</a>
</label>
</div>
<button class="button is-primary" type="submit">Submit</button>
</form>
Checkbox Groups (min checked)
When you need the user to check at least N boxes from a group, use data-rule="min-checked:N" on any one checkbox in the group. Like radio groups, all checkboxes sharing the same name are treated as a unit. Each checkbox + label pair gets its own .group-checkbox.group-inline wrapper, stacked inside a fieldset.
<form fa-component="form" novalidate>
<fieldset class="group">
<legend class="form-label">Interests (pick at least 2)</legend>
<div class="group-checkbox group-inline">
<input
class="form-checkbox"
type="checkbox"
name="interests"
value="design"
id="i-design"
data-rule="min-checked:2"
data-error-msg="Please select at least 2 interests."
/>
<label class="form-label" for="i-design">Design</label>
</div>
<div class="group-checkbox group-inline">
<input class="form-checkbox" type="checkbox" name="interests" value="code" id="i-code" />
<label class="form-label" for="i-code">Code</label>
</div>
<div class="group-checkbox group-inline">
<input class="form-checkbox" type="checkbox" name="interests" value="marketing" id="i-marketing" />
<label class="form-label" for="i-marketing">Marketing</label>
</div>
<div class="group-checkbox group-inline">
<input class="form-checkbox" type="checkbox" name="interests" value="data" id="i-data" />
<label class="form-label" for="i-data">Data</label>
</div>
</fieldset>
<button class="button is-primary" type="submit">Submit</button>
</form>
Available Rules Summary
| Control | Rule | What it checks |
|---|---|---|
<select> | required | Selected option value must not be empty. |
| Radio group | required | At least one radio in the group must be checked. |
| Single checkbox | required | The checkbox must be checked. |
| Checkbox group | min-checked:N | At least N checkboxes sharing the same name must be checked. |
Floating-Label Validation
The validation engine works the same way inside a .floating-label wrapper — no separate setup. Just keep the <label> as the input's or select's immediate sibling like any other floating-label field, and add data-rule / data-regex as usual. Auto-generated feedback is still injected right after the control, outside the .floating-label wrapper.
<form fa-component="form" novalidate>
<div class="group floating-label">
<input
class="form-input"
id="fl-email"
name="email"
type="email"
placeholder=" "
data-rule="email"
data-required-msg="Email is required."
data-error-msg="Enter a valid email address."
/>
<label class="form-label" for="fl-email">Email</label>
</div>
<div class="group floating-label">
<select
class="form-select"
id="fl-country"
name="country"
data-rule="required"
data-required-msg="Please select your country."
>
<option value=""></option>
<option value="az">Azerbaijan</option>
<option value="tr">Turkey</option>
<option value="de">Germany</option>
</select>
<label class="form-label" for="fl-country">Country</label>
</div>
<button class="button is-primary" type="submit">Submit</button>
</form>
Note: For text inputs and textareas,
placeholder=" "(a single space) is required — the:not(:placeholder-shown)selector driving the label's float animation only works when the input has some placeholder value. Selects don't need it, since their floating-label state is driven separately.
Complete Match & Validation Example
A realistic password reset form demonstrating match, password-strength, auto-generated feedback, and AJAX submission — wired to the same JSONPlaceholder demo endpoint, so it's fully runnable as-is.
<form
fa-component="form"
id="reset-form"
data-ajax="true"
action="https://jsonplaceholder.typicode.com/posts"
method="POST"
novalidate
>
<div class="group">
<label class="form-label" for="new-pass">New Password</label>
<input
class="form-input"
id="new-pass"
name="password"
type="password"
data-rule="password-strength"
data-error-msg="Must be 8+ chars with uppercase, lowercase, and numbers."
/>
</div>
<div class="group">
<label class="form-label" for="confirm-pass">Confirm Password</label>
<!-- Automatically ensures it matches the value of #new-pass -->
<input
class="form-input"
id="confirm-pass"
name="password_confirmation"
type="password"
data-rule="match:#new-pass"
/>
</div>
<button class="button is-primary" type="submit">Update Password</button>
</form>