HTMX Crash Course - Dynamic Pages Without Writing JavaScript

htmx javascript Jan 04, 2024

You may have heard about HTMX. It's a small JavaScript library that allows you to create dynamic websites and interfaces without writing any JavaScript. It does this with the help of special attributes called 'hyperscript attributes'. It's great for interacting with your server and adding dynamic content to your site while making HTTP requests without having to write any of JavaScript or use a larger framework like React or Vue. HTMX is more like a library than a framework. It doesn't wrap around your application like a framework, it just offers tools for you to use, sort of like jQuery.

One of the main things that you need to understand is that you're pretty dependant on your server with HTMX. You need to have a server that can handle the requests and send back the responses. So it's not really for simple frontend applications, it's more for the frontend of a full-stack app. You can use anything for your backend. In this video, I'll use Node.js and Express, but some other popular options are Python Django and GoLang. If you want something that's more front-end focused for smaller projects, you can check out Alpine.js. It's a great tool for adding very simple and dynamic behavior to your HTML. Things like if statements and conditionals. I have a crash course on Alpine if you're interested.

In this article, we'll talk about what you can do with HTMX, how to get started with it, and we're going to create a few small projects using a Node.js/Express backend. So we will write some backend JavaScript, but none at all on the frontend.

The video version of this article is available on YouTube.

All of the code is available in the GitHub repo

Here are the mini-projects that we'll be building:

  • A button that makes a GET request to the server to get and display a list of users
  • A temperature converter that makes a POST request with the fahrenheit value and displays the celsius value
  • A page that uses the polling feature of HTMX to get the updated weather every 5 seconds
  • A page that has an input that let's us search the backend for specific users
  • A form with inline validation
  • A profile that we can click to edit and update the user's information

So before we jump into the code, let's talk about what HTMX is and what it can do.

What Can You Do With HTMX?

There's a lot you can do with HTMX. I just want to briefly touch on some of the important ones.

  • HTTP/AJAX Requests - Easily make requests to your server and update parts of your page with the response, allowing for dynamic content loading.
  • Trigger Events - Trigger server-side events in response to user actions on the client.
  • Dynamic Forms - Send form data to the server without full-page refreshes and update the page with the form submission results.
  • CSS Manipulation - You can add dynamic styling by changing CSS properties, classes, and styles on specific elements.
  • Data Binding - You can bind data to elements and have them automatically update when the data changes.
  • Validation - Perform client-side and server-side validation of user input before submitting forms. It actually builds on top of the native HTML validation.
  • Progress Indicators - Show loading spinners or progress bars during long-running server operations.
  • Error Handling - Display error messages or handle errors gracefully when server requests fail.
  • Routing - You can use HTMX to handle client-side routing with the hx-boost attribute.
  • WebSockets - You can use HTMX with an extension to open WebSocket connections and send and receive data.

There's a lot more you can do but these are some of the main features. Let's take a look at how to get started with HTMX.

Hyperscript Attributes

HTMX uses what are called hyperscript attributes. These are attributes that start with hx-. Here are some examples of the most used attributes, but you can find a full list of them here.

  • hx-get - This is used to make a GET request to the server.
  • hx-post - This is used to make a POST request to the server.
  • hx-patch - This is used to make a PATCH request to the server.
  • hx-put - This is used to make a PUT request to the server.
  • hx-delete - This is used to make a DELETE request to the server.
  • hx-target - This is used to target an element to swap the content with. You can use this, parent, next-sibling, previous-sibling, first-child, last-child, all, none, or a CSS selector.
  • hx-trigger - This is used to trigger the request. You can use all of the standard DOM events like click, mouseover, submit, etc.
  • hx-swap - This is used to swap the content of the element with the response from the server. You can use outerHTML, innerHTML, afterbegin, beforebegin, afterend, beforeend, and none.
  • hx-indicator - This is used to show a loading indicator while the request is being made. You can use dots, ellipsis, outline, text, grow, shrink, hidden, inline, block, flex, grid, table, table-row, table-cell, none, or a CSS selector.
  • hx-boost - This is used to have <a> tags use Ajax requests instead of full page refreshes.
  • hx-select - This is used to select elements to send to the server. You can use this, parent, next-sibling, previous-sibling, first-child, last-child, all, none, or a CSS selector.
  • hx-params - This is used to send additional parameters to the server. You can use true, false, or a CSS selector.

There is more to HTMX such as a JavaScript API and extensions, but we're not going to cover that in this video. You can find more information on the HTMX website.

Getting Started

There's a few ways to get started with HTMX.

CDN

The easiest way is to use a CDN. You simply include this script tag in your HTML and you're good to go.

<script
  src="https://unpkg.com/[email protected]"
  integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
  crossorigin="anonymous"
> </script>

Direct Download

You can also download the library and include it in your project. You can find that here:

https://htmx.org/docs/#installing

NPM

If you're using NPM, you can install it with this command:

npm install htmx.org

Express Server

We need some kind of backend to handle the requests. I'll be using Node.js and Express, but you can use whatever you want. I'll have the code for the server in the description below. We'll start by creating a new folder and initializing a new Node.js project.

mkdir htmx-crash-course
cd htmx-crash-course
npm init -y

Next, we'll install Express. I also want to install a package called XSS. This is a security package that will help prevent cross-site scripting attacks. We'll use this when we're sending data to the server.

npm install express xss

I also want to install Nodemon so that we can restart the server automatically when we make changes.

npm install nodemon -D

ES Modules

I want to use ES modules instead of CommonJS modules. This is a newer feature of Node.js. To use it, we need to add a type property to our package.json file and set it to module.

"type": "module"

Now we can use ES modules. We'll start by creating a new file called server.js. We'll import Express and create a new Express app.

import express from 'express';
import xss from 'xss';

const app = express();

// Set static folder
app.use(express.static('public'));
// Parse URL-encoded bodies (as sent by HTML forms)
app.use(express.urlencoded({ extended: true }));
// Parse JSON bodies (as sent by API clients)
app.use(express.json());

// Start the server
app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

We imported our dependencies and created a new Express app. We also set the static folder to public and we're using the urlencoded and json middleware. We'll use the urlencoded middleware for our forms and the json middleware for our API. We also started the server on port 3000. Now let's create a public folder and add an index.html file. This is our frontend where we will put all of our HTML and HTMX.

For now, Add the following to the public/index.html file.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HTMX Crash Course</title>
    <script
      src="https://unpkg.com/[email protected]"
      integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
      crossorigin="anonymous"
    > </script>
    <script src="https://cdn.tailwindcss.com"> </script>
  </head>
  <body>
    <div class="text-center">
      <h1 class="text-2xl font-bold my-5">Simple Request Example</h1>
      <button
        class="bg-blue-500 text-white py-2 px-4 my-5 rounded-lg hover:bg-blue-600"
      >
        Fetch Users
      </button>
    </div>
  </body>
</html>

We included the HTMX script tag and a Tailwind CSS script tag. We'll use Tailwind for styling. I added a wrapper div, an h1 and a button. No HTMX yet.

If you run your server with npm run dev and go to http://localhost:3000, you should see the output.

Triggering Events <hx-trigger> and Making Requests <hx-get>

Let's start by looking at how to trigger events. We'll start with a simple button that will make a get request to the JSON placeholder API. So we're not even using our server yet. Add the following to the button.

<button
  hx-get="https://jsonplaceholder.typicode.com/users"
  hx-trigger="click"
  class="bg-blue-500 text-white py-2 px-4 my-5 rounded-lg hover:bg-blue-600"
>
  Fetch Users
</button>

So we have a button with an hx-get attribute. This is going to make a GET request to the URL that I provided. You can also make POST, PUT, PATCH and DELETE requests by using the correct attribute.

We also have an hx-trigger attribute that will trigger the request when the button is clicked. You can use any of the standard DOM events like click, mouseover, submit, etc.

If you click the button, what happens is it makes the request. You can check your devtools network tab if you want. Then it takes whatever the response is, in this case, a JSON array of users and it puts it in the element that has the hx-target attribute. So in this case, it's putting it right in the button.

In this case, we actually don't even need the hx-trigger attribute because click is the default. So if we remove it, it will still work. You can try temporarily changing the trigger to a mouseover event. Now if you hover over the button, it will make the request. I'm going to just remove the hx-trigger attribute and leave it as a click event.

Swapping Content <hx-swap>

We can also swap the content of the button with the response from the server instead of putting it inside of the element/button. We can do this by adding an hx-swap attribute to the button. We can use outerHTML, innerHTML, afterbegin, beforebegin, afterend, beforeend, and none. Let's use outerHTML.

<button
  hx-get="https://jsonplaceholder.typicode.com/users"
  hx-swap="outerHTML"
  class="bg-blue-500 text-white py-2 px-4 my-5 rounded-lg hover:bg-blue-600"
>
  Fetch Users
</button>

You may not want to swap the content of the button, so you can target another area on the page to put the response.

Targeting Elements <hx-target>

Let's get rid of the hx-swap and add an hx-target attribute to the button. You can put an id or class in here as well as things like this, meaning the current element, parent, next-sibling, previous-sibling and some others. Let's add a div with an id of users and add the hx-target attribute to the button.

<button
  hx-get="https://jsonplaceholder.typicode.com/users"
  hx-target="#users"
  class="bg-blue-500 text-white py-2 px-4 my-5 rounded-lg hover:bg-blue-600"
>
  Fetch Users
</button>
<div id="users"></div>

Now it will put the response in the div with the id of users.

Adding a Confirmation Dialog <hx-confirm>

Another attribute that we could use is hx-confirm. This will show a confirmation dialog before making the request.

<button
  hx-get="https://jsonplaceholder.typicode.com/users"
  hx-target="#users"
  hx-confirm="Are you sure you want to fetch users?"
  class="bg-blue-500 text-white py-2 px-4 my-5 rounded-lg hover:bg-blue-600"
>
  Fetch Users
</button>

I'm going to remove this for now. I just wanted to show you that it's there.

Using Our Own Server

Now, in most cases, you're not going to want to display a JSON array on the page. If we want these users, we would want markup to show a list of user names or whatever else we want to show. So let's use our own server to get the users and return some markup.

I'm going to just put all of our backend routes directly in the server.js file, so it will get a bit messy. If you want to create a separate file for your routes, you can do that. I'll have the code for this in the description below.

Let's add a route called /users that will return a list of users.

// Handle GET request to fetch users
app.get('/users', (req, res) => {
  const users = [
    { id: 1, name: 'John Doe' },
    { id: 2, name: 'Bob Williams' },
    { id: 3, name: 'Shannon Jackson' },
  ];

  res.send(`
    <h1 class="text-2xl font-bold">Users</h1>
    <ul>
      ${users.map((user) => `<li>${user.name}</li>`).join('')}
    </ul>
    `);
});

So now instead of getting the JSON array of users, we're getting some markup that we can display on the page. Change the URL to /users:

```html
<button
  hx-get="/users"
  hx-target="#users"
  class="bg-blue-500 text-white py-2 px-4 my-5 rounded-lg hover:bg-blue-600"
>
  Fetch Users
</button>

Let's update the server to use the JSON placeholder API. Update the /users route to this:

// Handle GET request to fetch users
app.get('/users', async (req, res) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await response.json();

  res.send(`
    <h1 class="text-2xl font-bold">Users</h1>
    <ul>
      ${users.map((user) => `<li>${user.name}</li>`).join('')}
    </ul>
    `);
});

We had to add the async keyword to the callback function and use await for the fetch and response. Now if we click the button, we get a list of users from the API.

Adding a Loading Indicator <hx-indicator>

We can also add a loading indicator while the request is being made. We add the attribute hx-indicator and we can set it to an element that we want to show while the request is being made. Let's add a value of #loading. Now I am going to put an image called loader.gif in the public/img folder. You can use whatever you want including just text. you can get the image from the main repo on GitHub. Next we can add a span with a class of htmx-indicator and an id of loading and an image tag inside of it.

<div class="text-center">
  <h1 class="text-2xl font-bold my-5">Simple Request Example</h1>
  <button
    hx-get="/users"
    hx-target="#users"
    hx-indicator="#loading"
    class="bg-blue-500 text-white py-2 px-4 my-5 rounded-lg hover:bg-blue-600"
  >
    Fetch Users
  </button>
  <span class="htmx-indicator" id="loading">
    <img src="img/loader.gif" class="m-auto h-10" />
  </span>
  <div id="users"></div>
</div>

At the moment, you probably won't see the loader because the request is so fast. You can slow it down by adding a setTimeout to the server.

// Handle GET request to fetch items
app.get('/users', async (req, res) => {
  setTimeout(async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const users = await response.json();

    res.send(`
    <h1 class="text-2xl font-bold">Users</h1>
    <ul>
      ${users.map((user) => `<li>${user.name}</li>`).join('')}
    </ul>
    `);
  }, 2000);
});

Now if you click the button, you should see the loader for a couple of seconds.

Sending Data to the Server <hx-vals>

You may want to send additional data to the server as well. We can do this with the hx-vals attribute. Let's send a number to limit the users. Add the hx-vals attribute to the button and set it to limit=5.

<button
  hx-get="/users"
  hx-target="#users"
  hx-indicator="#loading"
  hx-vals='{"limit": 5}'
  class="bg-blue-500 text-white py-2 px-4 my-5 rounded-lg hover:bg-blue-600"
>
  Fetch Users
</button>

Now, in our backend, we can get that value with req.query.limit. Let's update the /users route to this:

// Handle GET request to fetch items
app.get('/users', async (req, res) => {
  setTimeout(async () => {
    const limit = parseInt(req.query.limit) || 10;
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/users?_limit=${limit}`
    );
    const users = await response.json();

    res.send(`
    <h1 class="text-2xl font-bold">Users</h1>
    <ul>
      ${users.map((user) => `<li>${user.name}</li>`).join('')}
    </ul>
    `);
  }, 2000);
});

We converted the limit value to a number and used 10 as the default. Be sure to use backticks for the template string. Now if you click the button, you should see 5 users.

Temperature Converter

That small project demonstrated most of the basics of how HTMX works. Let's create some other example projects. Let's start with a simple temperature converter. We'll have an input for fahrenheit and then we will make a POST request to the server to convert it to celsius. We'll use the hx-post attribute to make a POST request to the server. You can use the current html file or create another. I'm going to create another one called public/temp.html. Keep the same <head> with the HTMX and Tailwind includes and add the following to the ` 

<body class="bg-gray-100">
  <div class="container mx-auto mt-8 text-center">
    <div class="bg-white p-4 border rounded-lg max-w-lg m-auto">
      <h1 class="text-2xl font-bold mb-4">Temperature Converter</h1>
      <form
        hx-trigger="submit"
        hx-post="/convert"
        hx-target="#result"
        hx-swap="innerHTML"
        hx-indicator="#indicator"
      >
        <label for="fahrenheit">Fahrenheit:</label>
        <input
          type="number"
          id="fahrenheit"
          name="fahrenheit"
          class="border p-2 rounded"
          value="32"
          required
        />
        <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">
          Convert
        </button>

        <div id="result" class="mt-6 text-xl"></div>
        <span class="htmx-indicator" id="indicator">
          <img src="img/loader.gif" class="m-auto h-10" />
        </span>
      </form>
    </div>
  </div>
</body>

So now we have a form that will make a POST request to /convert and we're targeting the #result div. We will show an indicator as well. Let's add the route to the server.

// Handle POST request for converting temperature
app.post('/convert', (req, res) => {
  setTimeout(() => {
    const fahrenheit = parseFloat(req.body.fahrenheit);
    const celsius = (fahrenheit - 32) * (5 / 9);

    res.send(
      `<p>${fahrenheit} degrees Fahrenheit is equal to ${celsius.toFixed(
        2
      )} degrees Celsius.</p>`
    );
  }, 2000);
});

I used a setTimeout to simulate a slow request. We did the conversion and returned the result as a string. Now if you go to http://localhost:3000/temp.html and enter a value and click the button, you should see the result.

Polling

Another thing that we can do is polling. This is where we make a request to the server every so often to get updated data. Let's create a new file called public/poll.html.

Add the following to the html body:

<body>
  <div class="text-center">
    <span id="data-value" hx-get="/poll" hx-trigger="every 10s"
      >Loading...</span
    >
  </div>
</body>

Now create an endpoint that will have a counter that will update every second:

// Define a route that returns updated data every second
app.get('/poll', (req, res) => {
  let counter = 0;
  // Simulate data update
  counter++;
  const data = { value: counter };

  // Send the data as JSON response
  res.json(data);
});

Now if you go to http://localhost:3000/poll.html, you should see the counter updating every 10 seconds.

Let's make it a bit more interesting and have it give us the temperature and update it every few seconds. Then we can use polling to get the updated temperature. Change the route to /get-temperature and add the following code:

// Route to get the current temperature
app.get('/get-temperature', (req, res) => {
  // Simulate fetching temperature from an API
  currentTemperature += Math.random() * 2 - 1; // Random temperature change
  res.send(currentTemperature.toFixed(1) + '°C');
});

Now let's add the HTML.

<body
  class="bg-blue-900 text-white min-h-screen flex items-center justify-center"
>
  <div class="text-center">
    <h1 class="text-xl mb-4">Weather in New York</h1>
    <p class="text-5xl">
      <span id="temperature" hx-get="/get-temperature" hx-trigger="every 5s"
        >Loading...</span
      >
    </p>
  </div>
</body>

Now if you go to http://localhost:3000/poll.html, you should see the temperature updating every 5 seconds.

Searching

Let's create a page that has an input that let's us search the backend for specific users. We'll have a table with the user name and email.

Create a file called public/search.html and add the following to the html body:

<body class="bg-blue-900">
  <div class="container mx-auto py-8 max-w-lg">
    <span class="htmx-indicator" id="loading">
      <img src="img/loader.gif" class="h-10 mb-3 m-auto" />
    </span>
    <div
      class="bg-gray-900 text-white p-4 border border-gray-600 rounded-lg max-w-lg m-auto"
    >
      <h3 class="text-2xl mb-3 text-center">Search Contacts</h3>
      <input
        class="border border-gray-600 bg-gray-800 p-2 rounded-lg w-full mb-5"
        type="search"
        name="search"
        placeholder="Begin Typing To Search Users..."
        hx-post="/search"
        hx-trigger="input changed delay:500ms, search"
        hx-target="#search-results"
        hx-indicator="#loading"
      />
      <table class="min-w-full divide-y divide-gray-200">
        <thead class="bg-gray-800 text-white">
          <tr>
            <th
              scope="col"
              class="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider"
            >
              Name
            </th>
            <th
              scope="col"
              class="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider"
            >
              Email
            </th>
          </tr>
        </thead>
        <tbody id="search-results" class="divide-y divide-gray-600">
          <!-- Your search results go here -->
        </tbody>
      </table>
    </div>
  </div>
</body>

Now, lets create the route that will handle the search. Add the following to the server:

// Simulate a database of contacts
const contacts = [
  { name: 'John Doe', email: '[email protected]' },
  { name: 'Jane Doe', email: '[email protected]' },
  { name: 'Alice Smith', email: '[email protected]' },
  { name: 'Bob Williams', email: '[email protected]' },
  { name: 'Mary Harris', email: '[email protected]' },
  { name: 'David Mitchell', email: '[email protected]' },
];

// Handle GET request for searching
app.post('/search', (req, res) => {
  const searchTerm = req.body.search.toLowerCase();

  if (!searchTerm) {
    // If the search term is empty, return an empty table
    return res.send('<tr></tr>');
  }

  // Perform a basic search on the contacts data
  const searchResults = contacts.filter((contact) => {
    const name = contact.name.toLowerCase();
    return (
      name.includes(searchTerm) ||
      contact.email.toLowerCase().includes(searchTerm)
    );
  });

  // Simulate a delay to show the indicator
  setTimeout(() => {
    // Generate HTML markup for the search results
    const searchResultHtml = searchResults
      .map(
        (contact) => `
      <tr>
        <td><div class="my-4 p-2">${contact.name}</div></td>
        <td><div class="my-4 p-2">${contact.email}</div></td>
      </tr>
    `
      )
      .join('');

    // Send the HTML response
    res.send(searchResultHtml);
  }, 1000);
});

Now you should be able to search for the users.

If you wanted to use an API like JSON placeholder, we could change it to the following:

// Handle GET request for searching JSON Placeholder
app.post('/search/api', async (req, res) => {
  const searchTerm = req.body.search.toLowerCase();

  // Check if the search term is empty
  if (!searchTerm) {
    return res.send(`
      <tr>
        <td></td>
        <td></td>
      </tr>
    `);
  }

  try {
    // Fetch the users from the external API
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const users = await response.json();

    // Perform a basic search on the fetched users data
    const searchResults = users.filter((user) => {
      const fullName = `${user.name}`.toLowerCase();
      return (
        fullName.includes(searchTerm) ||
        user.email.toLowerCase().includes(searchTerm)
      );
    });

    // Simulate a delay to show the indicator
    setTimeout(() => {
      // Check if there are no search results
      if (!searchResults.length) {
        return res.send(`
          <tr>
          <td><div class="my-4 p-2">No Results Found</div></td>
            <td></td>
          </tr>
        `);
      }

      // Generate HTML markup for the search results
      const searchResultHtml = searchResults
        .map(
          (user) => `
        <tr class="w-full">
          <td><div class="my-4 p-2">${user.name}</div></td>
          <td><div class="my-4 p-2">${user.email}</div></td>
        </tr>
      `
        )
        .join('');

      res.send(searchResultHtml);
    }, 2000); // Adjust the delay time as needed
  } catch (error) {
    console.error(error);
    res.status(500).send('Internal Server Error');
  }
});

Now, just change the hx-post attribute to /search/api and it will search the JSON placeholder API.

Inline Validation

Let's create a form with some inline validation using HTMX. Create a new file called public/validation.html and add the following to the html body:

<body class="bg-blue-500">
  <form
    hx-post="/contact"
    hx-target="this"
    hx-swap="innerHTML"
    class="max-w-md mx-auto p-6 bg-white rounded-lg shadow-lg mt-6"
  >
    <h1 class="text-2xl font-bold mb-4 text-center">Contact Us</h1>
    <div hx-target="this" hx-swap="outerHTML" class="mb-4">
      <label class="block text-gray-700 text-sm font-bold mb-2" for="email"
        >Email Address</label
      >
      <input
        name="email"
        hx-post="/contact/email"
        class="border rounded-lg py-2 px-3 w-full focus:outline-none focus:border-blue-500"
        type="email"
        id="email"
        required
      />
    </div>
    <div class="mb-4">
      <label class="block text-gray-700 text-sm font-bold mb-2" for="firstName"
        >First Name</label
      >
      <input
        type="text"
        name="firstName"
        class="border rounded-lg py-2 px-3 w-full focus:outline-none focus:border-blue-500"
        id="firstName"
        required
      />
    </div>
    <div class="mb-6">
      <label class="block text-gray-700 text-sm font-bold mb-2" for="lastName"
        >Last Name</label
      >
      <input
        type="text"
        name="lastName"
        class="border rounded-lg py-2 px-3 w-full focus:outline-none focus:border-blue-500"
        id="lastName"
        required
      />
    </div>
    <div class="flex items-center justify-between">
      <button
        class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:bg-blue-600"
        type="submit"
      >
        Submit
      </button>
    </div>
  </form>
</body>

We do have an hx-post attribute on the form tag itself, but that isn't my focus. Notice the email input has an hx-post attribute as well. This will make a POST request to /contact/email when this individual input changes.

So let's add that route and have it check the email and return the input along with a message. Add the following to the server:

// Handle POST request for email validation
app.post('/contact/email', (req, res) => {
  const submittedEmail = req.body.email;
  const emailRegex = /^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;

  const isValid = {
    message: 'That email is valid',
    class: 'text-green-700',
  };
  const isInvalid = {
    message: 'Please enter a valid email address',
    class: 'text-red-700',
  };

  if (!emailRegex.test(submittedEmail)) {
    return res.send(`
      <div hx-target="this" hx-swap="outerHTML" class="mb-4">
        <label class="block text-gray-700 text-sm font-bold mb-2" for="email">Email Address</label>
        <input name="email" hx-post="/contact/email" value="${submittedEmail}" class="border rounded-lg py-2 px-3 w-full focus:outline-none focus:border-blue-500" type="email" id="email" required>
        <div class='${isInvalid.class}'>${isInvalid.message}</div>
      </div>
    `);
  } else {
    return res.send(`
      <div hx-target="this" hx-swap="outerHTML" class="mb-4">
        <label class="block text-gray-700 text-sm font-bold mb-2" for="email">Email Address</label>
        <input name="email" hx-post="/contact/email" value="${submittedEmail}" class="border rounded-lg py-2 px-3 w-full focus:outline-none focus:border-blue-500" type="email" id="email" required>
        <div class='${isValid.class}'>${isValid.message}</div>
      </div>
    `);
  }
});

Now if you go to http://localhost:3000/validation.html and enter an invalid email, you should see the error message. If you enter a valid email, you should see the success message.

Profile Editor

The last little project that we will create is a simple profile page with a name and bio that we can click on to edit.

Create a new file called public/profile.html and add the following to the html body:

<body class="bg-gray-100">
  <div
    class="container mx-auto py-8 max-w-lg"
    hx-target="this"
    hx-swap="outerHTML"
  >
    <div class="bg-white p-6 rounded-lg shadow-lg">
      <h2 class="text-xl text-gray-400 mb-4">PROFILE</h2>
      <h1 class="text-2xl font-bold mb-4">Brad Traversy</h1>
      <p class="text-gray-700">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla
        vestibulum vestibulum elit, ac facilisis ipsum eleifend sed. Duis
        tincidunt augue nec neque cursus, nec aliquet purus tempor.
      </p>
      <button
        hx-get="/profile/1/edit"
        class="bg-indigo-700 text-white font-bold w-full py-2 px-4 rounded-lg mt-4 hover:bg-indigo-600"
      >
        Click To Edit
      </button>
    </div>
  </div>
</body>

We are using this as the target and swapping the outer HTML. This will replace the entire page with the response from the server. Let's add the route to the server:

// Handle GET request for editing
app.get('/profile/:id/edit', (req, res) => {
  // You can send an HTML form for editing here
  res.send(`
  <div
  class="container mx-auto py-8 max-w-lg"
  hx-target="this"
  hx-swap="outerHTML"
>
<form hx-put="/profile/1" hx-target="this" hx-swap="outerHTML">
    <div class="bg-white p-6 rounded-lg shadow-lg">
      <div class="mb-4">
        <label for="name" class="text-lg font-semibold">Name</label>
        <input type="text" id="name" name="name" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring focus:ring-blue-400" value="Brad Traversy">
      </div>
      <div class="mb-4">
        <label for="bio" class="text-lg font-semibold">Bio</label>
        <textarea id="bio" name="bio" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring focus:ring-blue-400" rows="6">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vestibulum vestibulum elit, ac facilisis ipsum eleifend sed. Duis tincidunt augue nec neque cursus, nec aliquet purus tempor.</textarea>
      </div>
      <div class="mt-6">
      <button type="submit" class="px-4 py-2 bg-indigo-700 text-white rounded-lg hover:bg-indigo-600">Save Changes</button>
      <button type="button" hx-get="/profile.html" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg ml-2 hover:bg-gray-400">Cancel</button>
    </div>
    </div>
  </form>
</div>
  `);
});

Now if you go to http://localhost:3000/profile.html and click the button, you should see the edit form. If you click the cancel button, it will take you back to the profile page.

Let's create the route for the PUT request to save the changes. Add the following to the server:

// Handle PUT request for editing
app.put('/profile/:id', (req, res) => {
  const name = xss(req.body.name);
  const bio = xss(req.body.bio);

  // Send the updated profile back
  res.send(`
  <div
  class="container mx-auto py-8 max-w-lg"
  hx-target="this"
  hx-swap="outerHTML"
>
  <div class="bg-white p-6 rounded-lg shadow-lg">
    <h1 class="text-2xl font-bold mb-4">${name}</h1>
    <p class="text-gray-700">
      ${bio}
    </p>

    <button
      hx-get="/profile/1/edit"
      class="bg-indigo-700 text-white font-bold w-full py-2 px-4 rounded-lg mt-4 hover:bg-indigo-600"
    >
      Click To Edit
    </button>
  </div>
</div>
  `);
});

Alright, we had quite a few examples there. I hope you got a good idea of how HTMX works. There are some other capabilities, but I think we covered the basics.

Stay connected with news and updates!

Join our mailing list to receive the latest news and updates from our team.
Don't worry, your information will not be shared.

We hate SPAM. We will never sell your information, for any reason.