Skip to content

About

Export Laravel validation rules as a JSON schema and check them in the browser — three-valued, so it never claims a server-only rule passed.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

113 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

laranail/validation-js

Tests Static analysis License: MIT

laranail/validation-js is not published to Packagist, so there is no registry-version badge to show: see Install.

Export your Laravel validation rules as a JSON schema and check them in the browser — with no round trip, no duplicated rule definitions, and no lying about what the client can actually decide.

Targets PHP ^8.4.1 on Laravel ^13, and Node >=22.6. The JavaScript has zero dependencies, runtime and dev.

The part most client-side validation gets wrong

A browser cannot check unique, exists or active_url. Libraries that quietly treat those as passing show the user a green tick for input the server is about to reject.

This one is three-valued. A field is invalid, valid, or undetermined — carrying at least one rule only the server can decide:

const { valid, failures, undetermined } = validate(values, schema);

A rule the exporter does not recognise goes to the server, including custom rules, package rules, and rules added by a future Laravel version. The cost of that default is a round trip; the cost of the other one is a false green tick.

Install

composer require laranail/validation-js
npm install @laranail/validation-js

@laranail/validation-js is published to GitHub Packages, not npm, while npm cannot take a publish from this repository (npm/cli#9969). GitHub Packages asks for a token even for a public package, so before npm install add a GitHub token with read:packages to the project's .npmrc:

@laranail:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

See npm or GitHub Packages.

Quick start guide and usage

Getting started

Nothing is required after the two installs: ValidationJsServiceProvider is auto-discovered and the npm package has zero runtime dependencies. Publish the config only when you want the transport endpoints or different runtime defaults:

php artisan vendor:publish --tag=laranail::validation-js-config

Usage

Export on the server:

use Simtabi\Laranail\ValidationJs\RuleExporter;

$schema = app(RuleExporter::class)->export(
    (new StoreUserRequest())->rules(),
    attributes: ['email' => 'Email address'],
);

Check in the browser:

import { validate } from '@laranail/validation-js';

const result = validate({ email: '', age: '15' }, schema);

result.valid;         // false
result.failures;      // [{ field: 'email', rule: 'required', message: 'The Email address field is required.' }]
result.undetermined;  // ['email'] — it also carries `unique`, which only the server can answer

Submit when valid is true, even if undetermined is non-empty. The server gives the real answer; the browser just saves the user a round trip on the things it can be sure about.

How the parity claim is tested

The PHP suite runs Laravel's own validator over a grid of 511 rule-and-value combinations, records its verdicts, and writes them to a fixture. The JavaScript suite then has to reproduce every one.

That is the only honest way to claim the runner matches Laravel. Asserting it against hand-written expectations would prove it matches what its author believed Laravel does — and while building this, four of those beliefs turned out to be wrong:

I assumed Laravel actually
email needs a dot in the domain accepts a@b — the default validation is RFC, not "has a TLD"
max:5 compares "6" numerically compares string LENGTH unless a numeric rule is present
contains:foo is a substring test asks whether the attribute ARRAY contains the value
an empty value skips every rule runs implicit rules anyway — accepted on '' fails

CI regenerates the fixture and fails if the committed copy disagrees, so the claim cannot rot into a statement about a past version of Laravel.

The two packages upgrade separately

laranail/validation-js on Packagist writes the schema; @laranail/validation-js on npm reads it. They are separate releases and neither waits for the other.

That is a property of the format, not a promise. Within a major schema version every change is additive: the runner ignores keys it does not recognise, degrades a rule whose parameters it cannot find to undetermined rather than guessing, and a renamed key is emitted under both names for as long as anyone is running the old code. A mismatch costs precision — some fields round trip that need not have — and never correctness.

The alternative, which this replaced, was a version check that fired on any change and sent whole forms to the server until both halves were upgraded in step. See Schema.

Form runtime, adapters and bridges

createValidator(form, schema, options) binds a real form with debounced eager validation, an injectable renderer (ClassMapRenderer over plain-data presets — bootstrap5, tailwind, bulma, vanilla), input-widget resolvers, a dual event channel (instance on() plus bubbling laranail:* DOM events), wizard-step validate({ only }), server-422 setErrors(), repeater-aware refresh(), and core-owned accessibility: aria-invalid, non-destructive aria-describedby, a polite live region, and a role="alert" summary with focus management. Everything is instance-scoped — two validators coexist on one page, attach is idempotent, and destroy() is leak-free, all pinned by the Playwright suite. The engine stays zero-dependency with a CI bundle budget (Layer 0 ≤ 8 KB min+gzip).

Where a framework owns the DOM, the library answers with state instead: HeadlessForm under a React hook (@laranail/validation-js/react) and a Vue 3 composable (…/vue), with Alpine (…/alpine) and declarative autoboot for Blade/HTMX/Turbo/Livewire pages (…/autoboot) on the DOM-owning side, and …/debug console tracing that tree-shakes out by absence.

Stability

1.0 states what SemVer covers. Stable: the JS createValidator / createHeadless / HeadlessForm and the Validator surface (validate, validateField, setErrors, refresh, explain, on, use, registerRule, destroy), the framework adapters' documented API, the shipped .d.ts types, the wire schema (Schema); on the PHP side RuleExporter, SchemaFactory, RemoteRegistry, the events, the Blade surface, the console commands, and the laranail.validation-js.* config keys. Not stable (@internal, may change in a minor): RuleCatalogue, the HTTP controllers, EngineIntrospection, and anything undocumented. A test enforces that every class is one or the other. Deprecations post-1.0 are marked @deprecated with the replacement and removal version, kept for at least one minor, removed only in the next major.

Browsers: Safari/iOS 15.4+, Chrome/Edge 93+, Firefox 92+ (declared in browserslist; the floor is Object.hasOwn — see Installation). Accessibility: the runtime's a11y behaviour (aria-invalid, non-destructive aria-describedby, polite live region, role="alert" summary, focus management, reduced-motion) lives in core, is asserted with axe in the browser suite, and is part of the stable contract.

Documentation

Guides

  • Installation — both halves, the VCS repositories, the subpath exports
  • Getting started — one path from FormRequest to validated form
  • Configuration — the laranail.validation-js.* keys and per-instance options
  • Architecture — the layered design, and why the adapters are headless
  • Schema — the wire contract both halves implement
  • Transport — the three delivery tiers, the validate endpoint, and the remote channel
  • Release — three version lines, one discipline

Reference

  • Rules — what runs in the browser, what does not, and why
  • Form runtime — createValidator, scheduling, rendering, a11y, debug
  • Headless form — the DOM-free stateful facade under the adapters
  • Adapters — the React hook and the Vue composable
  • Bridges — autoboot, Alpine, and the Livewire rules
  • Console commands — export, doctor, parity

Recipes

Project

  • Upgrading — why the two packages upgrade separately, and what would change that

Prior art

The two ideas at the centre of this — a client allow-list with an unknown-rule fallback to the server, and a positional-to-named parameter table — were established by proengsoft/laravel-jsvalidation. No code was carried over; that package is MIT and a derivative would inherit its attribution obligation. See CREDITS.md.

License

MIT © Simtabi LLC. See LICENSE.

About

Export Laravel validation rules as a JSON schema and check them in the browser — three-valued, so it never claims a server-only rule passed.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages