No jQuery Validate: reactive and extensible
jQuery Validate is not available in Hyva Themes, and that is a good thing. A custom Alpine.js validation system is more reactive, lighter and fully integrated into the state model. Custom rules, asynchronous server checks and submit protection all fit into fewer than 60 lines of code.
Table of Contents
- 1. Why no jQuery Validate in Hyva?
- 2. Architecture: field rules as data objects
- 3. Defining and combining valid rules
- 4. Showing error messages reactively
- 5. Validation on @blur vs. @input
- 6. Asynchronous server validation
- 7. Submit protection and full form validation
- 8. Integration into the Magento checkout form
- 9. Comparing validation approaches
- 10. Summary
- 11. FAQ
1. Why no jQuery Validate in Hyva?
Hyva Themes was built with the explicit goal of removing jQuery, Knockout.js and Magento's UI component system from the frontend. That means jQuery Validate, which accompanied traditional Magento frontends for decades, simply is not available in Hyva. Anyone who adds it manually does not just import the library, they import the entire jQuery core, which fundamentally contradicts Hyva's architectural decision.
jQuery Validate also has conceptual weaknesses in an Alpine context regardless. It works DOM centric: it reads fields via selectors, manages state in the DOM itself and communicates errors through DOM manipulation. Alpine.js works the opposite way: state lives in a JavaScript object, and the DOM is a reactive projection of it. Error messages appear through x-show, not through jQuery classes. That produces conflicts between jQuery Validate's DOM state and Alpine's state.
An Alpine.js validation system is therefore not a workaround, it is the architecturally correct approach. It is fully reactive, does not rely on DOM selectors, integrates seamlessly with x-model, and can run asynchronous validations against Magento REST endpoints without callbacks or promise bridging.
2. Architecture: field rules as data objects
The core of the validation system is a fields object in Alpine state. Each field has a value, an errors array and a touched flag. The touched flag controls when errors are shown: only after the first interaction, not already on page load. Validation rules are functions that receive the field value as a parameter and return either null (valid) or an error string.
This architecture has a decisive advantage: rules are pure functions that can be tested independently. Anyone writing unit tests for the validation system simply imports the rule functions and tests them directly, no DOM, no Alpine, no mocking required. That is a substantial quality improvement over DOM centric validators like jQuery Validate.
// Alpine.js form validation: core structure
// Register via Alpine.data('contactForm', () => formValidator({ ... }))
document.addEventListener('alpine:init', () => {
Alpine.data('contactForm', () => ({
fields: {
name: { value: '', errors: [], touched: false },
email: { value: '', errors: [], touched: false, checking: false },
message: { value: '', errors: [], touched: false },
},
submitting: false,
submitted: false,
// Validation rules per field: return null = valid, string = error message
rules: {
name: [
v => v.trim().length === 0 ? 'Name is required.' : null,
v => v.trim().length < 2 ? 'Name must be at least 2 characters.' : null,
v => v.trim().length > 100 ? 'Name must not exceed 100 characters.' : null,
],
email: [
v => v.trim().length === 0 ? 'Email address is required.' : null,
v => !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? 'Please enter a valid email address.' : null,
],
message: [
v => v.trim().length === 0 ? 'Message is required.' : null,
v => v.trim().length < 10 ? 'The message must be at least 10 characters long.' : null,
],
},
}));
});
3. Defining and combining valid rules
Each rule is a function that takes the current field value as a parameter and returns either null for valid or an error message string. That makes rules extremely composable: a required check, a minimum length check and a format check are three separate functions combined in the rules array. All of them run in sequence and all errors are collected.
Reusable rule factories make the system even more ergonomic. minLength(n) returns a rule that performs the minimum length check for the given n. matches(regex, message) returns a regex validation rule. These factory functions can be defined in a separate validation-rules.js file and reused across the whole project. That creates consistency across different forms and avoids duplicated validation code.
4. Showing error messages reactively
Error messages appear in the template with x-show="field.touched && field.errors.length > 0". The touched flag prevents errors from being shown on initial page load. Only after the first blur event is the flag set to true and errors become visible. Alpine.js updates visibility automatically as soon as errors or touched changes.
The error styling on the input field itself, a red border and a red background tint, is bound to the same state with a :class binding: :class="{ 'border-red-500 bg-red-50': field.touched && field.errors.length > 0 }". A green indicator can be added for the success case: :class="{ 'border-green-500': field.touched && field.errors.length === 0 && field.value }". All visual feedback is declared in the template, without a single DOM manipulation in the JavaScript.
// Validate a single field and update its errors array
// Call on @blur or @input depending on UX requirements
validateField(fieldName) {
const field = this.fields[fieldName];
const rules = this.rules[fieldName] ?? [];
field.errors = rules
.map(rule => rule(field.value))
.filter(err => err !== null);
},
// Mark field as touched and validate, call on @blur
touchField(fieldName) {
this.fields[fieldName].touched = true;
this.validateField(fieldName);
},
// Validate all fields, call before submit
validateAll() {
let valid = true;
for (const name of Object.keys(this.fields)) {
this.fields[name].touched = true;
this.validateField(name);
if (this.fields[name].errors.length > 0) valid = false;
}
return valid;
},
// Computed: is the whole form valid right now?
get isValid() {
return Object.values(this.fields).every(f => f.errors.length === 0);
},
5. Validation on @blur vs. @input
Deciding when validation fires has a significant impact on UX. Validating too early, on the very first keystroke, frustrates users who are still typing. Validating too late, only on submit, gives feedback only at the end and forces users to check every field again. The best balance: validate on the @blur event (when the field loses focus) and re-validate on every @input event once the field has already been touched.
This pattern is expressed with an @blur="touchField('email')" and an @input="if (fields.email.touched) validateField('email')". Once the user has left a field and comes back to it, they see immediate feedback as they type. Fields that have not been touched yet stay quiet. That is the UX standard users expect from modern web applications, and it is a nuance jQuery Validate rarely offers.
6. Asynchronous server validation
Some validations cannot be performed client side: whether an email address is already registered, whether a coupon code is valid, whether a chosen username is still available. These cases need asynchronous validation. Alpine.js supports async methods natively, an async validateEmail() in the state object works exactly as expected.
The asynchronous validation flow: a checking flag on the field shows a loading state in the template. The method sends a request to a Magento REST endpoint. On response, checking is reset and the errors array is filled with the result. Debouncing prevents a request from being sent on every keystroke, a setTimeout with 400ms that gets cancelled on further input is the simplest solution without an external library.
// Async email existence check against Magento customer API
// Debounced to avoid a request on every keystroke
let emailCheckTimer = null;
async checkEmailExists() {
const field = this.fields.email;
// Only check if basic format validation passed
if (field.errors.length > 0) return;
if (!field.value) return;
clearTimeout(emailCheckTimer);
emailCheckTimer = setTimeout(async () => {
field.checking = true;
try {
const response = await fetch(
`/rest/V1/customers/isEmailAvailable`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ customerEmail: field.value }),
}
);
const available = await response.json();
if (!available) {
// Email already registered, push server error into errors array
field.errors = ['This email address is already registered.'];
}
} catch {
// Network error, do not block submission, handle server-side
} finally {
field.checking = false;
}
}, 400);
},
7. Submit protection and full form validation
The submit handler validates all fields first. For that, validateAll() sets every touched flag to true and runs every rule. If there are errors anywhere, the browser scrolls to the first invalid field and the submit is aborted. Alpine.js can find the first invalid field with this.$el.querySelector('[aria-invalid="true"]') and focus it with .focus().
The submitting flag prevents double submits: it is set to true at the start of the submit handler and the submit button shows a loading state. In the template: :disabled="submitting" and x-text="submitting ? 'Sending…' : 'Submit'". After a successful send or an error, submitting is reset. This simple pattern replaces the complex submit blocking logic jQuery Validate used to build into Magento forms.
8. Integration into the Magento checkout form
In the Magento Hyva checkout, the form fields are already embedded in Alpine components. The custom validation system integrates as an Alpine.data component that gets combined with the checkout component via x-data="Object.assign(checkoutState(), contactValidation())". This object spread technique merges two Alpine data objects into a single scope without conflicts, as long as the property names are unique.
The Magento REST API returns structured error responses with field names when validation fails. These can be pushed directly into the relevant field's errors array: this.fields[fieldName].errors = [apiError.message]. That creates a seamless integration between client side and server side validation, errors from both sources appear in the same template at the same spot.
9. Comparing validation approaches
A direct comparison between jQuery Validate and a custom Alpine.js system shows clear differences in architecture, weight and integrability.
| Criterion | jQuery Validate | Custom-built Alpine.js | Recommendation (Hyva) |
|---|---|---|---|
| Dependency | jQuery + plugin (~90 KB) | Alpine only (~15 KB) | Alpine, jQuery is not available |
| State approach | DOM centric | JS state centric | Alpine, no state conflict |
| Async validation | remote option (cumbersome) | async/await natively | Alpine, cleaner syntax |
| Custom rules | $.validator.addMethod | Plain JS functions | Alpine, testable without a DOM |
| Hyva compatibility | Not compatible | Fully compatible | Alpine, the only sensible choice |
For projects outside Hyva Themes that load jQuery anyway, jQuery Validate can still make sense, especially when legacy code already relies on it. In a Hyva context, a custom Alpine.js validation system is the only architecturally consistent solution.
Mironsoft
Alpine.js, Hyva Themes and Magento 2 frontend development
Need Hyva compatible form validation for your checkout?
We build reactive validation systems with Alpine.js, compliant with Hyva Themes, Magento CSP policies and accessible to WCAG 2.1.
Form development
Checkout, contact and registration forms with complete Alpine.js validation
API integration
Asynchronous validation against the Magento REST API: email, coupons, addresses
Migration
Migrating jQuery Validate forms to Alpine.js without losing any functionality
10. Summary
A custom Alpine.js validation system without jQuery Validate is not just possible in a Hyva context, it is the architecturally correct solution. The core structure is a fields object with a value, errors and touched flag per field, a rules object with arrays of rule functions, and methods for single field and whole form validation. Reactive error messages come from x-show and :class bindings, with no DOM manipulation at all.
The decisive advantage over jQuery Validate lies not just in weight, but in the state model: validation state is Alpine state, not DOM state. Asynchronous server validation with async/await, debouncing and the checking flag for loading states are all natively achievable in Alpine. The result is a validation system that feels like a native part of the page, not a bolted on plugin.
Alpine.js Validation: The Essentials At A Glance
State structure
fields[name].value, .errors, .touched per field. rules[name] as an array of rule functions (v => null | errorString). validateField, touchField, validateAll as methods.
Error display
x-show="field.touched && field.errors.length > 0" for error messages. :class for color feedback. No DOM selector access needed, fully declarative.
UX timing
@blur for the initial touchField. @input with an if guard for live feedback after the first blur. validateAll before submit with scroll/focus on the first invalid field.
Async & submit
async/await for server validation with a checking flag. Debounce with clearTimeout/setTimeout. submitting flag against double submits. API errors pushed directly into the errors array.