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
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
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>
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"
}
}
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);
}
});
We are doing three key things here:
- For each form field that failed validation, attach the error message to it
- Attach an event listener to pop up the error message when the field gets focus
- 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)
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
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 :-)
Thanks! I'll bring it up in the Discord and see what people say as I don't have much experience with htmx extensions.