DEV Community

Yawar Amin
Yawar Amin

Posted on • Edited on

Handling form errors in htmx

FROM time to time I hear a criticism of htmx that it's not good at handling errors. I'll show an example of why I don't think that's the case. One of the common operations with htmx is submitting an HTML form (of the type application/x-www-form-urlencoded) to your backend server and getting a response. The happy path of course is when the response is a success and htmx does the HTML fragment swap. But let's look at the sad path.

Form validation messaging

A common UX need is to show an error message next to each field that failed to validate. Look at this example from the Bulma CSS framework documentation: https://bulma.io/documentation/form/general/#complete-form-example

Bulma CSS framework example showing a form with an error message

That does look nice...but it also requires custom markup and layout for potentially every field. What if we take advantage of modern browser support for the HTML Constraint Validation API? This allows us to attach an error message to each field with its own pop-up that lives outside the document's markup. You can see an example here: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity#results

Mozilla Developer Network showing a form with an error message using the Constraint Validation API

What if we had a message like this pop up for every field that failed validation? This is the question of this post.

Example form

Suppose you have an endpoint POST /users which handles a form with the payload fullname=Foo&email=bar@baz.com. You get the data in the backend, decode it, and if successful you are on the happy path as mentioned earlier. But if the form decode fails, we come to the interesting bit.

Here's the key point: if the form decode fails, you need some way to let htmx know about this specific error as opposed to some other error that could have happened. We need to make a decision here. Let's say we use the 422 Unprocessable Content status code for a form which fails validation.

Now, we need to decide how exactly to format the validation error message. The Constraint Validation API mentioned earlier is a JavaScript API, so that pretty much makes the decision for us. We will format the errors as JSON.

Here's an example form:

<form
  id=add-user-form
  method=post
  action=/users
  hx-post=/users
>
  <input name=fullname>
  <input name=email type=email>
  <input type=submit value="Add User">
</form>
Enter fullscreen mode Exit fullscreen mode

Of course, in a real app both these inputs would have the required attribute; here I am just leaving them out for demonstration purposes.

If we submit this form with the fullname and email fields left empty, then the backend should fail to validate the form and respond with the following:

HTTP 422
Content-Type: application/json

{
  "add-user-form": {
    "fullname": "Please fill out this field",
    "email": "Please fill out this field"
  }
}
Enter fullscreen mode Exit fullscreen mode

How do we make this happen? Well, htmx sends a request header HX-Trigger which contains the id of the triggered element, which will be add-user-form in this case. So we get the outermost object's key from there. Then, our form validation function should tell us the names of the fields that failed to validate and the error message for each. This gives us the inner object with the keys and values.

The error handler

With this response from the backend, we need some JavaScript to traverse the JSON and attach the error messages to each corresponding form field.

document.addEventListener('htmx:responseError', evt => {
  const xhr = evt.detail.xhr;

  if (xhr.status == 422) {
    const errors = JSON.parse(xhr.responseText);

    for (const formId of Object.keys(errors)) {
      const formErrors = errors[formId];

      for (const name of Object.keys(formErrors)) {
        const field = document.querySelector(`#${formId} [name="${name}"]`);

        field.setCustomValidity(formErrors[name]);
        field.addEventListener('focus', () => field.reportValidity());
        field.addEventListener('change', () => field.setCustomValidity(''));
        field.reportValidity();
      }
    }
  } else {
    // Handle the error some other way
    console.error(xhr.responseText);
  }
});
Enter fullscreen mode Exit fullscreen mode

We are doing three key things here:

  1. For each form field that failed validation, attach the error message to it
  2. Attach an event listener to pop up the error message when the field gets focus
  3. Attach an event listener to clear out the error message when the field's value is changed

The fourth action above, while not critical, is a nice to have: we just tell one of the fields to make it pop up its error message. This shows the user that something went wrong with the form submission. Of course, you can give even bigger hints, like highlighting inputs in an invalid state with CSS by targeting the input:invalid pseudo-selector.

Now, any time the form is submitted and there is a validation error, the response will automatically populate the error messages to the right places.

Not htmx?

If you have been paying close attention, you may be thinking that this technique seems to be not limited to htmx–and you're right! This technique based on the Constraint Validation API can be used with any frontend which uses forms. It doesn't need to be used specifically with htmx. You just need to adapt it to handle a form validation error from the backend server.

By taking advantage of a built-in feature of modern browsers, we make the code more adaptable and benefit from future improvements that browsers make to their UIs.

Top comments (3)

Collapse
 
sake_92 profile image
Sakib Hadžiavdić

Very nice post, thanks!
I've integrated it in one of my projects, it works like a charm. :)
Tho I have only one form per page, so it's easy to implement.

Maybe it could become an HTMX extension?
Standardize on a errors format and ship it? :D

Collapse
 
yawaramin profile image
Yawar Amin

OK so after getting some guidance from the Discord folks, it seems like an extension is just a tiny wrapper for the logic using the htmx function call htmx.defineExtension(), eg: github.com/bigskysoftware/htmx-ext...

I think really anyone can make this, it doesn't have to be me :-)

Collapse
 
yawaramin profile image
Yawar Amin

Thanks! I'll bring it up in the Discord and see what people say as I don't have much experience with htmx extensions.