Logo

dev-resources.site

for different kinds of informations.

Handling form errors in htmx

Published at
10/11/2024
Categories
htmx
javascript
Author
yawaramin
Categories
2 categories in total
htmx
open
javascript
open
Author
9 person written this
yawaramin
open
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 protected]. 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.

htmx Article's
30 articles in total
Favicon
Creating a To-do app with HTMX and Django, part 9: active search
Favicon
Creating a To-do app with HTMX and Django, part 8: inline edit, and using Iconify
Favicon
Creating a To-Do app with HTMX and Django, part 7: infinite scroll
Favicon
Creating a To-Do app with HTMX and Django, part 6: implementing Delete with tests
Favicon
Creating a To-Do app with HTMX and Django, part 5: Testing the views
Favicon
Creating a To-Do app with HTMX and Django, part 4: adding new Todos
Favicon
Creating a To-Do app with HTMX and Django - Part 3: Creating the frontend and adding HTMX
Favicon
Creating a To-Do app with HTMX and Django - Part 1: Creating the Django project with uv
Favicon
Creating a To-Do app with HTMX and Django - Part 2: Adding the Todo model with tests
Favicon
Htmx alpine component
Favicon
Don't Fall Into the CDN Trap! πŸͺ€
Favicon
How to Set Up Authorization in a Bookstore Management System with Go, HTMX, and Permit.io
Favicon
Django project - Part 4 HTMX, TailwindCSS and AlpineJS
Favicon
Implement reCAPTCHA in htmx + expressjs
Favicon
htmx and ExpressJS
Favicon
</> htmx post json
Favicon
</> htmx handle array response
Favicon
Building Simple Real-Time System Monitor using Go, HTMX, and Web Socket
Favicon
πŸ”₯HMPL β€” best alternative to HTMX
Favicon
I've built the TodoMVC app with HTMX and lived to tell the story
Favicon
Summary of the AJAX frameworks comparison
Favicon
A minimalist newsletter signup app with HTMX and Manifest
Favicon
Leveraging Go Tailwind Template (GoTTH) for Efficient Microservices Architecture
Favicon
.
Favicon
</> htmx in 5 minutes
Favicon
Handling form errors in htmx
Favicon
Refactoring RATOM: Day ...604
Favicon
Personal Finance Management App with Django, HTMX, Alpine, Tailwind, and Plaid
Favicon
Augmenting the client with HTMX
Favicon
The GoTTH Stack

Featured ones: