Dark mode switch icon Light mode switch icon

Giving HTML elements the power to update themselves with HTMX and Astro

4 min read

Say you’re building a web app that allows the user to upload a PDF document. The web app processes the PDF document, but it takes more than 10 seconds to complete.

Initially, that processing task happens within the HTTP request when someone submits a HTML form with the PDF file, and the browser tab loading spinner spins and spins. If their internet connection is interrupted, or they accidentally close the tab, the progress will be lost.

So you set up a background task queue with Redis. Now, when the user submits a HTML form, you add the task to the task queue and respond with a ‘Task added to queue!’ message:

<h1>Job</h1>
<div class="message">
  <p>Task added to queue!</p>
</div>
<p>Job progress: <span id="job-progress">0%</span></p>

The user needs to refresh their window to check the job progress. While this is an improvement over doing the job within the HTTP request, this isn’t great.

You could write some client-side JavaScript that sends a fetch request to an API endpoint that checks the job progress, and then update the #job-progress span.

But you prefer to have as little client-side JavaScript as possible. You prefer to work in the backend, and have full control over the server. You want to do as much as possible on the server.

Instead of just returning static HTML that returns the current progress of the job, using HTMX, we can return HTML with the power to update itself:

<h1>Job</h1>
<div class="message">
  <p>Task added to queue!</p>
</div>
<p>
  Job progress:
  <span
    id="job-progress"
    hx-get="/job/1/progress"
    hx-trigger="every 500ms"
    hx-swap="outerHTML"
  >
    0%
  </span>
</p>

Those hx- attributes on the <span> element will send a GET request to /job/1/progress every 500ms, and replace the full <span> tag with what comes back.

So in your project, you now need to implement a new endpoint, /job/[id]/progress that will return a HTML fragment with the current progress of the job. When you’re rendering the HTML, you can check if the job is done. If it’s not done yet, you’ll include the hx- attributes for it to update itself, and if it is done, you get rid of those attributes. For example, in Astro you would do something like:

---
// src/pages/job/[id]/progress.astro

import {getJobProgress} from "@src/utils/job"

const {id} = Astro.params;
const progress = getJobProgress(id);
const isDone = progress === 100;
---

{isDone ? (
	<span id="job-progress">100%</span>
) : (
	<span
		id="job-progress"
		hx-get={`/job/${id}/progress`}
		hx-trigger="every 500ms"
		hx-swap="outerHTML">
			{progress}%
	</span>
)}

To reduce duplicated code, you can create a <JobProgress> component that takes id and progress as props:

---
// src/components/JobProgress.astro

const {id, progress} = Astro.props;
const isDone = progress === 100;
---

{isDone ? (
	<span id="job-progress">100%</span>
) : (
	<span
		id="job-progress"
		hx-get={`/job/${id}/progress`}
		hx-trigger="every 500ms"
		hx-swap="outerHTML">
			{progress}%
	</span>
)}

And then use that component, both when you render the initial job page, and the HTML fragment:

The job page at src/pages/job/[id]/index.astro:

---
// src/pages/job/[id]/index.astro

import {getJobProgress} from "@src/utils/job";
import JobProgress from "@src/components/JobProgress.astro";

const {id} = Astro.params;
const progress = getJobProgress(id);
const isDone = progress === 100;
---

<h1>Job</h1>
<p>Job progress: <JobProgress {progress} {id} /></p>

HTML fragment page at src/pages/job/[id]/progress.astro:

---
// src/pages/job/[id]/progress.astro

import {getJobProgress} from "@src/utils/job"

const {id} = Astro.params;
const progress = getJobProgress(id);
const isDone = progress === 100;
---

<JobProgress {progress} {id} />

This is just one of the ways that HTMX can power-up the HTML in your web app. I’m exploring the examples on the HTMX site and building them, to get more comfortable with the HTMX way of working.

Extra note: if you’re having trouble installing HTMX into your Astro project (like I did), the trick is to import HTMX as an explicit URL import by adding ?url to the end of the import path. So install HTMX into Astro, run npm install htmx.org then add this to your .astro file (page or component):

---
import HtmxPath from "htmx.org/dist/htmx.min.js?url"
---

<script is:inline src="{HtmxPath}"></script>

Originally published on by Larry Hudson