HTMx - tutorial part V - Validation

· January 22, 2025

This is the fifth post in a series that I’m making about HTMx that I think is utterly amazing and will change how I (we?) write web apps in the future.

You are more than welcome to read from here, but then I would recommend getting the code from this point, if you intend to type along.

The application is working fine, but there’s zero validation in place. I wanted to do some simple validation on the client (and here I have a bug) and then also show how to do validation on the server-side and return an error message from there.

Validation can be tricky and often lead to a lot of code. But it’s an obvious addition to the code.

required attribute - simple client-side validation

HTML has a features to do some simple validation on the client-side. Using required fields is one of them. And if you check the new.ejs you can see that I actually used it.

<form id="todo-form" hx-on::after-request="this.reset()">
  <input type="text" name="title" placeholder="Title" required />
  <input type="date" name="duedate" required />
  <button
    type="submit"
    hx-post="/todo"
    hx-target="#todo-list"
    hx-swap="afterbegin"
  >
    Add Todo
  </button>
</form>

However it’s not triggering. And that is my fault, but not obvious to understand. The HTML validations are only firing on submission of the form. Since I put the hx-post attribute on the <button> it’s is not the form that is submitting.

We still get the form data thanks to HTMx’s awesomeness. But to trigger the required validations, we need to put the hx-post attribute on the form. I just move it up to the <form> tag and it gets trigger.

This will become a bit more troublesome later on, but we will fix it then. Read more abut this feature here.

Anyhow - here’s the updated form.

<form
  id="todo-form"
  hx-on::after-request="this.reset()"
  hx-post="/todo"
  hx-target="#todo-list"
  hx-swap="afterbegin"
>
  <input type="text" name="title" placeholder="Title" required />
  <input type="date" name="duedate" required />
  <button type="submit">Add Todo</button>
</form>

Doing the same for the edit.ejs-file shows us some of HTMx’s power with inherited attributes. Here’s the form after my changes to have the hx-put on the <form>:

<form
  id="todo-form"
  hx-put="/todo/<%= todo.id %>"
  hx-target="closest .todo-item"
  hx-swap="outerHTML"
>
  <input type="text" name="title" required value="<%= todo.title %>" />
  <input type="date" name="duedate" required value="<%= todo.duedate %>" />
  <button type="submit">Update</button>
  <button class="cancel-button" type="button" hx-get="/todo/<%= todo.id %>">
    Cancel
  </button>
</form>

Notice that:

The .cancel-button doesn’t have the hx-target or hx-swap attributes. Any attribute that is put on a parent element is automatically inherited to children. Since these attributes should be the same, in this case, I can omit them on the .cancel-button

I can use any action attributes (hx-get, hx-put, hx-post etc.) to trigger the validation on the form. In this case I’m using a hx-put since I’m going to update the resource using a HTTP PUT. The .cancel-button overrides the hx-put action by issuing a hx-get and the required-validation is not run.

Ok - that is much better already. No empty todos allowed!

Side-note; HTML has many great validation attributes that you get for free by setting the correct input type, like type="email", type="url" or type="number". In combination with pattern and step for example this becomes very powerful. Using these fields is also helpful for people with screen readers etc.

Server-side validation

We should validate the same thing on the server, but I thought it would be more interesting to do other validations there. I’m going to check that 1) a todo with the same name doesn’t exists in the database for this user and 2) that the due date is not passed already.

Strictly speaking the second the item could be checked on the client, but I’m using it as a vehicle to show of server-side-validations (and to avoid JavaScript in my application on the client - it’s HTMx, anyways.)

HTMx has written an example on how to do this, and I’m following their lead.

Validating dates

Firstly, I have redesigned the form a little bit, so that a error message <span> can fit. And added a lot of styles - you can find them here.

We will do an inline validation here, which means that when the user changes the value of the duedate field we will trigger a HTTP request. In true HTMx (HATEOAS) style, this request will return a HTML snippet that contains any validation error.

We could, for example, create a todo/partials/duedate.ejs file and let it accept and errormessage and duedate to be generated like this:

<div
  class="form-group <%= errormessage ? "error" : "valid"  %></div>"
  hx-target="this"
  hx-swap="outerHTML">

  <% if(errormessage) { %>
    <span class="error-message" id="duedate-error">
      <%= errormessage -%>
    </span>
  <% } %>

  <input
    class="<%= errormessage ? "error" : "valid"  %>"
    type="date"
    id="duedate"
    name="duedate"
    required
    hx-post="/todo/duedate"
    value="<%= duedate %>"
  />
</div>

Based on if the errormessage is empty or not we now:

  • Generate a <span> with the errormessage
  • Add a CSS class or use the valid that is the default
  • We also add the value of duedate so that we can see what was faulty.

Notice that the <input> field now have a hx-post="/todo/duedate" attribute. The default hx-trigger event for inputs are change in most cases which means that when the user tabs out of the field, we will HTTP POST to the endpoint.

The response from the HTTP POST /todo/duedate endpoint will be a form updated with, potentially, an error message.

Backend

That means that we can now write the backend, including a function that validates the duedate:

const validateDueDate = (inputDueDate) => {
  if (!inputDueDate) return "Due date is required.";

  const dueDate = new Date(inputDueDate);
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  if (isNaN(dueDate.getTime())) return "Invalid date format.";
  if (dueDate < today) return "The due date has already passed.";

  return "";
};

// Validation routes
router.post("/duedate", (req, res) => {
  const { duedate } = req.body;
  const errormessage = validateDueDate(duedate);
  return res.render("todo/partials/duedate.ejs", { errormessage, duedate });
});

Pretty simple, when you see it like this. We just validate the incoming duedate and then render the todo/partials/duedate.ejs with the given parameters.

Update the new.ejs template

One thing left - let’s update todo/new.ejs to use todo/partials/duedate.ejs, to keep our views DRY (don’t-repeat-yourself) and nice. In the new.ejs-view we will pass empty values for errormessage and duedate (for now…):

<form
  class="todo-form"
  hx-post="/todo"
  hx-target="#todo-list"
  hx-swap="afterbegin"
  hx-on::after-request="this.reset()"
>
  <div class="form-group" hx-swap="outerHTML">
    <input
      type="text"
      name="title"
      placeholder="Title"
      required
      hx-post="/todo/title"
    />
  </div>
  <%- include("partials/duedate.ejs", {errormessage: "", duedate: ""}) %>

  <div class="form-group">
    <button type="submit">Add Todo</button>
  </div>
</form>

The form reset

At this point I ran into problems (as I promised earlier), and I honestly don’t remember the order I got them in. The duedate field was emptied on validation or it was not emptied on submit.

The problem has to do with the hx-on::after-request="this.reset()" functionality. My understanding is this: since we now trigger a hx-post from a child element, it will inherit the hx-on::after-request and run the trigger on those requests too. this will then refer to the input that trigger the event, and hence does not .reset().

You can see this behavior by logging some data in the hx-on::after-request like this:

<form
  class="todo-form"
  hx-post="/todo"
  hx-target="#todo-list"
  hx-swap="afterbegin"
  hx-on::after-request="
  console.log('Resetting');
  console.log(event.detail);
  "
>

I tried to move the hx-on::after-request to the button but then I didn’t get it to fire at all. My understanding is that it’s not firing since the button is not issuing the request.

We’re stuck between a rock and hard place.

But did you see my log statement there - console.log(event.detail)? That object contains some information that we can use. Let’s see - we want to reset the form when:

  • The result was ok, so that we know that todo item was created properly
  • The <form /> was posted, not the underlying <input />

We can use JavaScript to accomplish this. Yes HTMx is declarative-first but not anti-JavaScript. Quite the opposite actually.

Here’s some JavaScript, including some logging that you can remove, that shows how to accomplish the reset:

<form
  class="todo-form"
  hx-post="/todo"
  hx-target="#todo-list"
  hx-swap="afterbegin"
  hx-on::after-request="
  console.log(event.detail);
  console.log(event.detail.pathInfo);
  if(event.detail.successful && event.detail.pathInfo.requestPath == '/todo') {
    console.log('Resetting the form');
    document.getElementById('title').value = '';
    document.getElementById('duedate').value = '';
  }"
>

When we validate the duedate the event.detail.pathInfo.requestPath will be the path that we HTTP POST to; /todo/duedate. Hence we can differentiate between the two types of requests and only reset the form when the actual <form> is posted.

The final thing that I didn’t get to work was the this.reset();. I think it has to do with that reset() doesn’t clear the form. It resets it.

What bothers me though is that the this.reset() doesn’t seem to do anything. I think that it has to do with that is reset not clear. When we validate the the information we set the value property of the <input /> and it will be interpreted as the initial value.

Hence I have to reset the values to '' manually.

PHEW - that was a lot of extra work that I didn’t think of. But it works and I now validate the duedate both on the client (for required) and the server.

Validate the title

We are going to do the same thing again, but now for title. We don’t allow two todos with the same title. It’s a little bit arbitrary but it will work as an example.

Here are my updated files:

views/todo/new.ejs

<form
  class="todo-form"
  hx-post="/todo"
  hx-target="#todo-list"
  hx-swap="afterbegin"
  hx-on::after-request="
  if(event.detail.successful && event.detail.pathInfo.requestPath == '/todo') {
    document.getElementById('title').value = '';
    document.getElementById('duedate').value = '';
  }"
>
  <%- include("partials/title.ejs", {errormessage: "" , title: "" }) %>
  <%- include("partials/duedate.ejs", {errormessage: "", duedate: ""}) %>

  <div class="form-group">
    <button type="submit">Add Todo</button>
  </div>
</form>

views/todo/partials/title.ejs

<div
  class="form-group <%= errormessage ? "error" : ""  %>"
  hx-target="this"
  hx-swap="outerHTML">

  <% if(errormessage) { %>
    <span class="error-message" id="title-error">
      <%= errormessage -%>
    </span>
  <% } %>

  <input
    class="<%= errormessage ? "error" : ""  %>"
    type="text"
    id="title"
    name="title"
    placeholder="Title"
    required
    hx-post="/todo/title"
    value="<%= title %>"/>
</div>

Update the edit.ejs

Before we leave the dueDate-validation, we can also update the edit.ejs-template to the partials.

<form
  class="todo-form"
  hx-put="/todo/<%= todo.id %>"
>
  <%- include("partials/title.ejs", {errormessage: "", title: todo.title }) %>
  <%- include("partials/duedate.ejs", {errormessage: "", duedate: todo.duedate }) %>
  <div class="form-group">
    <button type="submit" hx-target="closest .todo-item" hx-swap="outerHTML">
      Update
    </button>
  </div>
  <div class="form-group">
    <button class="cancel-button" type="button" hx-get="/todo/<%= todo.id %>">
      Cancel
    </button>
  </div>
</form>

We do not need the hx-on::after-request stuff in the edit.ejs form since it will be updated with the list item.

Wrong focus

This creates one final problem; after validating fields in the edit.ejs-file the field in the new.ejs-form gets focus.

This has to do with the fact that our controls have the same values in id and name. And after trying many alternatives I found a surprisingly easy solution; delete the id property.

It’s not just me being lazy, but the only thing that we are using that property for is to get hold of the element when doing the .reset()

I tried to give it a prefix based on the form we are in, but since we replacing parts of the form from the backend that will become problematic.

I also thought about replacing the entire form (expand the target, as the HTMx-kids say) on my validations, but decided that it was too much and didn’t really solve the problem at hand.

In the end, removing the id property works fine. It means title.ejs looks like this (and duedate.ejs is similar):

<div
  class="form-group <%= errormessage ? "error" : ""  %>"
  hx-target="this"
  hx-swap="outerHTML">

  <% if(errormessage) { %>
    <span class="error-message" id="title-error">
      <%= errormessage -%>
    </span>
  <% } %>

  <input
    class="<%= errormessage ? "error" : ""  %>"
    type="text"
    name="title"
    placeholder="Title"
    required
    hx-post="/todo/title"
    value="<%= title %>"/>
</div>

I had to update the hx-on::after-request in new.ejs. We cannot use document.getElementById now, since there’s no id. But since we want to reset the form in the new.ejs I can grab the first element with the name title and duedate like this:

<form
  id="new-form"
  class="todo-form"
  hx-post="/todo"
  hx-target="#todo-list"
  hx-swap="afterbegin"
  hx-on::after-request="
  if(event.detail.successful && event.detail.pathInfo.requestPath == '/todo') {
    document.getElementsByName('title')[0].value = '';
    document.getElementsByName('duedate')[0].value = '';
  }"
>

Not beautiful, but works.

For the new.ejs I’m thinking that expanding the target would be a good idea. That would mean that the response would include the (empty) form and a new item to add to the list (as we are doing now with hx-target="#todo-list" and hx-swap="afterbegin").

But I refrained from that since it would be a bit complicated by updating hx-oob etc.

Summary

As many thing HTMx I learn so much about how HTML and HTTP really works by using it.

After I was done I didn’t really write that much code to get it to work, but the journey there held a lot of learning. I like it!

The fact that many hx-* are inherited is a bit troublesome at times, but as with many tools you need to get into the thinking on how to use it before it starts to be easy. When working with it you also start to see the way that the tool nudges you to write it. At no point when writing this I felt completely stuck. There was always a new little nugget of knowledge, documentation or feedback that helped me moving forward.

The code is found here in the state that I left it in at the end of this post.. The current main is found here.

Twitter, Facebook