Skip to content

Commit f82c96c

Browse files
docs: Add guide on table structure limitations
1 parent f80b27e commit f82c96c

File tree

4 files changed

+174
-0
lines changed

4 files changed

+174
-0
lines changed

docs/source/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ templates
2222
actions
2323
child-components
2424
django-models
25+
table-limitations
2526
```
2627

2728
```{toctree}

docs/source/table-limitations.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Table Rendering Limitations
2+
3+
When using components inside HTML `<table>` elements, you must follow strict HTML structure rules. Failing to do so can cause reactivity issues that may look like state updates are not working.
4+
5+
This is not a bug in **Django Unicorn**, but a consequence of how browsers handle table DOM structure.
6+
7+
## Why This Happens
8+
9+
HTML tables are **structurally strict**. Browsers automatically correct invalid markup inside table elements.
10+
11+
Valid structure rules:
12+
* `<table>` may contain `<thead>`, `<tbody>`, `<tfoot>`
13+
* `<tbody>` may contain **only** `<tr>`
14+
* `<tr>` may contain **only** `<td>` or `<th>`
15+
* `<td>` may contain flow content (`<div>`, `<span>`, etc.)
16+
17+
If a component rendered inside a `<tbody>` outputs something like:
18+
19+
```html
20+
<tr>
21+
<td>Row content</td>
22+
</tr>
23+
24+
<div class="modal">...</div>
25+
```
26+
27+
The `<div>` is **invalid inside `<tbody>`**, so the browser will automatically move it elsewhere in the DOM.
28+
29+
When this happens:
30+
1. The browser modifies the DOM structure.
31+
2. Unicorn's DOM diffing no longer matches the expected structure.
32+
3. Reactive updates may silently fail or appear inconsistent.
33+
34+
This commonly appears as:
35+
* A modal not showing reactively
36+
* Conditional blocks not updating
37+
* Child components not re-rendering properly
38+
39+
## Example of Problematic Pattern
40+
41+
Parent template:
42+
43+
```html
44+
<tbody>
45+
{% for item in items %}
46+
{% unicorn 'row-component' item=item key=item.id %}
47+
{% endfor %}
48+
</tbody>
49+
```
50+
51+
Child component template:
52+
53+
```html
54+
<tr>
55+
<td>{{ item.name }}</td>
56+
<td>
57+
<button unicorn:click="show_modal">Delete</button>
58+
</td>
59+
</tr>
60+
61+
{% if modal_visible %}
62+
<div class="modal">Are you sure?</div>
63+
{% endif %}
64+
```
65+
66+
The `<div>` rendered after `<tr>` is invalid inside `<tbody>` and will be relocated by the browser.
67+
68+
## Why It Works With `<div>`
69+
70+
Replacing table tags with `<div>` works because `<div>` elements have no structural constraints. The browser does not auto-correct their placement, so Unicorn's DOM diffing remains stable.
71+
72+
## Recommended Solutions
73+
74+
### 1. Move Modals Outside the Table (Recommended)
75+
76+
Control modal state from the parent component and render the modal outside the `<table>`.
77+
78+
Child component:
79+
80+
```python
81+
def request_delete(self):
82+
self.parent.confirm_delete(self.item.id)
83+
```
84+
85+
Parent component:
86+
87+
```python
88+
selected_id = None
89+
show_modal = False
90+
91+
def confirm_delete(self, item_id):
92+
self.selected_id = item_id
93+
self.show_modal = True
94+
```
95+
96+
Render the modal below the table:
97+
98+
```html
99+
</table>
100+
101+
{% if show_modal %}
102+
<div class="modal">...</div>
103+
{% endif %}
104+
```
105+
106+
This keeps table markup valid and avoids DOM restructuring.
107+
108+
### 2. Render Modal Inside a Valid `<td>`
109+
110+
If you must render it within the table, wrap it inside a `<td colspan="...">`:
111+
112+
```html
113+
{% if modal_visible %}
114+
<tr>
115+
<td colspan="5">
116+
<div class="modal">...</div>
117+
</td>
118+
</tr>
119+
{% endif %}
120+
```
121+
122+
This preserves valid table structure.
123+
124+
### 3. Ensure Single Valid Root Per Component
125+
126+
When rendering a component inside `<tbody>`, its root element must be a `<tr>`.
127+
When rendering inside `<tr>`, its root must be `<td>` or `<th>`.
128+
129+
Avoid rendering sibling elements that break table hierarchy.
130+
131+
## Key Takeaway
132+
133+
If you experience reactivity issues inside tables:
134+
* Verify your component outputs valid HTML table structure.
135+
* Ensure no `<div>` or non-table elements are placed directly inside `<tbody>` or `<tr>`.
136+
* Prefer handling overlays and modals outside the table.
137+
138+
Table elements are one of the strictest parts of HTML. Following valid structure rules ensures Unicorn's DOM diffing remains reliable and reactive updates function correctly.

docs/source/templates.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Templates are normal Django HTML templates, so anything you could normally do in
55
```{warning}
66
`Unicorn` requires there to be one root element that contains the component HTML. Valid HTML and a wrapper element is required for the DOM diffing algorithm to work correctly, so `Unicorn` will try to log a warning message if they seem invalid.
77
8+
One common issue is when a component is a table row (`tr`). Since `tr` elements can only contain `td` or `th` elements, any other element (like a `div`) will be "foster parented" out of the table structure by the browser. This will cause the DOM diffing to fail since the element structure is not what `Unicorn` expects.
9+
810
For example, this is an **invalid** template:
911
:::{code} html
1012
:force: true

docs/source/troubleshooting.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,36 @@ MIDDLEWARE = [
4646
</body>
4747
</html>
4848
```
49+
50+
## Tables and invalid HTML
51+
52+
Browsers are very strict about table structure (e.g. `<tr>` can only be a direct child of `<tbody>`, `<thead>`, `<tfoot>`, or `<table>`, and `<div>` is not allowed as a direct child of `<tr>`). If you have a component that renders a table row, unexpected behavior can occur because the browser will "foster parent" invalid elements out of the table structure before `Unicorn` can seemingly react to them.
53+
54+
For example, this valid-looking component template will cause issues:
55+
56+
```html
57+
<!-- invalid-table-row.html -->
58+
<tr>
59+
<td>{{ name }}</td>
60+
{% if show_modal %}
61+
<div class="modal">...</div>
62+
{% endif %}
63+
</tr>
64+
```
65+
66+
The browser will move the `div` out of the `tr` (and likely out of the `table` entirely), so when `Unicorn` tries to update the component, it will be confused by the missing element.
67+
68+
To fix this, ensure that all content is inside a valid table element, like a `td`:
69+
70+
```html
71+
<!-- valid-table-row.html -->
72+
<tr>
73+
<td>{{ name }}</td>
74+
<td>
75+
{% if show_modal %}
76+
<div class="modal">...</div>
77+
{% endif %}
78+
</td>
79+
</tr>
80+
```
81+

0 commit comments

Comments
 (0)