htmx Tutorial · Going Further · Lesson 23 of 30

Events and hx-on

Run small scripts with hx-on, react to htmx's lifecycle events, and trigger client events from the server with HX-Trigger.

Create a free account to save this page, take notes and track your progress. Sign in

hx-on

hx-on:<event>="code" runs a little JavaScript when an event happens, right in the HTML. For htmx events you can use the short form with a double colon: hx-on::after-request means hx-on:htmx:after-request.

<button hx-on:click="alert('Clicked!')">Plain JavaScript event</button>
<form hx-post="/save" hx-on::after-request="this.reset()">...</form>

Inside the code, this is the element and event is the event.

htmx lifecycle events

Event When
htmx:configRequest before sending: change parameters or headers (event.detail.parameters, .headers)
htmx:beforeRequest just before the request is sent (call preventDefault() to cancel)
htmx:beforeSwap after the response arrives, before swapping (change event.detail.shouldSwap, .target)
htmx:afterSwap / htmx:afterSettle after new content is in the page
htmx:afterRequest after the request finishes, with event.detail.successful
htmx:responseError the server answered with an error status
htmx:sendError the network failed
htmx:load new content was added and processed (use it to initialise widgets)
hx-on with htmx eventsTry it yourself
<button hx-get="/slow" hx-target="#out"
        hx-on::before-request="document.querySelector('#status').textContent = 'Sending...'"
        hx-on::after-request="document.querySelector('#status').textContent =
          event.detail.successful ? 'Finished OK' : 'Failed'">Run</button>
<button hx-get="/broken" hx-target="#out"
        hx-on::after-request="document.querySelector('#status').textContent =
          event.detail.successful ? 'Finished OK' : 'Failed with ' + event.detail.xhr.status">Run (broken)</button>
<p>Status: <strong id="status">idle</strong></p>
<div id="out"></div>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  server.get("/slow", () => "<p>Here is the result.</p>", { delay: 1000 });
  server.get("/broken", () => ({ status: 500, body: "Server error" }));
</script>

Triggering events from the server: HX-Trigger

The server can ask the browser to fire events with the HX-Trigger response header. Other parts of the page listen with hx-trigger="<event> from:body" and refresh themselves. This keeps components independent: the "add task" form doesn't need to know which parts of the page show task counts.

The server fires an event with HX-TriggerTry it yourself
<p>Open tasks: <strong hx-get="/tasks/count" hx-trigger="load, taskAdded from:body">...</strong></p>
<form hx-post="/tasks" hx-target="#tasks" hx-swap="beforeend" hx-on::after-request="this.reset()">
  <input name="title" placeholder="New task" required> <button>Add</button>
</form>
<ul id="tasks"></ul>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  const tasks = [];
  server.post("/tasks", (req) => {
    tasks.push(req.body.title);
    return { body: `<li>${server.escape(req.body.title)}</li>`, headers: { "HX-Trigger": "taskAdded" } };
  });
  server.get("/tasks/count", () => String(tasks.length));
</script>

Test yourself

A form adds a task, and a counter elsewhere must update. Which approach keeps them independent?

  1. The server sends HX-Trigger: taskAdded and the counter listens with hx-trigger="taskAdded from:body"
  2. The form's hx-target points at the counter
  3. Poll the counter every 100 ms
  4. Reload the page after each task
Show answer

Answer: The server sends HX-Trigger: taskAdded and the counter listens with hx-trigger="taskAdded from:body"

Server-triggered events decouple the form from every part of the page that cares.

Was this page helpful?