started tables reference page
This commit is contained in:
@@ -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.
|
||||
- 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.
|
||||
- Implementing forms, uploads, refreshes, live updates, or background work.
|
||||
- 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
|
||||
- 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
|
||||
|
||||
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.
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
- Prefer event-driven updates and explicit refreshes over unrelated polling.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user