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.
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) |
<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.
<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?
The server sends HX-Trigger: taskAdded and the counter listens with hx-trigger="taskAdded from:body"The form's hx-target points at the counterPoll the counter every 100 msReload 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.