Bootstrap Custom Styles Form Validation with HTMX
This post expects you to know the basics about HTMX, Bootstrap (or any other CSS framework) and JavaScript form validation.
HTMX the javascript library that extends HTML to build modern websites, is based on the concept to swap out server-side rendered html.
To validate form input I learned to not trust client-side validation alone, and check any user-input on the server side1. HTMX showcases a really neat example of their approach of inline validation by sending the form to the server and receive chunks of html with the result of the validation.
This kind of approach works well, but sometimes I need immediate feedback, before the data hit the server. One case is file upload. I need to try and minimize the server roundtrips when uploading a file. So if I wait one minute to upload a file and the form comes back with an invalid email, dat would suck.
To minimize that, client-side validation offers a first check and server-side validation takes care of the rest.
It's possible to set up a form with htmx and use the native HTML5 validation API2 and that works just as expected. There is even a section about it in the docs. But if I want to use custom styles and error messages, I have to use the <form novalidate>
attribute to bypass the browser's automatic validation.
A key point here is that setting the
novalidate
attribute on the form is what stops the form from showing its own error message bubbles, and allows us to instead display the custom error messages in the DOM in some manner of our own choosing.
A good primer into this topic can be found on mdn or more visually the Custom Styles Bootstrap Example.
This Bootstrap example works out of the box, but how can I use this in HTMX?
With custom events!
The form gets posted by HTMX when the custom event bs-send
is triggered. And this event gets only triggered htmx.trigger(form, "bsSend")
when the form is valid form.checkValidity()
. Everything else stays the same like in the boostrap example.
<form
hx-post="/foo"
hx-trigger="bs-send"
class="row g-3 needs-validation"
novalidate
>
</form>
<script>
(function () {
'use strict'
// Fetch all the forms we want to apply custom Bootstrap validation styles to
var forms = document.querySelectorAll('.needs-validation')
// Loop over them and prevent submission
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (form.checkValidity()) {
// trigger custom event hx-trigger="bs-send"
htmx.trigger(form, "bsSend");
console.log('bsSend')
}
console.log('prevent')
event.preventDefault()
event.stopPropagation()
form.classList.add('was-validated')
}, false)
})
})()
</script>
To make this approach more re-usable I wrapped it into a HTMX extension and instead of defining the <form hx-trigger="bs-send">
attribute I'm setting a <form hx-ext="bs-validation">
that sets the hx-trigger
automatically
<form
hx-post="/foo"
hx-ext="bs-validation"
class="row g-3 needs-validation"
novalidate
>
</form>
htmx.defineExtension('bs-validation', {
onEvent: function (name, evt, data) {
if (name !== "htmx:afterProcessNode") {
return;
}
let form = evt.detail.elt;
// check if trigger attribute and submit event exists
// for the form
if(!form.hasAttribute('hx-trigger')){
// set trigger for custom event bs-send
form.setAttribute('hx-trigger','bs-send');
// and attach the event only once
form.addEventListener('submit', function (event) {
if (form.checkValidity()) {
// trigger custom event hx-trigger="bs-send"
htmx.trigger(form, "bsSend");
}
// focus the first invalide field
let invalidField = form.querySelector(':invalid');
if(invalidField) {
invalidField.focus();
}
event.preventDefault()
event.stopPropagation()
form.classList.add('was-validated')
}, false)
}
}
});
JSFiddle | Bootstrap Form Validation HTMX Extension - Gist
More Information on Form Validation
- Native form validation with JavaScript | falldowngoboone
- HTML Input Validation with JavaScript | Aleksandr Hovhannisyan
- Designing Better Inline Validation UX — Smashing Magazine
- Happier HTML5 Form Validation - daverupert.com
- Examples - Hyperform - Capture form validation back from the browser
- Pristine - Vanilla javascript form validation library | Pristine
- sha256/Pristine: Vanilla javascript form validation micro-library