Ersetzen von Tabelleninhalten via HTMX
Problem
Ich habe folgende Tabelle:
Wie man sehen kann gibt es (derzeit) einen Filter über den man definieren kann das man nur bezahlte Positionen sehen möchte.
Im Quellcode sieht das (auf das wesentliche gekürzt) so aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<table>
<tr>
<th>Datum</th>
<th>Menge</th>
<th>Einheit</th>
<th>Bezeichnung</th>
<th>Einzelpreis - Netto</th>
<th>Gesamtpreis - Netto</th>
<th>Mehrwertsteuer %</th>
<th>Einzel - Mehrwertsteuer</th>
<th>Einzelpreis - Brutto</th>
<th>Gesamt - Mehrwertsteuer</th>
<th>Gesamtpreis - Brutto</th>
<th>Zahlungmethode</th>
<th>Bezahlt</th>
<th>Anmerkung</th>
<th>Interne Anmerkung</th>
</tr>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th>
<input type="checkbox" name="payed" hx-trigger="click" hx-get="/deliveryposition/list/table" hx-target="#delivery-table" hx-swap="outerHTML" class="">
</th>
<th></th>
<th></th>
</tr>
<tr>
<td> 2026-01-03 </td>
<td> 6 </td>
<td> Stück </td>
<td> Blubber </td>
<td> 16.80672268907563 </td>
<td> 100.84033613445379</td>
<td> 19.0 </td>
<td> 3.19327731092437 </td>
<td> 20.0 </td>
<td> 19.15966386554622 </td>
<td> 120.0 </td>
<td> </td>
<td> True </td>
<td> </td>
<td> </td>
</tr>
</table>
Die Filter-Checkbox ist in Zeile 34, innerhalb der Tabelle untergebracht.
Wie man sieht wird ein Get-Request an den Server gesendet wenn der Zustand der Checkbox geändert wird, der Server sendet dann den gefilterten (nur die Positionen die bezahlt oder nicht bezahlt sind (je nach dem was die Checkbox sagt)) zurück.
Das wird realisiert durch htmx was in Zeile 1 eingebunden wurde und durch die folgenden Attribute gesteuert wird:
-
hx-trigger → definiert das JavaScript-Event welches die Aktion auslösen soll; hier "click", also wenn die Checkbox geklickt wird.
-
hx-get → Definiert das ein Get-Request gesendet werden soll und an welche URL
-
hx-target → Enthält den Selector für das Element welches ersetzt werden soll; In diesem Fall ein Element mit der ID "delivery-table"
-
hx-swap → definiert was an dem eigentlichen Element durch die Server-Antwort ersetzt werden soll. In diesem Fall das Element selbst und all seine Kind-Elemente "outerHTML"
Der Wert der Checkbox wird mit dem Request an den Server gesendet.
Das Ziel ist es letzten Endes nur die Tabelle oder Teile der Tabelle durch die Serverantwort zu ersetzen, ohne die ganze Seite neu laden zu müssen.
Anfänglich habe ich die ganze Tabelle durch die Server-Antwort ersetzt:
1
2
3
4
5
6
7
8
9
10
11
12
13
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<table id="delivery-table">
<tr>
<th>Datum</th>
<th>Menge</th>
</tr>
<tr>
<th></th>
<th>
<input type="checkbox" name="payed" hx-trigger="click" hx-get="/deliveryposition/list/table" hx-target="#delivery-table" hx-swap="outerHTML" class="">
</th>
</tr>
</table>
Das <table>-Element ist hier das mit der ID "delivery-table" und damit das was ersetzt wird.
Da die Checkbox aber selbst Teil der Tabelle ist wird sie auch mit neu geladen.
Das hat zur Folge das wenn sie vorher "checked" war, ist sie anschließend "unchecked", was nicht mehr mit den angezeigten Daten übereinstimmt und die Benutzeraktion rückgängig gemacht hat.
Das kann man natürlich auf Server-Seite abfangen, in dem man den Wert der Checkbox (und aller anderen Filter) beim rendern der Antwort entsprechend der empfangenen Werte setzt (man bekommt sie ja mit dem Request mitgesendet).
Das ist aber zusätzlicher Aufwand.
Deswegen kam ich auf die Idee nur den Inhalt (also nur die <td>-Elemente) zu ersetzen, aber nicht den Tabellenkopf.
Dabei gibt es nur ein kleines Problem, die Tabelle (samt Inhalt) sieht nach dem ersten laden so aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<table>
<tr>
<th>Datum</th>
<th>Menge</th>
<tr>
<th></th>
<th>
<input type="checkbox" name="payed" hx-trigger="click" hx-get="/deliveryposition/list/table" hx-target="#delivery-table" hx-swap="outerHTML" class="">
</th>
</tr>
<tr>
<td> 2026-01-03 </td>
<td> 6 </td>
</tr>
<tr>
<td> 2026-01-03 </td>
<td> 4 </td>
</tr>
</table>
Man hat also 2 <tr>-Element mit <th>-Elementen, also die beiden Header-Zeilen.
Daraufhin folgen <tr>-Elemente> die die Daten enthalten, in Form von <td>-Elementen.
Das Problem ist nun auf welches Element setzt man id="delivery-table", sprich, welches Element/dessen Inhalt soll ersetzt werden? Es gibt kein Container-Element in dem sich die <tr>-Element befinden die den Inhalt ausmachen, außer <table>, was ich ja aber nicht ersetzen will, weil da auch die Header, die bleiben sollen, drin stehen.
Mein erster Gedanke war die <tr> die ersetzt werden sollen mit einem <div> einzufassen, also so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<table>
<tr>
<th>Datum</th>
<th>Menge</th>
<tr>
<th></th>
<th>
<input type="checkbox" name="payed" hx-trigger="click" hx-get="/deliveryposition/list/table" hx-target="#delivery-table" hx-swap="outerHTML" class="">
</th>
</tr>
<div id="delivery-table">
<tr>
<td> 2026-01-03 </td>
<td> 6 </td>
</tr>
<tr>
<td> 2026-01-03 </td>
<td> 4 </td>
</tr>
</div>
</table>
Dabei gibt es nur ein winziges Problem, die Elemente die in <table>-Elementen eingeschlossen werden können sind definiert und weder <span>, noch <div> gehören dazu.
Das rendern von obigem HTML platzierte die Daten oberhalb der Tabelle (also praktisch außerhalb von ihr).
Lösung
<table>
<tbody>
<tr>
<th>Datum</th>
<th>Menge</th>
<tr>
<th></th>
<th>
<input type="checkbox" name="payed" hx-trigger="click" hx-get="/deliveryposition/list/table" hx-target="#delivery-table" hx-swap="innerHTML" class="">
</th>
</tr>
</tbody>
<tbody id="delivery-table">
<tr>
<td> 2026-01-03 </td>
<td> 6 </td>
</tr>
<tr>
<td> 2026-01-03 </td>
<td> 4 </td>
</tr>
</tbody>
</table>
Statt <div> kann man <tbody> verwenden.
<tbody> dient dazu Tabellenteile die inhaltlich voneinander getrennt sind zu unterscheiden. Es dürfen pro Tabelle mehrere <tbody> vorkommen.
Wenn jetzt also die Checkbox geklickt wird, wird ein Get-Request an den Server gesendet und die Antwort ersetzt den Inhalt des 2. <tbody> mit der id "delivery-table".
Es wird der Inhalt ersetzt (statt wie vorher das Element samt Inhalt), da "hx-swap" jetzt auf "innerHTML" gesetzt ist in Zeile 9.