proengsoft/laravel-jsvalidation
Reuse Laravel validation rules, messages, and FormRequests to validate forms client-side automatically—no JavaScript coding required. Supports AJAX rules like unique and exists, localization, and works without the Form Builder.
Install and publish
composer require proengsoft/laravel-jsvalidation
php artisan vendor:publish --provider="Proengsoft\JsValidation\JsValidationServiceProvider"
The default config/jsvalidation.php works out of the box; you will mostly touch it later for selectors, remote-validation toggles, and custom rule mappings. Also publish/copy the JS bundle if you don't want to load public/vendor/jsvalidation/js/jsvalidation.js from the package's asset path.
Where to look first
config/jsvalidation.php — this is the real API surface. Read it top to bottom once: form/field/message/error selectors, valid_class/error_class, disable_remote_validation, and the rule-to-JS method mapping.JsValidator facade — formRequest(), validator(), rules() are the three entry points you'll use 95% of the time.jsvalidation.js bundle — it wraps jQuery Validation; if a rule behaves oddly, read the JS implementation before filing a bug.First use case: a FormRequest-backed form
// app/Http/Requests/StoreCustomerRequest.php
public function rules(): array
{
return [
'name' => 'required|string|max:120',
'email' => 'required|email|unique:customers,email',
'country' => 'required|exists:countries,iso',
'age' => 'nullable|integer|min:18',
];
}
public function messages(): array
{
return ['age.min' => 'You must be at least :min years old.'];
}
<form id="customer-form" method="POST" action="{{ route('customers.store') }}">
@csrf
<input name="name"> <input name="email"> <input name="age">
<select name="country">...</select>
<button type="submit">Save</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script src="{{ asset('vendor/jsvalidation/js/jsvalidation.js') }}"></script>
{!! JsValidator::formRequest('App\Http\Requests\StoreCustomerRequest') !!}
Load jQuery before the package script, render the facade output after the form markup, and you have client-side parity with the server rules with zero hand-written JS. That's the package's whole value proposition — validate it on one form before rolling it out.
Pick the right entry point
| Source of truth | Facade call | When |
|---|---|---|
| FormRequest class | JsValidator::formRequest(StoreCustomerRequest::class) |
Default. Rules, messages, and attributes all come from the class. |
Validator instance |
JsValidator::validator($validator) |
Rules built at runtime (conditionals, tenancy, feature flags). |
| Raw array | JsValidator::rules($rules, $messages = [], $attributes = []) |
Ad-hoc forms, wizards, settings pages. |
For runtime rules, build the validator the same way the controller does so the two never drift:
$validator = Validator::make($request->all(), [
'plan' => 'required|in:free,pro',
'seats' => $user->isPro() ? 'required|integer|min:1' : 'nullable',
'vat_id' => 'nullable|required_if:country,DE|string|max:20',
]);
return view('billing', ['jsValidator' => JsValidator::validator($validator)]);
Standard controller/view split
Keep the rule definition in the FormRequest and only the rendering in the view. A common convention is a @push('scripts') stack:
@push('scripts')
{!! JsValidator::formRequest('App\Http\Requests\StoreCustomerRequest', '#customer-form') !!}
@endpush
The second argument is a CSS selector; use it whenever the form has an id so the generated rules bind to exactly one form instead of relying on the package's default form selector.
Multiple forms on one page
Render one facade call per form and give each form a distinct selector. Avoid duplicate name attributes across forms when the selectors overlap, otherwise jQuery Validation will pull values from the wrong scope.
Remote rules (unique / exists / active_url / custom)
These are validated server-side via AJAX against your own routes. Two practical implications:
unique:customers,email,{id} needs the id resolved at validation time. In a FormRequest, Laravel resolves it from the route model binding; the package then generates the remote rule against that value. When the record is unknown client-side (e.g. AJAX-created rows), the remote rule must run server-side anyway — don't fight it.unique on three fields will fire three requests on blur. If that's too chatty, disable remote validation in config and rely on the server response for those fields, keeping the fast local rules client-side.Custom validation rules
Rules implemented with Rule objects / Validator::extend are not transpiled to JS. They fall back to AJAX validation, which means the server round-trip is your only check. Two patterns work well:
exists/unique).$.validator.addMethod and map the rule name in config/jsvalidation.php.Localization
Messages come through Laravel's translator at render time, so the locale of the request determines the message language. That makes per-user locale switching work automatically — but it also means a cached full-page response will bake in whichever locale rendered it. If you cache HTML, exclude the JsValidator:: output or key the cache by locale.
Experimental AJAX FormRequest mode
Since 4.4 the package can skip client-side rule translation for a FormRequest and submit the whole thing over AJAX, returning Laravel's error bag. This is the pragmatic escape hatch when a form mixes many remote/custom rules or uses rules the JS layer doesn't implement. Trade-off: you lose instant feedback and every submit costs a round-trip. Use it per-form, not globally.
Testing
The JS layer is invisible to PHPUnit. Cover it with Dusk or a Playwright smoke test on one representative form per rule group (required, size, date, remote). Assert the error element text, not just the class — message content is where localization and custom messages break.
jQuery is a hard dependency. The package bundles jQuery Validation, not jQuery itself. In a Vite/Livewire app this is the biggest friction point: jQuery must be a global loaded before the bundle, and it must survive Turbo/Livewire navigation. Load both once in your layout, and re-render/re-init validators after any partial replace of the form.
Hidden and disabled inputs are skipped. jQuery Validation ignores :hidden by default. Token fields, tab panels using display:none, and inputs inside collapsed accordions will not be validated. Either show the panel on error or override the ignore option in a small custom script — the config exposes selectors, not the ignore list.
Rules the JS layer doesn't implement. present and timezone-qualified date_format are documented as unsupported, so those inputs pass client-side and fail server-side. Audit your rule sets for them before telling stakeholders "validation is client-side now." Anything custom also lands in the AJAX bucket.
Remote validation fails open. If the AJAX endpoint 404s, is CSRF-blocked, or is rate-limited, the field generally errs on the side of pass. Always keep server-side validation authoritative — which you should be doing anyway — and add a monitoring check that the generated validation endpoint returns 200 in each environment.
Quoting and escaping in generated output. The facade emits a JS object literal containing your messages and rule parameters. Apostrophes in messages, and regex rules like regex:/^[A-Z'\-]+$/, are the classic breakage. Publish the views and confirm the config's escaping option is enabled; write messages without stray quotes when you can, and test any rule containing / or ' in a browser.
Size rules differ from character counts. min/max on numeric fields mean value bounds, on strings they mean character counts, and on arrays they mean element counts. Releases 4.4.3 and 4.4.4 exist precisely because of mismatches here. If you use min/max against a multiple select or file array, verify client behavior matches the server for the same input.
Date comparison rules are the flakiest area. before/after/date_format have had several fixes for comparison and type errors. Keep date formats simple and identical between the rule and the input's placeholder/flatpickr config; avoid mixing timezone-aware strings with plain Y-m-d.
Config changes need a cache clear. php artisan config:cache in production means editing jsvalidation.php without re-caching does nothing. Add it to your deploy checklist alongside view:clear when you publish and edit the package views.
Don't stack a second submit handler on the same form. If you also have a $(form).on('submit', ...) handler for spinners, autosave, or Stripe, ordering matters: jQuery Validation blocks submission on error, so your handler may never fire or may fire twice. Bind your handler to the invalid-form / valid events rather than raw submit.
Extension points, in order of preference:
config/jsvalidation.php — selectors, classes, remote toggle, view paths.defer, nonce for CSP).$.validator.addMethod + a config rule mapping — for rules you want validated without a round-trip.CSP note. If you use a strict Content-Security-Policy, the facade output is an inline <script>. Either publish the view and add your nonce/hash, or render rules into a data- attribute and initialize from a bundled file.
Maturity reality check. Last release is 2026-03-17 with PHP 8.2–8.4 / Laravel 11–13 support, so it's maintained, but the surrounding ecosystem has moved on: jQuery is legacy in new builds. Budget the jQuery inclusion as a real architectural decision, and isolate the dependency to the forms that need it rather than adding jQuery to a modern SPA-style front end.
How can I help you explore Laravel packages today?