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

Editing Rows in Place

Turn a table row into an edit form, save with PUT, cancel back to the view, and stop a second row from being edited at the same time.

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

What you'll build

Each contact row gets an Edit button. Clicking it swaps the row for a row of inputs; Save sends a PUT and swaps the updated row back; Cancel restores the row without saving.

The idea

The server has two small templates per contact, a view row and an edit row, and htmx swaps between them with hx-target="closest tr" and hx-swap="outerHTML". Inputs in a table row aren't inside a <form>, so the Save button uses hx-include="closest tr" to send the row's inputs.

The code

Click to edit a rowTry it yourself
<table>
  <thead><tr><th>Name</th><th>Email</th><th></th></tr></thead>
  <tbody hx-target="closest tr" hx-swap="outerHTML">
    <tr><td>Ada Lovelace</td><td>[email protected]</td><td><button hx-get="/contacts/1/edit">Edit</button></td></tr>
    <tr><td>Alan Turing</td><td>[email protected]</td><td><button hx-get="/contacts/2/edit">Edit</button></td></tr>
  </tbody>
</table>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  const db = { 1: { name: "Ada Lovelace", email: "[email protected]" }, 2: { name: "Alan Turing", email: "[email protected]" } };
  const e = server.escape;
  const view = (id) => `<tr><td>${e(db[id].name)}</td><td>${e(db[id].email)}</td>
    <td><button hx-get="/contacts/${id}/edit">Edit</button></td></tr>`;
  const edit = (id) => `<tr>
    <td><input name="name" value="${e(db[id].name)}" aria-label="Name"></td>
    <td><input name="email" type="email" value="${e(db[id].email)}" aria-label="Email"></td>
    <td><button hx-put="/contacts/${id}" hx-include="closest tr">Save</button>
        <button hx-get="/contacts/${id}">Cancel</button></td></tr>`;
  server.get("/contacts/:id/edit", (req) => edit(req.params.id));
  server.get("/contacts/:id", (req) => view(req.params.id));
  server.put("/contacts/:id", (req) => { Object.assign(db[req.params.id], req.body); return view(req.params.id); });
</script>

hx-target and hx-swap sit once on the <tbody> and are inherited by every button, including the buttons in rows the server returns later.

Common mistakes

  • Save button without hx-include: inputs in a table row aren't in a form, so nothing is sent.
  • Returning the edit row after a successful save. Return the view so the user sees the result.
  • Trusting the edit form's values: the PUT handler validates and checks the user may edit this contact, and returns the edit row with a 422 when invalid.
  • Two people editing the same contact: include a version or updated_at field and reject stale updates (409).

Key points

  • View and edit are two server templates; htmx swaps them with closest tr + outerHTML.
  • hx-include="closest tr" sends inputs that aren't inside a form.
  • Put shared attributes on a parent once; children inherit them.

Further reading: htmx example: edit row · hx-include

Practice questions

Why does the Save button in an editable table row need hx-include="closest tr"?

Answer

The inputs aren't inside a form, so htmx wouldn't send them; hx-include adds the row's inputs to the request.

What should a successful PUT return in click-to-edit?

Answer

The read-only view row, which replaces the edit row (outerHTML on closest tr).

How can you protect against two users overwriting each other's edits?

Answer

Send a version or updated_at value with the form and have the server reject stale updates (for example with 409 Conflict).

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

Was this page helpful?