Real-Time Validation Migration
When to Use
Migrate field-level validation that runs on blur (focusout) without submitting the form. Common for email availability checks, username validation, or format verification.
Steps
- Replace
#ajaxwith HTMX on field — Configuretrigger('focusout') - Add validation result container — Target for validation messages
- Check trigger in buildForm() — Run validation when specific field triggered
- Return validation message — Update the result container
BEFORE: AJAX
$form['email'] = [
'#type' => 'email',
'#title' => t('Email'),
'#ajax' => [
'callback' => '::validateEmailCallback',
'wrapper' => 'email-validation',
'event' => 'focusout',
'progress' => ['type' => 'none'],
],
];
$form['email_validation'] = [
'#type' => 'container',
'#attributes' => ['id' => 'email-validation'],
];
public function validateEmailCallback(array &$form, FormStateInterface $form_state) {
$email = $form_state->getValue('email');
if ($this->emailExists($email)) {
$form['email_validation']['#markup'] = '<span class="error">Email already taken</span>';
}
else {
$form['email_validation']['#markup'] = '<span class="success">Available</span>';
}
return $form['email_validation'];
}
AFTER: HTMX
use Drupal\Core\Htmx\Htmx;
use Drupal\Core\Url;
$form['email'] = [
'#type' => 'email',
'#title' => t('Email'),
];
// Configure HTMX to trigger on blur
(new Htmx())
->post(Url::fromRoute('<current>'))
->onlyMainContent()
->trigger('focusout')
->select('#email-validation')
->target('#email-validation')
->swap('outerHTML')
->applyTo($form['email']);
$form['email_validation'] = [
'#type' => 'container',
'#attributes' => ['id' => 'email-validation'],
];
// In buildForm, check if this is validation request
$trigger = $this->getHtmxTriggerName();
if ($trigger === 'email') {
$email = $form_state->getValue('email');
if ($email && $this->emailExists($email)) {
$form['email_validation']['#markup'] = '<span class="error">Email already taken</span>';
}
elseif ($email) {
$form['email_validation']['#markup'] = '<span class="success">Available</span>';
}
}
Reference: HTMX trigger patterns in /core/lib/Drupal/Core/Htmx/Htmx.php
Common Mistakes
- Using
'event' => 'focusout'→ HTMX usestrigger('focusout')method, not an array key - Not handling empty values → Check if field has value before validating. Empty blur shouldn't show error
- Creating separate callback → Put validation logic in
buildForm()checkinggetHtmxTriggerName(). No callback needed - Including progress indicator → HTMX is fast enough without progress indicators. Omit unless validation is slow
- Not throttling requests → There is no
->throttle()method. Throttle is a trigger modifier: use->trigger('focusout throttle:1s'). Debounce (delay) works similarly:->trigger('focusout delay:500ms')
See Also
- Previous: Multi-Step Wizard Migration
- Next: Infinite Scroll Migration
- Reference: HTMX trigger modifiers like
changed,delay,throttlein HTMX documentation