started tables reference page

This commit is contained in:
John Lancaster
2026-08-27 21:54:36 -05:00
parent 0638fe2fd7
commit 512db5f526
2 changed files with 269 additions and 1 deletions
+14 -1
View File
@@ -11,6 +11,7 @@ Use this skill as a progressive reference for NiceGUI applications built with Fa
- Planning or reviewing NiceGUI application structure and FastAPI composition. - Planning or reviewing NiceGUI application structure and FastAPI composition.
- Building or refactoring pages, components, layouts, and static assets. - Building or refactoring pages, components, layouts, and static assets.
- Creating editable tables with Python-authoritative state, validation, and persistence.
- Modeling UI state with bindings or bindable dataclasses. - Modeling UI state with bindings or bindable dataclasses.
- Implementing forms, uploads, refreshes, live updates, or background work. - Implementing forms, uploads, refreshes, live updates, or background work.
- Diagnosing UI state, concurrency, navigation, or asset problems. - Diagnosing UI state, concurrency, navigation, or asset problems.
@@ -71,6 +72,16 @@ Load [component mechanics and customization](./references/component-mechanics-an
- `ui.select` and `ui.icon` mechanics and caveats - `ui.select` and `ui.icon` mechanics and caveats
- dialog scaling when detached popup geometry must be preserved - dialog scaling when detached popup geometry must be preserved
### Editable Tables
Load [editable tables](./references/tables.md) for:
- Python-authoritative editable `ui.table` state
- stable row identity across sorting, filtering, and pagination
- NiceGUI editors in Quasar `body-cell-*` scoped slots
- validation, persistence, rejection, and canonical row refresh
- the full `body` slot required when escalating to `QPopupEdit`
### Bindable State ### Bindable State
Load [bindable dataclasses](./references/binding-dataclasses.md) for: Load [bindable dataclasses](./references/binding-dataclasses.md) for:
@@ -118,7 +129,8 @@ Load [source documentation](./references/source-documentation.md) when:
1. Load [application architecture](./references/architecture.md) for page and component ownership decisions. 1. Load [application architecture](./references/architecture.md) for page and component ownership decisions.
2. Load [styling and customization](./references/styling-and-customization.md) for themes, layout, responsive presentation, utility classes, or CSS. 2. Load [styling and customization](./references/styling-and-customization.md) for themes, layout, responsive presentation, utility classes, or CSS.
3. Load [component mechanics and customization](./references/component-mechanics-and-customization.md) when behavior must be mapped across NiceGUI, Quasar, and Vue, or when detached content and component-specific behavior are involved. 3. Load [component mechanics and customization](./references/component-mechanics-and-customization.md) when behavior must be mapped across NiceGUI, Quasar, and Vue, or when detached content and component-specific behavior are involved.
4. Add [interaction patterns](./references/interaction-patterns.md) or [bindable dataclasses](./references/binding-dataclasses.md) according to the page behavior. 4. Load [editable tables](./references/tables.md) when table cells accept user changes or `QPopupEdit` is being considered.
5. Add [interaction patterns](./references/interaction-patterns.md) or [bindable dataclasses](./references/binding-dataclasses.md) according to the page behavior.
### Debugging Or Production Review ### Debugging Or Production Review
@@ -133,6 +145,7 @@ Load [source documentation](./references/source-documentation.md) when:
- Avoid blocking I/O and CPU-heavy work in the UI event loop. - Avoid blocking I/O and CPU-heavy work in the UI event loop.
- Prefer event-driven updates and explicit refreshes over unrelated polling. - Prefer event-driven updates and explicit refreshes over unrelated polling.
- Discover component capabilities through NiceGUI docs and constructors, then the wrapped Quasar API. - Discover component capabilities through NiceGUI docs and constructors, then the wrapped Quasar API.
- Keep editable table records authoritative in Python; send stable row keys with edit proposals and reassert canonical rows after validation.
- Research the current NiceGUI and Quasar source documentation before generating component-specific code or CSS. - Research the current NiceGUI and Quasar source documentation before generating component-specific code or CSS.
- Prefer constructor arguments and native Quasar features through NiceGUI; use Tailwind for structure and scoped static CSS for stable fine tuning. - Prefer constructor arguments and native Quasar features through NiceGUI; use Tailwind for structure and scoped static CSS for stable fine tuning.
- Provide loading, success, and failure states for user-triggered work. - Provide loading, success, and failure states for user-triggered work.
@@ -0,0 +1,255 @@
# Editable Tables
Use this reference when a `ui.table` must accept cell edits while Python remains the authoritative owner of row state. Start with NiceGUI elements in named QTable cell slots. Escalate to raw Quasar row templates only when a requirement, such as `QPopupEdit`, cannot work in a cell slot.
## Version Baseline
This reference was verified against the latest released NiceGUI stack at the time of research:
| Layer | Version | Version evidence |
| --- | --- | --- |
| NiceGUI | `3.16.0` | [NiceGUI `v3.16.0` release](https://github.com/zauberzeug/nicegui/releases/tag/v3.16.0) |
| Quasar | `2.18.5` | [NiceGUI `v3.16.0` frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json) |
| Vue | `3.5.22` | [NiceGUI `v3.16.0` frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json) |
Recheck the dependency manifest and tagged sources when the target application uses another NiceGUI release. Do not infer the Quasar or Vue version from their latest independent releases; use the versions bundled by NiceGUI.
## Ownership Model
Treat an edit as a proposal, not a browser-side state mutation:
1. Python projects canonical records into `table.rows`.
2. A NiceGUI editor displays `props.value` from a QTable scoped slot.
3. The editor emits stable row identity, the field name, and the proposed value.
4. Python locates the canonical record, validates and coerces the proposal, and persists the accepted record.
5. Python sends a fresh canonical projection back with `table.update_rows(...)` whether the proposal was accepted or rejected.
```mermaid
flowchart LR
A[Python records] -->|project rows| B[QTable]
B -->|props.value| C[NiceGUI editor]
C -->|row key, field, proposed value| D[Python handler]
D --> E{validate}
E -->|accept| F[persist]
E -->|reject| G[notify]
F --> A
G --> A
```
The browser may hold temporary editor state, but it is never the source of truth. Do not mutate `props.row` and mistake Vue reactivity for persistence. Do not use a visual row index as identity: sorting, filtering, and pagination can all change it. Set `row_key` to an immutable, unique field and send that value with every edit proposal.
## Recommended Cell-Slot Pattern
[NiceGUI `ui.table`](https://nicegui.io/documentation/table) supports NiceGUI elements in scoped slots since `3.5.0`. The tagged [`Table.cell` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.py) creates the corresponding Quasar `QTd`, while the tagged [table client component](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.js) forwards QTable's scoped slot props.
The following example keeps canonical records in Python, uses a stable `id`, allows only known fields, validates every proposed value, and reasserts canonical rows after each event. `save_item` is the persistence boundary; replace its in-memory assignment with the application's service or repository call.
```python
from dataclasses import dataclass, replace
from nicegui import events, ui
@dataclass(frozen=True, slots=True)
class Item:
id: int
name: str
quantity: int
status: str
STATUS_OPTIONS = ["draft", "active", "archived"]
items_by_id = {
101: Item(id=101, name="Desk", quantity=4, status="active"),
102: Item(id=102, name="Lamp", quantity=12, status="draft"),
}
def canonical_rows() -> list[dict[str, str | int]]:
return [
{
"id": item.id,
"name": item.name,
"quantity": item.quantity,
"status": item.status,
}
for item in items_by_id.values()
]
def validate_edit(item: Item, field: str, raw_value: object) -> Item:
match field:
case "name":
if not isinstance(raw_value, str) or not (name := raw_value.strip()):
raise ValueError("Name is required")
return replace(item, name=name)
case "quantity":
if isinstance(raw_value, bool) or not isinstance(raw_value, (int, float, str)):
raise ValueError("Quantity must be an integer")
if isinstance(raw_value, float) and not raw_value.is_integer():
raise ValueError("Quantity must be an integer")
try:
quantity = int(raw_value)
except (ValueError, OverflowError) as error:
raise ValueError("Quantity must be an integer") from error
if not 0 <= quantity <= 1_000:
raise ValueError("Quantity must be between 0 and 1000")
return replace(item, quantity=quantity)
case "status":
if not isinstance(raw_value, str) or raw_value not in STATUS_OPTIONS:
raise ValueError("Unknown status")
return replace(item, status=raw_value)
case _:
raise ValueError(f"Field {field!r} is not editable")
def save_item(item: Item) -> None:
items_by_id[item.id] = item
def apply_edit(event: events.GenericEventArguments) -> None:
try:
raw_row_id, raw_field, raw_value = event.args
row_id = int(raw_row_id)
field = str(raw_field)
current = items_by_id.get(row_id)
if current is None:
raise ValueError("This row no longer exists")
save_item(validate_edit(current, field, raw_value))
except (TypeError, ValueError) as error:
ui.notify(str(error), type="negative")
finally:
table.update_rows(canonical_rows(), clear_selection=False)
columns = [
{"name": "name", "label": "Name", "field": "name", "align": "left"},
{"name": "quantity", "label": "Quantity", "field": "quantity", "align": "right"},
{"name": "status", "label": "Status", "field": "status", "align": "left"},
]
table = ui.table(
columns=columns,
rows=canonical_rows(),
row_key="id",
selection="multiple",
).classes("w-full")
with table.add_slot("body-cell-name"):
with table.cell("name"):
ui.input().props(
':model-value="props.value" dense borderless debounce=400'
).on(
"update:model-value",
handler=apply_edit,
js_handler="(e) => emit(props.row.id, props.col.name, e.value)",
)
with table.add_slot("body-cell-quantity"):
with table.cell("quantity"):
ui.number(min=0, max=1_000).props(
':model-value="props.value" dense borderless debounce=400'
).on(
"update:model-value",
handler=apply_edit,
js_handler="(e) => emit(props.row.id, props.col.name, e.value)",
)
with table.add_slot("body-cell-status"):
with table.cell("status"):
ui.select(STATUS_OPTIONS).props(
':model-value="props.value" dense borderless options-dense'
).on(
"update:model-value",
handler=apply_edit,
js_handler="(e) => emit(props.row.id, props.col.name, e.value)",
)
ui.run()
```
This uses the same transformed-event path documented by [NiceGUI's table selection example](https://nicegui.io/documentation/table): `.on("update:model-value", ...)` attaches directly to the editor, and `js_handler` emits only the serializable values Python needs. Vue component events [do not bubble](https://vuejs.org/guide/components/events.html), so listening on the table or cell instead of the editor will not capture the editor's model update.
The `:model-value="props.value"` prop is deliberately one-way. In Vue, component `v-model` expands to a `modelValue` prop plus an `update:modelValue` listener, as shown in the [Vue component `v-model` guide](https://vuejs.org/guide/components/v-model.html) and its tagged [compiler transform](https://github.com/vuejs/core/blob/v3.5.22/packages/compiler-core/src/transforms/vModel.ts). Here the update listener sends an intent to Python rather than assigning into `props.row`.
## Commit Policy
Choose when edits cross the client-server boundary according to the editor:
- Use `update:model-value` for discrete editors such as `ui.select`, switches, and checkboxes.
- For text and numeric inputs, use Quasar's documented `debounce` prop when accepting edits during typing. A trailing delay avoids one server round trip per keystroke.
- When the user must explicitly save or cancel a multi-field draft, keep the draft in a dialog or popup and emit one proposal on save. Python must still validate and reassert the canonical row.
- For asynchronous persistence, disable or mark the affected editor busy while saving. Add an entity version or other optimistic concurrency check when multiple clients can edit the same record.
Do not rely on browser validation alone. Quasar editor constraints improve feedback, but the event payload is still untrusted input. The Python handler must enforce the editable-field allowlist, types, ranges, permissions, record existence, and persistence constraints.
## Persistence And Refresh
Keep `table.rows` as a projection, not the business model. On acceptance:
1. validate and coerce into domain types
2. call the service or repository that owns persistence
3. fetch or derive the resulting canonical records
4. call `table.update_rows(canonical_rows(), clear_selection=False)`
On rejection, perform step 4 anyway. This overwrites any temporary editor display with the last accepted value. Preserve selection only when the selected row identities remain valid; otherwise use the default `clear_selection=True`.
For database-backed applications, make the handler `async`, await the service transaction, and refresh only after it commits. Catch the application's expected validation, conflict, and persistence exceptions separately so the user receives actionable feedback without hiding programming errors.
## QTable And QPopupEdit Escalation
The underlying [Quasar QTable guide](https://quasar.dev/vue-components/table) and tagged [`QTable` source](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.js) define the `body-cell-[name]` props used above, including `row`, `col`, `value`, and the key derived from `row-key`.
Use [Quasar `QPopupEdit`](https://quasar.dev/vue-components/popup-edit) only when its local draft, validation, save, and cancel interaction is specifically required. Quasar documents that `QPopupEdit` does not work in QTable cell scoped slots; it must be placed under the full `body` slot. Its tagged [source implementation](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/popup-edit/QPopupEdit.js) keeps a cloned draft and emits `save` and `update:modelValue` only after validation.
That restriction changes the implementation boundary: a full `body` slot must render every `QTr` and `QTd`, preserve QTable's scoped props and row keys, and host the popup. Before taking this path:
1. confirm an ordinary NiceGUI editor or dialog cannot meet the interaction requirement
2. copy the row structure from the matching Quasar `2.18.5` QTable documentation, not another version
3. keep popup draft state local rather than assigning into `props.row`
4. emit the stable row key, field, and saved proposal to Python
5. validate, persist, and replace the table rows from Python exactly as in the cell-slot pattern
6. test keyboard focus, save, cancel, validation failure, sorting, filtering, pagination, and selection
Replacing the full row template has a larger maintenance and accessibility surface. Keep the named cell-slot implementation as the default.
## Source Map
### NiceGUI `3.16.0`
- [Table developer documentation](https://nicegui.io/documentation/table)
- [`Table` Python source](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.py)
- [QTable client wrapper source](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.js)
- [Pinned frontend dependency manifest](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json)
### Quasar `2.18.5`
- [QTable developer documentation](https://quasar.dev/vue-components/table)
- [`QTable` source](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.js)
- [`QTable` API definition](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.json)
- [QPopupEdit developer documentation](https://quasar.dev/vue-components/popup-edit)
- [`QPopupEdit` source](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/popup-edit/QPopupEdit.js)
- [`QPopupEdit` API definition](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/popup-edit/QPopupEdit.json)
### Vue `3.5.22`
- [Component `v-model`](https://vuejs.org/guide/components/v-model.html)
- [Component events](https://vuejs.org/guide/components/events.html)
- [Scoped slots](https://vuejs.org/guide/components/slots.html#scoped-slots)
- [`v-model` compiler transform](https://github.com/vuejs/core/blob/v3.5.22/packages/compiler-core/src/transforms/vModel.ts)
- [Component event runtime](https://github.com/vuejs/core/blob/v3.5.22/packages/runtime-core/src/componentEmits.ts)
- [Native `v-model` directives](https://github.com/vuejs/core/blob/v3.5.22/packages/runtime-dom/src/directives/vModel.ts)
## Completion Check
Before accepting an editable table:
1. Pin the NiceGUI release and verify its bundled Quasar and Vue versions.
2. Use an immutable, unique `row_key`; never persist by view index.
3. Bind each editor's displayed value from QTable scoped props.
4. Attach the event listener directly to the editor and emit only row identity, field, and proposed value.
5. Validate field access, types, ranges, permissions, and record existence in Python.
6. Persist through the owning service or repository.
7. Reassert canonical rows after accepted and rejected proposals.
8. Test editing after sort, filter, pagination, and selection changes.
9. Test stale rows, invalid input, persistence failure, and concurrent edits.
10. Use a full `body` slot for `QPopupEdit`, never a `body-cell-*` slot.