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 |
| Quasar | 2.18.5 |
NiceGUI v3.16.0 frontend dependencies |
| Vue | 3.5.22 |
NiceGUI v3.16.0 frontend dependencies |
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.
Table Ownership Model¶
This pattern combines server-authoritative component events with bindable model projections:
- A render function converts dataframe records into row-scoped bindable dataclasses.
- Each bindable row owns its serializable QTable projection and whether an accepted edit has touched it.
- A NiceGUI editor displays that projection through
props.valuein a QTable scoped slot. - The editor emits stable row identity, the field name, and the proposed value.
- Python locates the row, validates and assigns the value, marks it as touched, and sends the resulting projection back with
table.update_rows(...).
flowchart LR
A[Dataframe or repository] -->|render| B[EditableTableState]
B --> C[Bindable row: fields, payload, touched]
C -->|field bindings| D[QTable row payloads]
D -->|props.value| E[NiceGUI editor]
E -->|row key, field, proposed value| F[Python handler]
F --> G{validate}
G -->|accept and mark touched| C
C -->|persist touched rows later| A
G -->|reject| H[notify]
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. Do not mutate props.row; locate the row model by its stable key and let Python update the bound QTable projection.
Recommended Cell-Slot Pattern¶
NiceGUI ui.table supports NiceGUI elements in scoped slots since 3.5.0. The tagged Table.cell implementation creates the corresponding Quasar QTd, while the tagged table client component forwards QTable's scoped slot props.
The following example uses a render function to transform a dataframe into an EditableTableState. Its rows_by_id container owns one EditableRow per stable identifier. Each row keeps its serializable QTable payload and touched flag with the editable fields, while the container provides identity lookup and ordered projections. The detailed binding.bind_to propagation behavior is covered by bindable dataclasses.
state.touched_rows() returns touched EditableRow instances that remain in rows_by_id, in table order. The example marks a row after an edit validates and leaves persistence to the caller, which can persist the returned dataclasses in one batch. Its Show changes button uses the same method to report each changed row's current ID, name, quantity, and status. Removing or replacing a row in the container automatically excludes the former object.
A QTable scoped slot is one client-side template reused for every matching cell. It cannot use bind_value(row_state, "name") because there is no single Python row_state for that template. Instead, the slot reads the bound payload through props.value and sends the stable key back to Python, where the handler selects and assigns the corresponding dataclass.
The complete runnable source is available as editable_table.py and as the supporting resource skill://nicegui/examples/editable_table.py.
The editor path uses the transformed-event pattern from controlled values and model events. Attach the listener directly to each cell editor because Vue component events do not bubble from the editor to the cell or table. Read the QTable cell value from props.value, emit props.row.<row_key>, props.col.name, and the proposed value, then resolve the row in Python. NiceGUI's text-input wrapper uses value and update:value, while the number and select editors use model-value and update:model-value. Remove the text input's static value prop before adding its scoped :value binding. The select editor emits a NiceGUI-normalized option object, so this example forwards option.label, which is also the canonical value in STATUS_OPTIONS.
When a row needs an explicit save/cancel workflow, add an actions cell (for example body-cell-actions) that emits only the immutable row key and opens one reusable ui.dialog. Keep dialog controls as local draft state rather than binding directly to the authoritative row model. On Save, re-resolve the row by key, validate and normalize every proposed field in Python (for example with a small Pydantic draft model), and then commit all assignments together so partial validation failure cannot leave mixed old/new values. On Cancel or dialog dismiss, close the dialog without mutating authoritative state. This keeps the table in named cell slots and avoids the full-row templating boundary required by QPopupEdit.
Persistence And Row Refresh¶
Keep table.rows as the serializable projection described in bindable dataclasses, not the business model. After every accepted or rejected proposal, call table.update_rows(state.table_rows(), clear_selection=False) so the canonical projection replaces any temporary editor display. Preserve selection only while the selected row identities remain valid; otherwise use the default clear_selection=True.
QTable And QPopupEdit Escalation¶
The underlying Quasar QTable guide and tagged QTable source define the body-cell-[name] props used above, including row, col, value, and the key derived from row-key.
Use Quasar QPopupEdit 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 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:
- confirm an ordinary NiceGUI editor or dialog cannot meet the interaction requirement
- copy the row structure from the matching Quasar
2.18.5QTable documentation, not another version - keep popup draft state local rather than assigning into
props.row, following the explicit save/cancel proposal pattern - emit the stable row key, field, and saved proposal to Python
- validate, persist, and replace the table rows from Python exactly as in the cell-slot pattern
- 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
TablePython source- QTable client wrapper source
- Pinned frontend dependency manifest
Quasar 2.18.5¶
- QTable developer documentation
QTablesourceQTableAPI definition- QPopupEdit developer documentation
QPopupEditsourceQPopupEditAPI definition
Completion Check¶
Before accepting an editable table:
- Pin the NiceGUI release and verify its bundled Quasar and Vue versions.
- Use an immutable, unique
row_key; never persist by view index. - Transform dataframe records into row-scoped bindable dataclasses during rendering.
- Keep each dataclass, serializable QTable row, and touched flag together on one bindable row.
- Display the projected value from QTable scoped props; do not bind one shared slot template to one Python row object.
- Apply the controlled-value event proposal directly to each editor and emit only row identity, field, and proposed value.
- Validate and normalize proposals in Python before assigning them.
- Mark accepted rows as touched and derive touched dataclasses from the rows still held by the container.
- Reassert canonical rows after accepted and rejected proposals.
- Test editing after sort, filter, pagination, and selection changes.
- Test stale rows, invalid input, persistence failure, concurrent edits, and removal of touched bindings.
- Use a full
bodyslot forQPopupEdit, never abody-cell-*slot.