Search code examples
htmx

HTMX hx-swap-oob adding a new table row only adds the <td>'s and not the wrapping <tr>


The Issue

The issue is obviously me. I'm learning HTMX and this is probably a really simple mistake. I'm having an issue with hx-swap-oob striping the <tr> tags out of the response and simply inserting the table columns where the row should go.

I'm using Python Flask on the server side. I'm using Bootstrap 5 for style. No other JS.

I'm attempting to build a "Widgets CRUD" and I'm following the example from the site for Updating Other Content. It's mostly working with this one sad exception.

Here is the issue:

<tr>...</tr>
<tr>...</tr>
<th scope="row">New Widget</th>
<td>123</td>
<td>$123</td>
<td>
    <button class="btn btn-warning" hx-get="/widget/{{ id }}">Edit</button>
    <button class="btn btn-danger" hx-delete="/widget/{{ id }}">Delete</button>
</td>
</table>

If the <tr> tags were there it'd be perfect :-(

I am also using the examples for Editing a row and Deleting a row so it's possible there is a conflict there?

Relevant code

The Table

<table class="table table-dark table-striped">
  <thead>
    <tr>
      <th scope="col">Name</th>
      <th scope="col">Qty</th>
      <th scope="col">Price</th>
      <th scope="col">Actions</th>
    </tr>
  </thead>
  <tbody hx-target="closest tr" hx-swap="outerHTML" id="widgets-table">
    {% for widget in widgets %}
    <tr>
      <th scope="row">{{ widget.name }}</th>
      <td>{{ widget.qty }}</td>
      <td>${{ widget.price / 100 }}</td>
      <td>
        <button class="btn btn-warning"
                hx-get="/widget/{{ widget.id }}">
          Edit
        </button>
        <button class="btn btn-danger"
                hx-delete="/widget/{{ widget.id }}"
                hx-confirm="Are you sure?">
          Delete
        </button>
      </td>
    </tr>
    {% endfor %}
  </tbody>
</table>

The Add Widget Form

<div class="row mb-3" id="add-form">
  <div class="col">
    <h3>Add new Widget</h3>
    <form hx-post="/save">
    <div class="mb-3">
      <label for="name" class="form-label">Name</label>
      <input type="text" class="form-control" id="name" name="name">
    </div>
    <div class="mb-3">
      <label for="name" class="form-label">Qty</label>
      <input type="text" class="form-control" id="qty" name="qty">
    </div>
    <div class="mb-3">
      <label for="name" class="form-label">Price</label>
      <input type="text" class="form-control" id="price" name="price">
    </div>
    <input type="submit" class="btn btn-primary" value="Save New Widget">
    </form>
  </div>
</div>

The example doesn't include the form submit button so I added that <input type="submit"> in there. Maybe I'm missing something there?

Server response template

<tr hx-swap-oob="beforeend:#widgets-table">
  <th scope="row">{{ name }}</th>
  <td>{{ qty }}</td>
  <td>${{ price / 100 }}</td>
  <td>
    <button class="btn btn-warning" hx-get="/widget/{{ id }}">Edit</button>
    <button class="btn btn-danger" hx-delete="/widget/{{ id }}">Delete</button>
  </td>
</tr>
<div hx-swap-oob="outerHTML" id="add-form"></div>

I've tried randomly sprinkling hx-* tags everywhere. Just missing a key something about how all this is supposed to work. Thanks in advance.


Solution

  • html parser is uncharacteristically strict about parsing table structure, which is especially painful with OOB elements that must have no parent

    Try wrapping your row in the response in tbody like so

    <tbody hx-swap-oob="beforeend:#widgets-table">
        <tr id="row2">
            <td>internals of cells from response</td>
            <td>internals of cells from response</td>
        </tr>
    </tbody>
    

    This should make html parser a bit happier about table structure and htmx will be able to find OOB content correctly. There might be some edge cases with that solution, but generally it should help