htmx: Build Interactive Web Apps with HTML · Build a Contacts App

Adding Contacts and Updating Several Places

Add a contact from a form, append the new row, update a counter in the header out of band, and notify other parts of the page with HX-Trigger.

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

What you'll build

An Add contact form under the table. Saving appends the new row, updates the "N contacts" counter in the page header, clears the form, and shows a short confirmation, all from one request.

The idea

Two techniques update more than the target:

  • Out-of-band swaps: an element in the response with hx-swap-oob="true" and an id replaces the element with that id anywhere on the page. Good when the server knows exactly which element changes.
  • HX-Trigger response header: the server fires an event (contactAdded); any element can listen with hx-trigger="contactAdded from:body" and refresh itself. Good when several independent widgets care.

The code

Add a contactTry it yourself
<header>Contacts (<span id="contact-count">2</span>) <small id="toast"></small></header>
<table><tbody id="rows">
  <tr><td>Ada Lovelace</td><td>[email protected]</td></tr>
  <tr><td>Alan Turing</td><td>[email protected]</td></tr>
</tbody></table>

<form hx-post="/contacts" hx-target="#rows" hx-swap="beforeend"
      hx-on::after-request="if (event.detail.successful) this.reset()">
  <input name="name" placeholder="Name" required>
  <input name="email" type="email" placeholder="Email" required>
  <button>Add contact</button>
</form>
<p hx-get="/contacts/latest" hx-trigger="contactAdded from:body">Latest: Alan Turing</p>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  const contacts = [["Ada Lovelace", "[email protected]"], ["Alan Turing", "[email protected]"]];
  server.post("/contacts", (req) => {
    contacts.push([req.body.name, req.body.email]);
    return {
      status: 201,
      headers: { "HX-Trigger": "contactAdded" },
      body: `<tr><td>${server.escape(req.body.name)}</td><td>${server.escape(req.body.email)}</td></tr>
             <span id="contact-count" hx-swap-oob="true">${contacts.length}</span>`,
    };
  });
  server.get("/contacts/latest", () => "Latest: " + server.escape(contacts[contacts.length - 1][0]));
</script>

Common mistakes

  • Returning a <tr> as an out-of-band element: browsers drop table rows outside a table while parsing. Wrap OOB rows in <template>.
  • An OOB element without an id (or with an id that isn't on the page): nothing happens.
  • Coupling everything to the form's response: when a fourth widget needs updating, add an HX-Trigger listener instead of growing the response.

Key points

  • One request can update many places: OOB swaps for known elements, HX-Trigger for independent listeners.
  • Clear the form only after a successful request.
  • Answer creates with 201; htmx swaps any 2xx.

Further reading: hx-swap-oob · HX-Trigger response header

Practice questions

What does hx-swap-oob="true" on an element in a response do?

Answer

htmx removes it from the main response and swaps it into the page element with the same id, in addition to the normal swap.

When is HX-Trigger a better choice than an out-of-band swap?

Answer

When several independent parts of the page should react; each listens for the event and refreshes itself, so the endpoint doesn't need to know about them.

Why might an out-of-band <tr> silently fail?

Answer

The HTML parser drops <tr> elements outside a table; wrap them in a <template> tag in the response.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?