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](https://htmx.org/docs/#validation). 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](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#a_more_detailed_example) or more visually the [Custom Styles Bootstrap Example](https://getbootstrap.com/docs/5.1/forms/validation/#custom-styles).

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>

JSFiddle

To make this approach more re-usable I wrapped it into a [HTMX extension](https://htmx.org/docs/#extensions) 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


  1. [Web Form Validation: Best Practices and Tutorials — Smashing Magazine](https://www.smashingmagazine.com/2009/07/web-form-validation-best-practices-and-tutorials/#server-side-validation

  2. Client-side form validation - Learn web development | MDN