Compare commits

...
4 Commits
Author SHA1 Message Date
John Lancaster 65669a2100 nicegui table updates 2026-08-30 01:01:50 -05:00
John Lancaster 3e2fc0ef25 dataclasses enhancement 2026-08-30 00:22:18 -05:00
John Lancaster 88474a75f5 doc updates for new structure 2026-08-30 00:16:58 -05:00
John Lancaster afedcda930 nicegui component mechanics 2026-08-30 00:06:22 -05:00
21 changed files with 822 additions and 294 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
This repository is resource-first. This repository is resource-first.
- Canonical skill guidance lives in `docs/skills/<skill-id>/SKILL.md`. - Canonical skill guidance lives in `src/personal_mcp/docs/skills/<skill-id>/SKILL.md`.
- Skills are exposed through FastMCP's native `skill://` resource family. - Skills are exposed through FastMCP's native `skill://` resource family.
- Prompts are exposed through native MCP prompt operations (`prompts/list`, `prompts/get`). - Prompts are exposed through native MCP prompt operations (`prompts/list`, `prompts/get`).
- General documentation pages are exposed through `resource://docs/{path*}`. - General documentation pages are exposed through `resource://docs/{path*}`.
@@ -1,19 +1,19 @@
--- ---
name: Authoring Content name: Authoring Content
description: "Use when editing Markdown under docs/. Routes authors to the canonical docs ownership, layout, and symlink guidance." description: "Use when editing Markdown under src/personal_mcp/docs/. Routes authors to the canonical docs ownership and layout guidance."
applyTo: 'docs/**/*.md' applyTo: 'src/personal_mcp/docs/**/*.md'
--- ---
For edits under `docs/`, use the [Authoring Guide](../../docs/authoring.md) as the entry point for content placement and contracts. For edits under `src/personal_mcp/docs/`, use the [Authoring Guide](../../src/personal_mcp/docs/authoring.md) as the entry point for content placement and contracts.
For source-tree ownership, symlink, packaging, or runtime questions, follow [Source Tree Ownership](../../docs/authoring.md). Treat that section as authoritative instead of restating its guidance here. For source-tree ownership, packaging, or runtime questions, follow [Source Tree Ownership](../../src/personal_mcp/docs/authoring.md). Treat that section as authoritative instead of restating its guidance here.
Primary references: Primary references:
- [Skill contract](../../docs/contracts/skill_contract.md) - [Skill contract](../../src/personal_mcp/docs/contracts/skill_contract.md)
- [Prompt contract](../../docs/contracts/prompt.md) - [Prompt contract](../../src/personal_mcp/docs/contracts/prompt.md)
- [Frontmatter contract](../../docs/contracts/frontmatter.md) - [Frontmatter contract](../../src/personal_mcp/docs/contracts/frontmatter.md)
- [URI contract](../../docs/contracts/uris.md) - [URI contract](../../src/personal_mcp/docs/contracts/uris.md)
- `skill://zensical-docs/SKILL.md` - `skill://zensical-docs/SKILL.md`
Inspect `skill://zensical-docs/_manifest` only when a supporting documentation reference is needed. Inspect `skill://zensical-docs/_manifest` only when a supporting documentation reference is needed.
@@ -21,4 +21,4 @@ Execution pattern:
If task intent is ambiguous, ask one clarifying question before editing. If task intent is ambiguous, ask one clarifying question before editing.
Be sure to also refer to the [testing page](../../docs/testing.md) page for design detail Be sure to also refer to the [testing page](../../src/personal_mcp/docs/testing.md) for design detail.
@@ -26,11 +26,11 @@ Use this prompt after test scaffolding exists and method names/docstrings are al
Load these in order and use only what matches the task: Load these in order and use only what matches the task:
1. Core defaults: [pytest scaffolding skill](../../docs/skills/pytesting/SKILL.md) 1. Core defaults: [pytest scaffolding skill](../../src/personal_mcp/docs/skills/pytesting/SKILL.md)
2. Naming/hierarchy preservation: [naming and organization](../../docs/skills/pytesting/references/naming-and-organization.md) 2. Naming/hierarchy preservation: [naming and organization](../../src/personal_mcp/docs/skills/pytesting/references/naming-and-organization.md)
3. Baseline pytest fixtures/markers: [pytest docs notes](../../docs/skills/pytesting/references/pytest-docs.md) 3. Baseline pytest fixtures/markers: [pytest docs notes](../../src/personal_mcp/docs/skills/pytesting/references/pytest-docs.md)
4. FastAPI-specific behavior (only when needed): [fastapi testing](../../docs/skills/pytesting/references/fastapi-testing.md) 4. FastAPI-specific behavior (only when needed): [fastapi testing](../../src/personal_mcp/docs/skills/pytesting/references/fastapi-testing.md)
5. SQLAlchemy-specific behavior (only when needed): [sqlalchemy testing](../../docs/skills/pytesting/references/sqlalchemy-testing.md) 5. SQLAlchemy-specific behavior (only when needed): [sqlalchemy testing](../../src/personal_mcp/docs/skills/pytesting/references/sqlalchemy-testing.md)
## Workflow ## Workflow
+2 -2
View File
@@ -24,8 +24,8 @@ Use this prompt to do in one run what we have been doing manually in chat:
## Repository Rules To Apply ## Repository Rules To Apply
- Use [pytest scaffolding skill](../../docs/skills/pytesting/SKILL.md) for strategy and defaults. - Use [pytest scaffolding skill](../../src/personal_mcp/docs/skills/pytesting/SKILL.md) for strategy and defaults.
- Use [naming and organization reference](../../docs/skills/pytesting/references/naming-and-organization.md) before finalizing hierarchy. - Use [naming and organization reference](../../src/personal_mcp/docs/skills/pytesting/references/naming-and-organization.md) before finalizing hierarchy.
- Use `uv run pytest --collect-only -q` as structural validation. - Use `uv run pytest --collect-only -q` as structural validation.
- Default to a source-mirror style adapted to this repository: - Default to a source-mirror style adapted to this repository:
- map selected modules to `tests/` with concise path segments when requested - map selected modules to `tests/` with concise path segments when requested
+6 -6
View File
@@ -6,7 +6,7 @@ icon: lucide/library
## Overview ## Overview
The application combines a FastMCP server with a pre-built Zensical documentation site. Markdown under `docs/` is the single authored content tree, while native FastMCP providers own skill and prompt discovery. The application combines a FastMCP server with a pre-built Zensical documentation site. Markdown under `src/personal_mcp/docs/` is the single authored content tree, while native FastMCP providers own skill and prompt discovery.
The runtime has four content paths: The runtime has four content paths:
@@ -23,8 +23,8 @@ There is no custom skill catalog, prompt catalog, or per-prompt Python module.
Each skill owns one directory: Each skill owns one directory:
1. `docs/skills/<skill-id>/SKILL.md` 1. `src/personal_mcp/docs/skills/<skill-id>/SKILL.md`
2. `docs/skills/<skill-id>/<supporting-path>` 2. `src/personal_mcp/docs/skills/<skill-id>/<supporting-path>`
`SkillsDirectoryProvider` publishes: `SkillsDirectoryProvider` publishes:
@@ -36,7 +36,7 @@ The provider parses standard skill frontmatter and generates the manifest. The g
### Prompts ### Prompts
Each prompt has one source: `docs/prompts/<prompt-id>/PROMPT.md`. Its nested `prompt` frontmatter owns runtime metadata and argument declarations, while its body owns canonical prose. Each prompt has one source: `src/personal_mcp/docs/prompts/<prompt-id>/PROMPT.md`. Its nested `prompt` frontmatter owns runtime metadata and argument declarations, while its body owns canonical prose.
The custom provider reads packaged Markdown with `importlib.resources`, validates metadata and exact placeholder-to-argument equality, and creates native FastMCP prompt objects. It rescans on each list and get request, so an editable deployment observes file additions, edits, and deletions without a restart. The custom provider reads packaged Markdown with `importlib.resources`, validates metadata and exact placeholder-to-argument equality, and creates native FastMCP prompt objects. It rescans on each list and get request, so an editable deployment observes file additions, edits, and deletions without a restart.
@@ -65,7 +65,7 @@ Server construction is lazy with respect to package import. Each application pro
## Packaging ## Packaging
The repository root `docs/` directory is the only authored Markdown source. `src/personal_mcp/docs` is a relative symlink used by source checkouts and editable installs. Hatchling follows it and stores regular files beneath `personal_mcp/docs/` in the wheel. The regular directory `src/personal_mcp/docs/` is the only authored Markdown source. The `uv_build` backend includes it as package data beneath `personal_mcp/docs/` in built distributions.
Runtime reads are package-relative: Runtime reads are package-relative:
@@ -92,7 +92,7 @@ Only these canonical provider and protocol surfaces are registered.
## Static Documentation ## Static Documentation
Zensical builds `docs/` into `site/` before deployment. FastAPI mounts that immutable output in the same process that hosts FastMCP. Generated `site/` files are deployment assets and are never an authored source. Zensical builds `src/personal_mcp/docs/` into `src/personal_mcp/site/` before deployment. FastAPI mounts that immutable output in the same process that hosts FastMCP. Generated site files are deployment assets and are never an authored source.
## Validation ## Validation
+8 -9
View File
@@ -4,7 +4,7 @@ icon: lucide/pencil
# Authoring Guide # Authoring Guide
This page defines the practical workflow for maintaining skills, prompts, and project documentation while keeping root `docs/` as the only authored source. This page defines the practical workflow for maintaining skills, prompts, and project documentation in the package-native `src/personal_mcp/docs` source tree.
Primary references: Primary references:
@@ -16,16 +16,16 @@ Primary references:
## Source Tree Ownership ## Source Tree Ownership
Edit content only under root `docs/`. The `src/personal_mcp/docs` path is a relative symlink for editable installs; do not author through a copied package tree. Edit content only under `src/personal_mcp/docs`. This directory is the canonical authored source for both MCP content and the documentation site.
Hatchling's normal package traversal follows `src/personal_mcp/docs` during wheel builds and archives the linked targets as regular files under `personal_mcp/docs/`. Do not add a `force-include` entry for root `docs/`; it duplicates those wheel paths. The installed package therefore gives `SkillsDirectoryProvider` a regular filesystem directory while Zensical builds the human site directly from root `docs/`. The `uv_build` backend packages this tree under `personal_mcp/docs/`. The installed package therefore gives runtime providers package-relative content, while Zensical builds the human site directly from `src/personal_mcp/docs` as configured by `docs_dir` in the repository's `zensical.toml`.
Generated `site/` content is a build artifact and must not be edited by hand. Generated `src/personal_mcp/site/` content is a build artifact and must not be edited by hand.
## Content Layout ## Content Layout
```text ```text
docs/ src/personal_mcp/docs/
*.md *.md
contracts/ contracts/
prompts/<prompt-id>/ prompts/<prompt-id>/
@@ -40,7 +40,7 @@ Keep skill and prompt files inside their owning directories. Relative links may
## Skill Authoring ## Skill Authoring
A skill is discovered when a direct child of `docs/skills/` contains `SKILL.md`. A skill is discovered when a direct child of `src/personal_mcp/docs/skills/` contains `SKILL.md`.
Required frontmatter: Required frontmatter:
@@ -72,9 +72,9 @@ Recommended sequence:
## Prompt Authoring ## Prompt Authoring
A prompt is one self-describing `docs/prompts/<prompt-id>/PROMPT.md` file: A prompt is one self-describing `src/personal_mcp/docs/prompts/<prompt-id>/PROMPT.md` file:
1. Create a lowercase kebab-case directory beneath `docs/prompts/`. 1. Create a lowercase kebab-case directory beneath `src/personal_mcp/docs/prompts/`.
2. Add a nested `prompt` frontmatter mapping with version, description, tags, and ordered arguments. 2. Add a nested `prompt` frontmatter mapping with version, description, tags, and ordered arguments.
3. Give every argument a description and explicit required flag. 3. Give every argument a description and explicit required flag.
4. Add `choices` only when a string argument accepts a fixed set of values. 4. Add `choices` only when a string argument accepts a fixed set of values.
@@ -112,7 +112,6 @@ When deeper guidance is needed, read the selected skill's `_manifest` and fetch
## Validation Checklist ## Validation Checklist
```bash ```bash
uv run pytest tests/skills/test_provider.py tests/web/test_mcp_skills.py -q
uv run zensical build uv run zensical build
uv run ruff check . uv run ruff check .
uv run ty check uv run ty check
@@ -69,4 +69,4 @@ Prompt validation is provider- and renderer-oriented. Every list or get request
1. Skills remain directly portable to tools that understand standard Agent Skills directories. 1. Skills remain directly portable to tools that understand standard Agent Skills directories.
2. Native skill discovery has no parallel catalog metadata source. 2. Native skill discovery has no parallel catalog metadata source.
3. Prompts use FastMCP's native component metadata and protocol surface without a parallel catalog or Python component file. 3. Prompts use FastMCP's native component metadata and protocol surface without a parallel catalog or Python component file.
4. All authored content remains under `docs/`. 4. All authored content remains under `src/personal_mcp/docs/`.
+8 -8
View File
@@ -21,13 +21,13 @@ This page defines the authored content contract for the docs-first MCP architect
## Canonical Source Of Truth ## Canonical Source Of Truth
1. All authored Markdown lives under `docs/`. 1. All authored Markdown lives under `src/personal_mcp/docs/`.
2. MCP resources and static docs are two distribution surfaces of the same authored files. 2. MCP resources and static docs are two distribution surfaces of the same authored files.
3. No parallel authored markdown is allowed in `src/` or other package-only paths. 3. No parallel authored Markdown is allowed in a root `docs/` directory or another source tree.
## Canonical Content Shape ## Canonical Content Shape
Authored content is organized under `docs/`: Authored content is organized under `src/personal_mcp/docs/`:
```mermaid ```mermaid
--- ---
@@ -41,7 +41,7 @@ config:
lineColor: '#FFFFFF' lineColor: '#FFFFFF'
--- ---
treeView-beta treeView-beta
"docs/" "src/personal_mcp/docs/"
"*.md (top-level docs pages)" "*.md (top-level docs pages)"
"contracts/" "contracts/"
"prompt.md" "prompt.md"
@@ -59,9 +59,9 @@ treeView-beta
## File Placement And Ownership Boundaries ## File Placement And Ownership Boundaries
1. Top-level project docs stay in `docs/*.md`. 1. Top-level project docs stay in `src/personal_mcp/docs/*.md`.
2. Skill docs stay in `docs/skills/<skill-id>/...`. 2. Skill docs stay in `src/personal_mcp/docs/skills/<skill-id>/...`.
3. Prompt docs stay in `docs/prompts/<prompt-id>/...`. 3. Prompt docs stay in `src/personal_mcp/docs/prompts/<prompt-id>/...`.
4. A skill or prompt may link across sections, but must not store content in another artifact's directory. 4. A skill or prompt may link across sections, but must not store content in another artifact's directory.
5. Server and runtime code may index and serve docs, but must not be the source of authored markdown. 5. Server and runtime code may index and serve docs, but must not be the source of authored markdown.
@@ -74,7 +74,7 @@ treeView-beta
This contract guarantees: This contract guarantees:
1. One authored source tree in `docs/` for both website and MCP. 1. One authored source tree in `src/personal_mcp/docs/` for both website and MCP.
2. Skill and prompt artifacts remain path-stable within their own sections. 2. Skill and prompt artifacts remain path-stable within their own sections.
3. Cross-surface publishing remains deterministic because authored content paths are canonical. 3. Cross-surface publishing remains deterministic because authored content paths are canonical.
+2 -2
View File
@@ -22,7 +22,7 @@ config:
lineColor: '#FFFFFF' lineColor: '#FFFFFF'
--- ---
treeView-beta treeView-beta
"docs/prompts/" "src/personal_mcp/docs/prompts/"
"<prompt-id>/" "<prompt-id>/"
"PROMPT.md" "PROMPT.md"
"src/personal_mcp/prompts/" "src/personal_mcp/prompts/"
@@ -45,7 +45,7 @@ Rules:
1. Each `PROMPT.md` owns both its runtime metadata and prose. 1. Each `PROMPT.md` owns both its runtime metadata and prose.
2. Python owns only generic parsing, validation, rendering, and provider behavior. 2. Python owns only generic parsing, validation, rendering, and provider behavior.
3. There is no central prompt catalog, generated signature, or metadata sidecar. 3. There is no central prompt catalog, generated signature, or metadata sidecar.
4. The provider scans direct children of packaged `docs/prompts/` on each list or get request. 4. The provider scans direct children of packaged `personal_mcp/docs/prompts/` on each list or get request.
5. Additions, edits, and deletions become visible on the next request without restarting the server. 5. Additions, edits, and deletions become visible on the next request without restarting the server.
6. Reload is pull-based; the provider does not watch files or emit proactive change notifications. 6. Reload is pull-based; the provider does not watch files or emit proactive change notifications.
@@ -8,7 +8,7 @@ This page defines the canonical contract for skills in the docs-first MCP archit
## Canonical Skill Shape ## Canonical Skill Shape
Each skill is one directory under `docs/skills/`: Each skill is one directory under `src/personal_mcp/docs/skills/`:
```mermaid ```mermaid
--- ---
@@ -22,7 +22,7 @@ config:
lineColor: '#FFFFFF' lineColor: '#FFFFFF'
--- ---
treeView-beta treeView-beta
"docs/" "src/personal_mcp/docs/"
"... (other docs)" "... (other docs)"
"skills/" "skills/"
"<skill-id>/" "<skill-id>/"
@@ -70,7 +70,7 @@ Invalid examples:
## Provider Publication ## Provider Publication
[`SkillsDirectoryProvider`](https://gofastmcp.com/servers/providers/skills) scans `docs/skills/` with `supporting_files="template"` and publishes: [`SkillsDirectoryProvider`](https://gofastmcp.com/servers/providers/skills) scans packaged `personal_mcp/docs/skills/` with `supporting_files="template"` and publishes:
1. `skill://<skill-id>/SKILL.md` 1. `skill://<skill-id>/SKILL.md`
2. `skill://<skill-id>/_manifest` 2. `skill://<skill-id>/_manifest`
+1 -1
View File
@@ -46,7 +46,7 @@ FastMCP confines reads to the selected skill directory. Absolute paths, traversa
## General Docs URI ## General Docs URI
General authored documentation is exposed through `resource://docs/{path*}`. The wildcard accepts normalized relative POSIX Markdown paths beneath `docs/`, excludes the provider-owned `skills/` subtree, and rejects absolute paths, traversal segments, backslashes, and non-Markdown targets. General authored documentation is exposed through `resource://docs/{path*}`. The wildcard accepts normalized relative POSIX Markdown paths beneath packaged `personal_mcp/docs/`, excludes the provider-owned `skills/` subtree, and rejects absolute paths, traversal segments, backslashes, and non-Markdown targets.
Prompts are MCP prompt components rather than resources. Clients discover them with the protocol `prompts/list` operation and render them with `prompts/get`. Prompts are MCP prompt components rather than resources. Clients discover them with the protocol `prompts/list` operation and render them with `prompts/get`.
+12 -14
View File
@@ -6,7 +6,7 @@ icon: lucide/server
## Purpose ## Purpose
The project serves native MCP content and a pre-built documentation site from one FastAPI process. Markdown is authored once under `docs/`; runtime providers and Zensical consume that same packaged tree for different purposes. The project serves native MCP content and a pre-built documentation site from one FastAPI process. Markdown is authored once under `src/personal_mcp/docs/`; runtime providers and Zensical consume that same package-owned tree for different purposes.
## Repository Layout ## Repository Layout
@@ -19,6 +19,7 @@ config:
--- ---
treeView-beta treeView-beta
"project-root" "project-root"
"src/personal_mcp"
"docs" "docs"
"prompts/<prompt-id>/PROMPT.md" "prompts/<prompt-id>/PROMPT.md"
"skills/<skill-id>/SKILL.md" "skills/<skill-id>/SKILL.md"
@@ -26,22 +27,19 @@ treeView-beta
"<general-pages>.md" "<general-pages>.md"
"site" "site"
"static build output" "static build output"
"src/personal_mcp" "app.py"
"mcp.py" "mcp.py"
"prompts/content.py" "skills.py"
"prompts/models.py" "prompts/"
"prompts/provider.py"
"registry/" "registry/"
"skills/provider.py"
"web/"
``` ```
Ownership rules: Ownership rules:
1. `docs/skills/` is owned exclusively by `SkillsDirectoryProvider` at runtime. 1. `src/personal_mcp/docs/skills/` is owned exclusively by `SkillsDirectoryProvider` at runtime.
2. Each file under `docs/prompts/` owns its prompt metadata, argument schema, and prose. 2. Each file under `src/personal_mcp/docs/prompts/` owns its prompt metadata, argument schema, and prose.
3. The docs registry owns only general Markdown resources and explicitly excludes skills. 3. The docs registry owns only general Markdown resources and explicitly excludes skills.
4. `site/` is generated output. 4. `src/personal_mcp/site/` is generated output.
5. The deleted custom `catalog/` package is not part of the runtime. 5. The deleted custom `catalog/` package is not part of the runtime.
## Runtime Composition ## Runtime Composition
@@ -70,8 +68,8 @@ Runtime guarantees:
## Build And Publish Flow ## Build And Publish Flow
1. Author prompt definitions and prose under `docs/prompts/`. 1. Author prompt definitions and prose under `src/personal_mcp/docs/prompts/`.
2. Run `uv run zensical build` to produce `site/`. 2. Run `uv run zensical build` to produce `src/personal_mcp/site/`.
3. Build the wheel, which packages the authored docs under `personal_mcp/docs/`. 3. Build the wheel, which packages the authored docs under `personal_mcp/docs/`.
4. Start the app and serve MCP plus the static site. 4. Start the app and serve MCP plus the static site.
@@ -79,10 +77,10 @@ No runtime Markdown-to-HTML conversion occurs.
## Machine-Facing Mapping ## Machine-Facing Mapping
1. `docs/skills/<skill-id>/SKILL.md` maps to `skill://<skill-id>/SKILL.md`. 1. `src/personal_mcp/docs/skills/<skill-id>/SKILL.md` maps to `skill://<skill-id>/SKILL.md`.
2. Skill supporting files map to `skill://<skill-id>/<path>`. 2. Skill supporting files map to `skill://<skill-id>/<path>`.
3. Declarative prompt documents map to native MCP prompt names. 3. Declarative prompt documents map to native MCP prompt names.
4. General `docs/<path>.md` maps to `resource://docs/{path*}`. 4. General `src/personal_mcp/docs/<path>.md` maps to `resource://docs/{path*}`.
The server publishes no tool projections of resources or prompts. The server publishes no tool projections of resources or prompts.
@@ -66,7 +66,8 @@ Load [component mechanics](./references/component-mechanics.md) for:
- the NiceGUI Python wrapper, element bridge, Quasar component, and Vue runtime boundaries - the NiceGUI Python wrapper, element bridge, Quasar component, and Vue runtime boundaries
- deciding between constructors, bindings, Quasar props, events, slots, and frontend methods - deciding between constructors, bindings, Quasar props, events, slots, and frontend methods
- server-client state and event flow - controlled values, model events, transformed payloads, and server-authoritative edit proposals
- server-client state and event flow, validation timing, and commit policy
- detached content and external icon assets - detached content and external icon assets
- source research against the installed NiceGUI and bundled Quasar versions - source research against the installed NiceGUI and bundled Quasar versions
- `ui.select` and `ui.icon` mechanics and caveats - `ui.select` and `ui.icon` mechanics and caveats
@@ -80,7 +81,7 @@ Load [editable tables](./references/tables.md) for:
- rendering dataframe records into row-scoped bindable dataclasses - rendering dataframe records into row-scoped bindable dataclasses
- stable row identity across sorting, filtering, and pagination - stable row identity across sorting, filtering, and pagination
- NiceGUI editors in Quasar `body-cell-*` scoped slots - NiceGUI editors in Quasar `body-cell-*` scoped slots
- validation, persistence, rejection, and canonical row refresh - QTable row refresh and selection preservation after edits
- the full `body` slot required when escalating to `QPopupEdit` - the full `body` slot required when escalating to `QPopupEdit`
### Bindable State ### Bindable State
@@ -88,7 +89,7 @@ Load [editable tables](./references/tables.md) for:
Load [bindable dataclasses](./references/binding-dataclasses.md) for: Load [bindable dataclasses](./references/binding-dataclasses.md) for:
- typed local UI state - typed local UI state
- propagation and refresh behavior - propagation, serializable projections, persistence, and rollback behavior
- nested structures and strict bindings - nested structures and strict bindings
- mutable defaults, performance, and version notes - mutable defaults, performance, and version notes
@@ -0,0 +1,30 @@
from dataclasses import field
from datetime import date
from nicegui import binding
from nicegui import ui
@binding.bindable_dataclass
class ReportFilters:
start_on: date = field(default_factory=date.today)
page_size: int = 25
filters = ReportFilters()
ui.date().bind_value(
filters,
"start_on",
forward=date.fromisoformat, # control str -> model date
backward=date.isoformat, # model date -> control str
)
ui.label().bind_text_from(
filters,
"start_on",
backward=lambda value: f"Starting {value:%d %B %Y}",
)
if __name__ in {"__main__", "__mp_main__"}:
ui.run(port=8888, reload=True)
@@ -7,6 +7,7 @@
# /// # ///
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field
import pandas as pd import pandas as pd
from nicegui import binding from nicegui import binding
@@ -15,25 +16,49 @@ from nicegui import ui
STATUS_OPTIONS = ["draft", "active", "archived"] STATUS_OPTIONS = ["draft", "active", "archived"]
EDITABLE_FIELDS = ("name", "quantity", "status") EDITABLE_FIELDS = ("name", "quantity", "status")
TableValue = str | int
TableRow = dict[str, TableValue] type TableValue = str | int
type TableRow = dict[str, TableValue]
@binding.bindable_dataclass(bindable_fields=EDITABLE_FIELDS) @binding.bindable_dataclass
class EditableRow: class EditableRow:
id: int id: int
name: str name: str
quantity: int quantity: int
status: str status: str
table_row: TableRow = field(init=False, repr=False)
touched: bool = False
def __post_init__(self) -> None:
self.table_row = {
"id": self.id,
"name": self.name,
"quantity": self.quantity,
"status": self.status,
}
for field_name in EDITABLE_FIELDS:
binding.bind_to(
self,
field_name,
self.table_row,
field_name,
other_strict=True,
)
@dataclass(slots=True) @dataclass(slots=True)
class EditableTableState: class EditableTableState:
rows_by_id: dict[int, EditableRow] rows_by_id: dict[int, EditableRow]
table_rows_by_id: dict[int, TableRow]
def row(self, row_id: int) -> EditableRow | None:
return self.rows_by_id.get(row_id)
def table_rows(self) -> list[TableRow]: def table_rows(self) -> list[TableRow]:
return list(self.table_rows_by_id.values()) return [row.table_row for row in self.rows_by_id.values()]
def touched_rows(self) -> list[EditableRow]:
return [row for row in self.rows_by_id.values() if row.touched]
def dataframe_to_state(dataframe: pd.DataFrame) -> EditableTableState: def dataframe_to_state(dataframe: pd.DataFrame) -> EditableTableState:
@@ -45,37 +70,20 @@ def dataframe_to_state(dataframe: pd.DataFrame) -> EditableTableState:
raise ValueError("The id column must contain unique row keys") raise ValueError("The id column must contain unique row keys")
rows_by_id: dict[int, EditableRow] = {} rows_by_id: dict[int, EditableRow] = {}
table_rows_by_id: dict[int, TableRow] = {}
for record in dataframe.to_dict(orient="records"): for record in dataframe.to_dict(orient="records"):
row_state = EditableRow( row = EditableRow(
id=int(record["id"]), id=int(record["id"]),
name=str(record["name"]), name=str(record["name"]),
quantity=int(record["quantity"]), quantity=int(record["quantity"]),
status=str(record["status"]), status=str(record["status"]),
) )
if row_state.status not in STATUS_OPTIONS: if row.status not in STATUS_OPTIONS:
raise ValueError(f"Unknown status {row_state.status!r}") raise ValueError(f"Unknown status {row.status!r}")
if row_state.id in rows_by_id: if row.id in rows_by_id:
raise ValueError("Row keys must remain unique after normalization") raise ValueError("Row keys must remain unique after normalization")
rows_by_id[row.id] = row
table_row: TableRow = { return EditableTableState(rows_by_id)
"id": row_state.id,
"name": row_state.name,
"quantity": row_state.quantity,
"status": row_state.status,
}
for field_name in EDITABLE_FIELDS:
binding.bind_to(
row_state,
field_name,
table_row,
field_name,
other_strict=True,
)
rows_by_id[row_state.id] = row_state
table_rows_by_id[row_state.id] = table_row
return EditableTableState(rows_by_id, table_rows_by_id)
def normalize_edit(field: str, raw_value: object) -> TableValue: def normalize_edit(field: str, raw_value: object) -> TableValue:
@@ -104,15 +112,6 @@ def normalize_edit(field: str, raw_value: object) -> TableValue:
raise ValueError(f"Field {field!r} is not editable") raise ValueError(f"Field {field!r} is not editable")
def save_row(dataframe: pd.DataFrame, row_state: EditableRow) -> None:
matching_rows = dataframe["id"].eq(row_state.id)
if int(matching_rows.sum()) != 1:
raise ValueError("This row no longer exists")
dataframe.loc[matching_rows, "name"] = row_state.name
dataframe.loc[matching_rows, "quantity"] = row_state.quantity
dataframe.loc[matching_rows, "status"] = row_state.status
def render_table(dataframe: pd.DataFrame) -> EditableTableState: def render_table(dataframe: pd.DataFrame) -> EditableTableState:
state = dataframe_to_state(dataframe) state = dataframe_to_state(dataframe)
columns = [ columns = [
@@ -125,33 +124,40 @@ def render_table(dataframe: pd.DataFrame) -> EditableTableState:
rows=state.table_rows(), rows=state.table_rows(),
row_key="id", row_key="id",
selection="multiple", selection="multiple",
).classes("w-full") pagination=10,
).classes("w-120")
def apply_edit(event: events.GenericEventArguments) -> None: def apply_edit(event: events.GenericEventArguments) -> None:
try: try:
raw_row_id, raw_field, raw_value = event.args raw_row_id, raw_field, raw_value = event.args
row_id = int(raw_row_id) row_id = int(raw_row_id)
field_name = str(raw_field) field_name = str(raw_field)
row_state = state.rows_by_id.get(row_id) row_state = state.row(row_id)
if row_state is None: if row_state is None:
raise ValueError("This row no longer exists") raise ValueError("This row no longer exists")
normalized_value = normalize_edit(field_name, raw_value) normalized_value = normalize_edit(field_name, raw_value)
previous_value = getattr(row_state, field_name)
setattr(row_state, field_name, normalized_value) setattr(row_state, field_name, normalized_value)
try: row_state.touched = True
save_row(dataframe, row_state)
except Exception:
setattr(row_state, field_name, previous_value)
raise
except (TypeError, ValueError) as error: except (TypeError, ValueError) as error:
ui.notify(str(error), type="negative") ui.notify(str(error), type="negative")
finally: finally:
table.update_rows(state.table_rows(), clear_selection=False) table.update_rows(state.table_rows(), clear_selection=False)
def show_changes() -> None:
changed_rows = state.touched_rows()
if not changed_rows:
ui.notify("No rows changed")
return
summary = "; ".join(
f"{row.id}: {row.name}, quantity {row.quantity}, status {row.status}" for row in changed_rows
)
ui.notify(f"Changed rows: {summary}")
with table.add_slot("body-cell-name"), table.cell("name"): with table.add_slot("body-cell-name"), table.cell("name"):
ui.input().props(':model-value="props.value" dense borderless debounce=400').on( name_input = ui.input().props(remove="value")
"update:model-value", name_input.props(':value="props.value" dense borderless debounce=400').on(
"update:value",
handler=apply_edit, handler=apply_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)", js_handler="(value) => emit(props.row.id, props.col.name, value)",
) )
@@ -167,9 +173,12 @@ def render_table(dataframe: pd.DataFrame) -> EditableTableState:
ui.select(STATUS_OPTIONS).props(':model-value="props.value" dense borderless options-dense').on( ui.select(STATUS_OPTIONS).props(':model-value="props.value" dense borderless options-dense').on(
"update:model-value", "update:model-value",
handler=apply_edit, handler=apply_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)", js_handler="(option) => emit(props.row.id, props.col.name, option.label)",
) )
with ui.row().classes("w-120 justify-end"):
ui.button("Show changes", icon="edit_note", on_click=show_changes)
return state return state
@@ -178,6 +187,14 @@ if __name__ in {"__main__", "__mp_main__"}:
[ [
{"id": 101, "name": "Desk", "quantity": 4, "status": "active"}, {"id": 101, "name": "Desk", "quantity": 4, "status": "active"},
{"id": 102, "name": "Lamp", "quantity": 12, "status": "draft"}, {"id": 102, "name": "Lamp", "quantity": 12, "status": "draft"},
{"id": 103, "name": "Chair", "quantity": 8, "status": "active"},
{"id": 104, "name": "Shelf", "quantity": 3, "status": "draft"},
{"id": 105, "name": "Monitor", "quantity": 15, "status": "active"},
{"id": 106, "name": "Keyboard", "quantity": 20, "status": "active"},
{"id": 107, "name": "Mouse", "quantity": 24, "status": "active"},
{"id": 108, "name": "Dock", "quantity": 6, "status": "archived"},
{"id": 109, "name": "Cable", "quantity": 40, "status": "draft"},
{"id": 110, "name": "Stand", "quantity": 10, "status": "active"},
] ]
) )
table_state = render_table(items) table_state = render_table(items)
@@ -0,0 +1,64 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "nicegui==3.16.0",
# ]
# ///
from datetime import UTC
from datetime import datetime
from nicegui import events
from nicegui import ui
OPTIONS = {
"python": "Python",
"typescript": "TypeScript",
"rust": "Rust",
}
with ui.card().classes("w-140 max-w-full"):
ui.label("Select event mechanics").classes("text-xl font-semibold")
event_log = ui.log(max_lines=12).classes("w-full h-64")
def record(event_name: str, payload: object) -> None:
timestamp = datetime.now(UTC).astimezone().strftime("%H:%M:%S")
event_log.push(f"{timestamp} {event_name}: {payload!r}")
def handle_change(event: events.ValueChangeEventArguments) -> None:
record("on_change event.value", event.value)
def handle_model_update(event: events.GenericEventArguments) -> None:
record("js_handler -> handler event.args", event.args)
language = (
ui.select(
options=OPTIONS,
value="python",
label="Language",
on_change=handle_change,
with_input=True,
clearable=True,
)
.props("outlined options-dense")
.classes("w-full text-h6")
)
language.on("popup-show", lambda: record("popup-show", None), args=[])
language.on("popup-hide", lambda: record("popup-hide", None), args=[])
# This only fires when the value is changed from the browser side (not from the button)
language.on(
"update:model-value",
handler=handle_model_update,
js_handler="(...args) => emit(...args)",
)
with ui.row().classes("w-full justify-end"):
ui.button("Set Rust", on_click=lambda: language.set_value("rust"))
ui.button("Clear log", on_click=event_log.clear).props("flat")
if __name__ in {"__main__", "__mp_main__"}:
ui.run(port=8888, reload=True)
@@ -1,16 +1,20 @@
# Binding Dataclasses Deep Dive # Binding Dataclasses
Use this reference to model NiceGUI state with bindable dataclasses and avoid common propagation and performance pitfalls. Use this reference to understand how NiceGUI creates binding links, detects changes, propagates values, and applies `forward` and `backward` transforms.
The implementation details and signatures below are verified against NiceGUI `3.16.0`. Check the target project's pinned version before copying version-sensitive behavior.
## Primary Sources ## Primary Sources
- NiceGUI binding docs: [binding properties](https://www.nicegui.io/documentation/section_binding_properties) - [NiceGUI binding documentation](https://www.nicegui.io/documentation/section_binding_properties): public binding behavior and examples
- Python dataclass docs: [dataclasses module](https://docs.python.org/3/library/dataclasses.html) - [NiceGUI `binding.py` at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/binding.py): binding graph, propagation, active links, strict checks, and `bindable_dataclass`
- Data class design rationale: [PEP 557](https://peps.python.org/pep-0557/) - [NiceGUI `ValueElement` at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/value_element.py): `bind_value*` signatures and transform direction
- [Python dataclasses](https://docs.python.org/3/library/dataclasses.html): generated methods, fields, defaults, and mutable-value rules
- [PEP 557](https://peps.python.org/pep-0557/): dataclass design rationale
## Bindable Dataclass Behavior ## What `bindable_dataclass` Changes
`@binding.bindable_dataclass` extends standard dataclasses by turning fields into bindable properties, allowing UI bindings to propagate when a field is assigned. `@binding.bindable_dataclass` first applies Python's `@dataclass`, then replaces each selected field on the resulting class with a NiceGUI `BindableProperty` descriptor. The descriptor stores the field value privately and intercepts later assignment.
```python ```python
from nicegui import binding, ui from nicegui import binding, ui
@@ -26,24 +30,187 @@ profile = Profile()
ui.input("Name").bind_value(profile, "name") ui.input("Name").bind_value(profile, "name")
ui.number("Age", min=0).bind_value(profile, "age") ui.number("Age", min=0).bind_value(profile, "age")
ui.label().bind_text_from(profile, "name", backward=lambda name: f"User: {name}") ui.label().bind_text_from(
profile,
"name",
backward=lambda name: f"User: {name}",
)
``` ```
## Propagation And Performance Assigning a different value to `profile.name` invokes the descriptor immediately. It records the new value, propagates it through the binding graph, and then runs any descriptor change handler. Assigning an equal value returns without propagation.
NiceGUI distinguishes between two link types: By default every dataclass field is bindable. Pass `bindable_fields` to limit descriptor conversion:
- Bindable properties propagate efficiently when values are assigned. ```python
- Active links are checked in a refresh loop. @binding.bindable_dataclass(bindable_fields={"query", "page_size"})
class SearchState:
query: str = ""
page_size: int = 25
request_count: int = 0
```
Prefer bindable dataclasses for frequently updated form state. Keep binding transforms pure and inexpensive. If an application has many active links, tune `binding_refresh_interval` in `ui.run(...)` only after measuring the impact. A bound field omitted from `bindable_fields` still works, but NiceGUI must treat it as an active link and poll it for changes.
## Binding Graph And Propagation
NiceGUI stores bindings as directed edges from one object attribute to another. A two-way binding is two one-way edges with transforms in opposite directions.
When an edge is registered, NiceGUI propagates its source immediately. For a two-way binding, it registers and runs the `backward` edge first, then registers the `forward` edge. The model value therefore wins initial synchronization and seeds the control.
After registration, propagation follows these rules:
1. A `BindableProperty` assignment starts propagation immediately when `old_value != new_value`.
2. NiceGUI walks outgoing edges depth first.
3. Each object-and-attribute node is visited at most once during that propagation pass, preventing a two-way cycle from running forever.
4. Each edge transforms the source value, compares it with the target, and only assigns and continues when the values differ.
Since NiceGUI `2.16.0`, this depth-first walk updates each affected node once per pass. Transform functions must not depend on call count or traversal order.
## Authoritative Models And Projections
A bindable dataclass can own canonical page state while plain dictionaries or component properties act as serializable projections. Use a one-way binding from each model field to its projection when browser rendering requires a different container shape:
```python
from nicegui import binding
projection = {"name": profile.name}
binding.bind_to(
profile,
"name",
projection,
"name",
other_strict=True,
)
```
Assigning `profile.name` then propagates immediately to `projection["name"]`. The projection is transport state, not a second business model; application code should locate and mutate the owning dataclass rather than treating browser-visible dictionaries as authoritative. This distinction is especially useful when one client-side scoped template renders many records and therefore cannot bind to one fixed Python object. The [editable-table pattern](./tables.md) applies it to one row dataclass and one QTable payload per stable row identity.
Browser-originated values still require Python validation before model assignment. Keep editable fields explicit, normalize into domain types, verify permissions and record existence, and only then assign the bindable field. For the client event path that carries such proposals, see [server-authoritative edit proposals](./component-mechanics.md#server-authoritative-edit-proposals).
### Persistence And Rollback
Treat a dataframe, service, or repository as the persistence boundary around the canonical bindable model:
1. validate and normalize the proposed value
2. remember the previous model value
3. assign the normalized value so bound projections update
4. persist the model through the owning adapter, service, or repository
5. if persistence fails, restore the previous model value before reporting or re-raising the error
6. refresh the affected component from the resulting projection on both acceptance and rejection
For asynchronous persistence, await the transaction and refresh only after it commits or rolls back. Catch expected validation, conflict, and persistence exceptions separately so the interface can report actionable failures without hiding programming errors. Component-specific refresh APIs and identity rules remain the responsibility of the consuming pattern; for QTable, see [persistence and row refresh](./tables.md#persistence-and-row-refresh).
## Bindable Properties Versus Active Links
| Source | Change detection | Update timing |
| --- | --- | --- |
| NiceGUI element property or `BindableProperty` field | descriptor intercepts assignment | immediate |
| ordinary object attribute or mapping entry | refresh loop compares source and target | next refresh step |
| tuple path such as `("address", "city")` | the full path is not a single bindable descriptor key | refresh loop unless the owning leaf object is bound directly |
The active-link refresh interval defaults to `0.1` seconds and is configured with `binding_refresh_interval` in `ui.run(...)`. Every refresh applies the transform and compares the result, so polling large collections or running expensive transforms can block the event loop. Tune the interval only after measuring; first reduce active links and transform cost.
## Transform Direction
The names `forward` and `backward` are relative to the element on which `bind_value*` is called:
| API | Source to target | Transform |
| --- | --- | --- |
| `element.bind_value_to(model, "field")` | element to model | `forward` |
| `element.bind_value_from(model, "field")` | model to element | `backward` |
| `element.bind_value(model, "field")` | both directions | both; `backward` runs first initially |
Each transform adapts the source value before NiceGUI assigns it to the target. The examples below convert between control values and native Python types only to make the two directions easy to observe; they do not prescribe a state-modeling approach.
Keep both functions pure, fast, and valid for every value the source can emit. NiceGUI does not turn transform exceptions into validation messages.
## Example: Observe Both Directions
This example uses [`datetime.date`](https://docs.python.org/3/library/datetime.html#date-objects) and `int` conversions to expose the mechanics. Their different representations make it clear which transform runs as a value crosses each binding edge.
```python
from dataclasses import field
from datetime import date
from nicegui import binding, ui
@binding.bindable_dataclass
class ReportFilters:
start_on: date = field(default_factory=date.today)
page_size: int = 25
filters = ReportFilters()
ui.date().bind_value(
filters,
"start_on",
forward=date.fromisoformat, # control str -> model date
backward=date.isoformat, # model date -> control str
)
ui.select(
options={"10": "10 rows", "25": "25 rows", "50": "50 rows"},
label="Page size",
).bind_value(
filters,
"page_size",
forward=int, # control str -> model int
backward=str, # model int -> control str
)
ui.label().bind_text_from(
filters,
"start_on",
backward=lambda value: f"Starting {value:%d %B %Y}",
)
```
At binding time, NiceGUI runs `backward` from the model to each control. Later control changes run `forward` toward the model. Assigning a new model value runs `backward` again.
## Example: Follow A Constrained Value
A select and an [`Enum`](https://docs.python.org/3/library/enum.html) provide a second visible representation change. Because the select only emits known values, this example keeps attention on propagation rather than parse failures.
```python
from enum import Enum
from nicegui import binding, ui
class SortOrder(Enum):
NEWEST = "newest"
OLDEST = "oldest"
@binding.bindable_dataclass
class ResultsState:
sort_order: SortOrder = SortOrder.NEWEST
state = ResultsState()
ui.select(
options={"newest": "Newest first", "oldest": "Oldest first"},
label="Sort order",
).bind_value(
state,
"sort_order",
forward=SortOrder, # control str -> model SortOrder
backward=lambda value: value.value, # model SortOrder -> control str
)
```
The concrete types are incidental. The same graph mechanics apply whenever `forward` and `backward` map two representations.
## Dataclass Modeling Rules ## Dataclass Modeling Rules
- Use `field(default_factory=...)` for mutable defaults. - Use `field(default_factory=...)` for mutable defaults and time-dependent defaults.
- Avoid `frozen=True` for models edited by UI controls. - NiceGUI `3.16.0` rejects `frozen=True` and `slots=True` in `bindable_dataclass`; both conflict with its descriptor storage model.
- Use `slots=True` only after confirming compatibility with inheritance and extension needs. - Keep UI-editable fields explicit and typed. Dataclass annotations describe intent but do not enforce runtime types; the control or transform must produce the right type.
- Keep UI-editable fields explicit and typed. - Replace collections instead of mutating them in place.
```python ```python
from dataclasses import field from dataclasses import field
@@ -55,28 +222,36 @@ from nicegui import binding
class Filters: class Filters:
query: str = "" query: str = ""
tags: list[str] = field(default_factory=list) tags: list[str] = field(default_factory=list)
filters = Filters()
filters.tags = [*filters.tags, "python"] # unequal assignment propagates
``` ```
Calling `filters.tags.append("python")` bypasses the descriptor. Mutating first and then assigning an equal copy also does not propagate because `BindableProperty` compares with `!=` and returns when values are equal.
## Nested Structures ## Nested Structures
NiceGUI supports tuple paths for nested data structures. Tuple paths support nested mappings and object attributes:
```python ```python
from nicegui import ui
data = {"user": {"name": "Ada"}} data = {"user": {"name": "Ada"}}
ui.input("Name").bind_value(data, ("user", "name")) ui.input("Name").bind_value(data, ("user", "name"))
ui.label().bind_text_from(data, ("user", "name")) ui.label().bind_text_from(data, ("user", "name"))
``` ```
Keep nested dataclass updates explicit and predictable at the field level. A tuple path is checked as an active link. When a nested object is itself a bindable dataclass, bind its owning object directly to preserve immediate descriptor-driven propagation:
## Strictness And Refactor Safety ```python
ui.input("City").bind_value(profile.address, "city")
```
- Object attributes are checked by default. If `profile.address` is replaced later, rebuild that direct binding or bind through the root tuple path and accept active-link polling.
- Dictionary keys are not checked by default.
- Use `strict=True` when missing dictionary keys should produce warnings. ## Strictness And Missing Paths
NiceGUI `3.16.0` checks object attributes by default and does not check mapping keys by default. A failed strict check raises `AttributeError` or `KeyError` while the binding is being created.
```python ```python
from nicegui import app, ui from nicegui import app, ui
@@ -84,17 +259,23 @@ from nicegui import app, ui
ui.input().bind_value(app.storage.user, "display_name", strict=True) ui.input().bind_value(app.storage.user, "display_name", strict=True)
``` ```
Use `strict=False` for an intentionally lazy object attribute and `strict=True` when a mapping key must already exist. On assignment, NiceGUI can create missing intermediate dictionaries, but it cannot create missing intermediate object attributes.
## Common Pitfalls ## Common Pitfalls
- In-place mutation may not produce immediate UI synchronization. Assign the updated value back to the bound field. - Do not put logging, I/O, model mutation, notifications, or other side effects in transforms. Propagation order and call count are implementation details.
- Heavy binding transforms can degrade refresh performance. Move expensive work to event handlers or services. - Do not use a transform as the validation boundary for free-form text. A raised parser exception interrupts propagation.
- State shared across unrelated pages or users can leak data. Scope models to the appropriate page, client, or user context. - Do not mutate a bound collection in place. Construct and assign a different value.
- Do not assume a nested tuple path gets the same immediate behavior as binding directly to a bindable leaf object.
- Scope bindable models to the appropriate page, client, or user. A module-global model shares state across users.
- Remove bindings with NiceGUI's public element lifecycle rather than retaining discarded elements or models indefinitely.
## Version Checks ## Version Checks
- `bindable_dataclass` was added in NiceGUI 2.11.0. - `bindable_dataclass` was added in NiceGUI `2.11.0`.
- Depth-first binding propagation was documented in NiceGUI 2.16.0. - Depth-first binding propagation changed in NiceGUI `2.16.0`.
- Binding `strict` behavior was documented in NiceGUI 3.0.0. - Binding strictness controls were added in NiceGUI `3.0.0`.
- Tuple paths for nested properties were documented in NiceGUI 3.10.0. - Tuple paths for nested properties were added in NiceGUI `3.10.0`.
- NiceGUI `3.16.0` supports `bindable_fields` and rejects `slots=True` and `frozen=True`.
Verify these behaviors against the NiceGUI version pinned by the target project. Verify the installed NiceGUI source and documentation when any of these mechanics affect application correctness.
@@ -1,6 +1,239 @@
# NiceGUI Component Mechanics # NiceGUI Component Mechanics
Use this reference to understand how customization crosses the NiceGUI Python wrapper, Quasar component, Vue runtime, and browser DOM. It owns constructor behavior, prop translation, events, bindings, slots, frontend methods, detached content, and component-specific caveats. For themes, utility classes, CSS properties, responsive page composition, and other cosmetic work, load [visual styling and CSS](./styling-and-customization.md). NiceGUI components are Python objects that describe browser UI elements. A component constructor creates an element, constructor arguments configure its common behavior, and methods on the returned object expose styling, events, bindings, slots, and client-side capabilities.
This reference begins with those everyday component APIs, then describes the NiceGUI, Quasar, Vue, and browser layers beneath them. Themes, responsive composition, and broader visual design are covered separately in [visual styling and CSS](./styling-and-customization.md).
## Basic Components
Components are created from the `ui` namespace. Layout components are context managers, so nested Python blocks describe the element hierarchy:
```python
from nicegui import ui
with ui.column().classes("gap-3"):
name = ui.input("Name", placeholder="Ada")
role = ui.select(
options={"admin": "Administrator", "reader": "Reader"},
value="reader",
label="Role",
).props("outlined dense")
ui.button("Save", on_click=lambda: ui.notify(f"Saved {name.value}"))
```
The [NiceGUI component documentation](https://nicegui.io/documentation) is the index of available `ui.*` constructors. Each component page documents its Python parameters, values, callbacks, methods, and examples. The implementation for each wrapper is available in the [NiceGUI element source tree](https://github.com/zauberzeug/nicegui/tree/main/nicegui/elements).
## Common Component Mechanics
Most NiceGUI elements inherit a common set of mechanics from `Element`; individual wrappers add component-specific properties and methods.
| Surface | What it represents | Source of supported values |
| --- | --- | --- |
| Constructor arguments | NiceGUI's typed, Python-facing API for initial content, values, callbacks, validation, and common behavior | the component's page in the [NiceGUI component documentation](https://nicegui.io/documentation) and its wrapper in the [NiceGUI element source tree](https://github.com/zauberzeug/nicegui/tree/main/nicegui/elements) |
| Properties such as `.value` and `.options` | Python-side component state maintained by a particular wrapper | the component documentation and wrapper source; these properties are not universal `Element` APIs |
| Wrapper methods such as `set_options()` | NiceGUI state transitions that normalize Python data and schedule a client update | the component documentation and wrapper source |
| `.props(...)` | Quasar component props, Vue bindings, or HTML attributes serialized onto the frontend element | the API section of the wrapped component in the [Quasar component documentation](https://quasar.dev/vue-components); [NiceGUI element customization](https://nicegui.io/documentation/element) defines the bridge syntax |
| `.classes(...)` | CSS class names attached to the element | [Tailwind's utility documentation](https://tailwindcss.com/docs) for Tailwind classes; Quasar's [breakpoint](https://quasar.dev/style/breakpoints), [spacing](https://quasar.dev/style/spacing), [visibility](https://quasar.dev/style/visibility), and [helper-class](https://quasar.dev/style/other-helper-classes) references for Quasar classes; or the application's own stylesheets for custom classes |
| `.style(...)` | Inline CSS declarations attached to the element | the [MDN CSS reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference) |
| Constructor callbacks and `.on(...)` | NiceGUI callbacks and forwarded browser or Quasar events | the component's NiceGUI page first, then the Events section of its Quasar API; [NiceGUI generic events](https://nicegui.io/documentation/generic_events) documents `.on(...)` |
| `on_*` methods | Named event conveniences implemented by a specific NiceGUI wrapper, such as `on_value_change` | the component documentation and wrapper source; there is no universal list that applies to every component |
| `bind_*` methods | synchronization between element properties and Python model properties | [NiceGUI binding documentation](https://nicegui.io/documentation/section_binding_properties) and the wrapper's documented bindable properties |
| `add_slot(...)` | content inserted into a Quasar or Vue named slot | the Slots and Scoped Slots sections of the wrapped component's Quasar API |
| `run_method(...)` | invocation of a public method on the client component | the Methods section of the wrapped component's Quasar API |
### Options And Values
`options` is component state rather than a universal styling mechanism. Components such as `ui.select`, `ui.radio`, `ui.toggle`, and `ui.table` define their own accepted option shapes and value semantics. For example, NiceGUI's [`ui.select` documentation](https://nicegui.io/documentation/select) describes list and dictionary options, while the [`Select` wrapper source](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/select.py) shows how those Python values are normalized for Quasar.
Reading `element.options` accesses the wrapper's current Python-side options. Assigning or mutating options only changes browser state when the wrapper detects or sends an update. Component helpers such as `set_options()` encode that synchronization behavior and therefore belong to the wrapper's API rather than to Quasar's raw `options` prop.
### Props
`.props()` writes props onto the frontend component:
```python
ui.button("Archive").props("outline color=negative")
ui.select(["A", "B"]).props("dense options-dense")
```
For NiceGUI elements backed by Quasar, supported names and values come from the wrapped Quasar component's API. For example, the full [`QSelect` API](https://quasar.dev/vue-components/select#qselect-api) lists `dense`, `options-dense`, `popup-content-class`, events, slots, and methods. NiceGUI may already expose some of those features as typed constructor arguments or wrapper methods; the NiceGUI component page and source describe that higher-level behavior.
#### Property-String Format
NiceGUI's `.props()` string is parsed on the Python side by the tagged [`Props.parse()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/props.py). It accepts whitespace-delimited tokens in these forms:
| Form | Python-side result | Frontend meaning |
| --- | --- | --- |
| `dense` | `{"dense": True}` | a true boolean prop |
| `label=Chair` | `{"label": "Chair"}` | a static string prop |
| `offset=[8, 8]` | `{"offset": [8, 8]}` | a Python literal serialized as a value |
| `:label=someExpression` | `{":label": "someExpression"}` | a JavaScript expression evaluated in the browser |
Quoted strings and bracketed or braced literals are parsed with Python's `ast.literal_eval`; unquoted values remain strings. Quote an expression when it contains whitespace or characters outside NiceGUI's unquoted-value grammar, or assign it through `element.props[":name"]` to avoid the string parser. Regular HTML attributes can pass through the same mechanism where the rendered element supports them. The [NiceGUI element documentation](https://nicegui.io/documentation/element) defines the public bridge syntax.
#### Dynamic Props And Vue Bindings
The leading colon borrows Vue's [`v-bind` shorthand](https://vuejs.org/api/built-in-directives.html#v-bind), but NiceGUI elements are created with Vue's `h()` render function rather than compiled from a template. NiceGUI's tagged [`renderRecursively()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/static/nicegui.js) removes the colon, evaluates the value as JavaScript, and passes the result in the vnode's props object. For example:
```python
ui.badge().props(':color="window.innerWidth < 600 ? \'primary\' : \'grey\'"')
```
corresponds conceptually to this Vue template:
```vue
<q-badge :color="window.innerWidth < 600 ? 'primary' : 'grey'" />
```
The right-hand side is JavaScript, not Python. It may read browser globals, call functions, or construct arrays and objects, provided the receiving HTML element or Vue component accepts the resulting property. Inside a scoped slot, NiceGUI additionally makes that slot's current scope object available under the name `props`; outside a scoped slot, that name has no slot object to reference.
There is one important render-function distinction. Vue template syntax allows argument-less `v-bind="object"` to spread every key in an object. A literal `.props("v-bind=someObject")` token is not compiled as a directive by NiceGUI's render-function path and does not spread the object. Use a raw `add_slot(..., template=...)` Vue template when a slot contract requires whole-object binding, or bind the documented fields individually. Vue's [render-function reference](https://vuejs.org/guide/extras/render-function.html#creating-vnodes) defines the equivalent programmatic form as passing or spreading those keys in the object supplied to `h()`.
#### Controlled Values And Model Events
Vue component `v-model` expands to a value prop plus an update listener. For the common `modelValue` contract, that means `modelValue` and `update:modelValue`, as defined by 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).
NiceGUI can express a deliberately one-way controlled value with a dynamic prop and handle the corresponding proposal separately. For example, `ui.number` follows QInput's common `modelValue` contract:
```python
number_editor = ui.number()
number_editor.props(':model-value="props.value"').on(
"update:model-value",
handler=apply_proposal,
js_handler="(value) => emit(value)",
)
```
This pattern is useful inside a scoped slot or whenever Python must authorize a change before reasserting component state. The dynamic prop displays the current client-side projection; the listener sends an edit proposal to Python instead of assigning into the source object in JavaScript. Use an ordinary NiceGUI value binding when the wrapper's two-way value model already matches the requirement.
##### `ui.input` Wrapper Exception
In NiceGUI `3.16.0`, `ui.input` is a NiceGUI client wrapper around QInput rather than a direct QInput element. The tagged [`input.js` component](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/input.js) defines its controlled prop and event as `value` and `update:value`, not `model-value` and `update:model-value`. It also adds a static empty `value` prop. Remove that prop before adding a row-scoped dynamic value:
```python
name_input = ui.input().props(remove="value")
name_input.props(':value="props.value"').on(
"update:value",
handler=apply_proposal,
js_handler="(value) => emit(value)",
)
```
Using `:model-value="props.value"` leaves this wrapper's own `value` unchanged, so repeated text inputs in a scoped table slot render blank. `ui.number` is a direct QInput specialization and therefore uses `model-value` and `update:model-value` as shown above. Check each NiceGUI wrapper's `VALUE_PROP` and client component before assuming the underlying Quasar model contract is exposed unchanged.
An `update:model-value` listener receives the component's emitted model value, whose shape is component-specific. A custom listener also bypasses normalization that a wrapper's built-in value handler may perform. For example, the Quasar input beneath `ui.number` can emit numeric text, so the Python proposal handler must perform authoritative numeric conversion.
NiceGUI serializes `ui.select` options into QSelect objects shaped like `{value: index, label: option_label}` and normally maps the selected object back to the corresponding Python option. A custom `js_handler` receives that object before NiceGUI's Python-side conversion. When list values and labels are intentionally identical, forward `option.label`; otherwise emit the index and resolve it against the authoritative Python options rather than trusting a browser-supplied label.
### Classes And Styles
`.classes()` adds class names to the rendered element:
```python
ui.label("Account").classes("text-lg font-semibold text-slate-800")
ui.row().classes("w-full items-center gap-4")
```
NiceGUI includes Tailwind-compatible utility styling, so names such as `flex`, `gap-4`, `w-full`, and `text-slate-800` are defined by Tailwind. The complete categorized list is the [Tailwind CSS documentation](https://tailwindcss.com/docs); its [utility-class guide](https://tailwindcss.com/docs/styling-with-utility-classes) explains variants, responsive prefixes, and arbitrary values. NiceGUI can alternatively run with a selected UnoCSS preset, whose compatibility limits are documented under [NiceGUI's UnoCSS engine](https://nicegui.io/documentation/section_styling_appearance#unocss_engine).
Quasar publishes its classes by category rather than through a single style index. The [breakpoint reference](https://quasar.dev/style/breakpoints) defines viewport thresholds, the [spacing reference](https://quasar.dev/style/spacing) lists the `q-p*` and `q-m*` permutations, the [visibility reference](https://quasar.dev/style/visibility) covers responsive and platform visibility, and the [other helper classes reference](https://quasar.dev/style/other-helper-classes) covers pointer, scrolling, sizing, rotation, and border helpers. Application-defined class names are supported when their CSS is loaded with `ui.add_css`, static assets, or page head content. `.style()` accepts CSS declarations directly, separated by semicolons.
### Events And `on_*` Methods
Callbacks supplied by a constructor are NiceGUI's documented event surface:
```python
ui.input("Search", on_change=lambda event: print(event.value))
ui.button("Refresh", on_click=lambda: print("refresh"))
```
Some wrappers also expose named registration methods such as `on_value_change`. Their availability and event argument type are component-specific and are documented on the NiceGUI component page or in its wrapper source.
`.on()` is the generic event bridge for events without a dedicated Python convenience API:
```python
field = ui.select(["A", "B"])
field.on("popup-show", lambda: print("opened"))
```
For Quasar-backed elements, the component API's Events section is the authoritative list of emitted event names and payloads. Native browser events are documented in the [MDN event reference](https://developer.mozilla.org/en-US/docs/Web/Events). NiceGUI's [generic event documentation](https://nicegui.io/documentation/generic_events) defines the public `.on()` API.
#### Mapping Quasar Event Names
Quasar documents each component event under its **Events** API entry. Use the documented kebab-case name with `.on()`. NiceGUI's tagged [`event_type_to_camel_case()` helper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/helpers/strings.py) converts the event name before the first modifier dot to the camelCase form emitted by the component; its frontend renderer then creates Vue's `onXxx` listener prop. These forms therefore map to the same event path:
| Quasar API name | NiceGUI registration | Vue runtime listener |
| --- | --- | --- |
| `popup-show` | `.on("popup-show", ...)` | `onPopupShow` |
| `input-value` | `.on("input-value", ...)` | `onInputValue` |
| `update:model-value` | `.on("update:model-value", ...)` | `onUpdate:modelValue` |
Vue component events are notifications emitted by the direct component; unlike DOM events, they do not bubble through component ancestors. Prefer a NiceGUI constructor callback, binding, or named wrapper method when one already owns the same behavior. In particular, use `on_change` or a value binding instead of registering another `update:model-value` listener unless the lower-level model event is specifically required.
#### Reading Event Payloads
The Quasar event's documented `params` define the positional arguments received by the listener. NiceGUI serializes those arguments and exposes them as `GenericEventArguments.args` in Python. If exactly one argument is emitted, NiceGUI presents that value directly; multiple emitted arguments remain a list in their documented order.
For example, the version-matched [`QSelect` event API](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.json) defines `add` as one details object containing `index` and `value`:
```python
def handle_add(event) -> None:
print(event.args["index"], event.args["value"])
item_select.on("add", handle_add, args=["index", "value"])
```
The `args` parameter controls transport, not Quasar's event signature:
| `args` value | Data sent to Python |
| --- | --- |
| `None` | all JSON-serializable attributes of every emitted argument |
| `[]` | no event arguments |
| `["index", "value"]` | only those attributes from a one-object event argument |
| `[[], ["name"], None]` | for a three-argument event: none from the first, `name` from the second, and all of the third |
Primitive values and arrays are forwarded as values rather than filtered by attribute name. Browser objects, DOM nodes, component references, functions, and cyclic structures are not meaningful server payloads; select the small serializable subset the Python handler actually needs.
#### Transforming Events In The Browser
`js_handler` receives the original Quasar or browser event arguments in the browser. Calling NiceGUI's injected `emit(...)` forwards only the transformed arguments to the Python `handler`:
```python
item_select.on(
"add",
handler=lambda event: print(event.args),
js_handler="(details) => emit({index: details.index, value: details.value})",
)
```
Omit the Python handler for a client-only action, or omit `js_handler` to use NiceGUI's default `(...args) => emit(...args)` forwarding behavior. Since NiceGUI `2.18.0`, both can be supplied together. A `js_handler` may also decide not to call `emit`, in which case no Python callback runs for that occurrence.
Events that pass imperative JavaScript callbacks require special care. For example, QSelect's `filter` event emits an input string plus `doneFn` and `abortFn` functions. Those functions cannot be serialized for later use by Python. Use NiceGUI's wrapper-supported filtering API, or consume such callbacks synchronously in browser-side JavaScript; do not treat them as ordinary server payloads.
#### Server-Authoritative Edit Proposals
Treat values received from the browser as proposals, even when Quasar validation or input constraints have already run. Attach the listener to the component that emits the event, use `js_handler` to send only the identity and serializable values Python needs, and validate the field allowlist, types, ranges, permissions, record existence, and persistence constraints in Python. The browser may keep temporary editor state, but it is not the source of truth.
Choose when proposals cross the client-server boundary according to the interaction:
- Use `update:model-value` for discrete editors such as selects, switches, and checkboxes.
- For text and numeric inputs accepted during typing, use the component's documented `debounce` prop to avoid a server round trip for every keystroke.
- For an explicit save/cancel workflow, keep a local draft in a dialog or popup and emit one proposal on save.
- During asynchronous persistence, disable the editor or expose a busy state. Add an entity version or another optimistic-concurrency check when multiple clients can edit the same record.
After validation, pass accepted values to the authoritative model and persistence boundary. See [bindable dataclasses](./binding-dataclasses.md#authoritative-models-and-projections) for projection, rollback, and refresh mechanics, and [editable tables](./tables.md) for the QTable-specific form of this pattern.
#### Modifiers And High-Frequency Events
Dot suffixes use Vue's [event and key modifier rules](https://vuejs.org/guide/essentials/event-handling.html#event-modifiers):
```python
field.on("keydown.enter", submit)
field.on("click.stop", handle_click)
viewport.on("scroll.passive", handle_scroll, throttle=0.1)
```
NiceGUI separates listener options such as `capture`, `once`, and `passive`, event modifiers such as `stop`, `prevent`, and `self`, and key filters such as `enter`. The tagged [`EventListener.to_dict()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/event_listener.py) performs that classification before the frontend applies Vue's `withModifiers()` and `withKeys()` helpers. `throttle`, `leading_events`, and `trailing_events` regulate messages sent to Python; they do not throttle a client-only `js_handler` that never calls `emit`.
## Framework Boundary Model ## Framework Boundary Model
@@ -15,9 +248,7 @@ A NiceGUI component is not a Python-rendered HTML fragment. Customization passes
Treat the generated DOM beneath a Quasar component as private implementation detail. Work through the highest owning layer that expresses the requirement. Treat the generated DOM beneath a Quasar component as private implementation detail. Work through the highest owning layer that expresses the requirement.
## How The APIs Map ## API Mapping Across Layers
Use this map after confirming the exact API against the installed NiceGUI and bundled Quasar versions:
| Requirement | NiceGUI surface | Underlying mechanic | | Requirement | NiceGUI surface | Underlying mechanic |
| --- | --- | --- | | --- | --- | --- |
@@ -28,7 +259,7 @@ Use this map after confirming the exact API against the installed NiceGUI and bu
| Imperative frontend action | a NiceGUI helper or `run_method(...)` | NiceGUI invokes a public method on the client component | | Imperative frontend action | a NiceGUI helper or `run_method(...)` | NiceGUI invokes a public method on the client component |
| Page placement or appearance | `.classes(...)`, `.style(...)`, or an application stylesheet | CSS applies to the rendered element; detached content needs its own class hook | | Page placement or appearance | `.classes(...)`, `.style(...)`, or an application stylesheet | CSS applies to the rendered element; detached content needs its own class hook |
Do not copy a Vue template into Python. Translate each part according to its owner: constructor data stays in Python, Quasar props go through `.props()`, emitted events go through callbacks or `.on()`, and named Vue slots go through NiceGUI's slot API. Constructor data remains in Python, Quasar props cross through `.props()`, emitted events cross through callbacks or `.on()`, and named Vue slots cross through NiceGUI's slot API. A Vue example in the Quasar documentation therefore maps to several distinct NiceGUI surfaces rather than to one copied template.
## State And Event Flow ## State And Event Flow
@@ -40,48 +271,28 @@ Server-driven changes and user-driven changes cross a client-server boundary:
4. NiceGUI forwards registered events to Python handlers. 4. NiceGUI forwards registered events to Python handlers.
5. Python mutations return through bindings, wrapper helpers, or an explicit `update()`. 5. Python mutations return through bindings, wrapper helpers, or an explicit `update()`.
Use wrapper helpers and bindings when available because they preserve NiceGUI's value model. Directly changing a Python collection or constructing a raw JavaScript object does not imply that the client receives the change. Wrapper helpers and bindings preserve NiceGUI's value model and schedule the corresponding client update. Directly changing a plain Python collection or constructing a raw JavaScript object does not itself imply that the client receives the change.
## Detached Content And Assets ## Detached Content And Assets
Some Quasar components render menus, dialogs, tooltips, and similar content outside the field or trigger's DOM subtree. A descendant CSS selector beneath the Python-created element will not reach that content. Use the component's documented popup or content class prop, then style that application-owned class separately. Some Quasar components render menus, dialogs, tooltips, and similar content outside the field or trigger's DOM subtree. A descendant CSS selector beneath the Python-created element will not reach that content. Component APIs expose props such as `popup-content-class` for assigning a separate class hook to detached content.
Icons and other externally defined visuals add another boundary: a valid Quasar icon name identifies an asset but does not load its font or stylesheet. Confirm both the naming convention and the application-level asset registration. Icons and other externally defined visuals add another boundary: a valid Quasar icon name identifies an asset but does not load its font or stylesheet. Confirm both the naming convention and the application-level asset registration.
## Component Customization Workflow ## Versioned Sources
Research the target component before generating code or CSS. Do not rely on a remembered NiceGUI or Quasar API, and do not mix source versions. The exact public surface depends on both the installed NiceGUI version and the Quasar version bundled with it. NiceGUI's tagged `package.json` records that pairing. The component details below describe NiceGUI `3.16.0` with Quasar `2.18.5`, as declared by [NiceGUI `v3.16.0` frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json).
### Establish The Version Pair Four source levels answer different questions:
1. Read the target project's lockfile or installed package metadata to identify its exact NiceGUI version. | Source | Information it defines |
2. Open `package.json` at that NiceGUI tag and read the exact `quasar` dependency version. | --- | --- |
3. Use the NiceGUI tag for both NiceGUI sources and the matching `quasar-v<version>` tag for both Quasar sources. | NiceGUI component documentation | documented Python constructors, callbacks, methods, and examples |
| NiceGUI wrapper source at the installed tag | normalization, validation, stored properties, bindings, updates, and the wrapped frontend component |
| Quasar component API at the bundled tag | accepted props, emitted events, named slots, public methods, accessibility behavior, and warnings |
| Quasar component source at the bundled tag | detailed runtime behavior behind that public API |
The curated component sections below use NiceGUI `3.16.0` and Quasar `2.18.5`. The pairing comes from [NiceGUI `v3.16.0` frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json). Repeat the version check when the target application uses another NiceGUI release. Never infer compatibility from Quasar's latest release or use NiceGUI `main` with Quasar `dev`. Links to `main`, `dev`, or the latest hosted documentation can describe a newer API than the installed package. Tagged NiceGUI and matching `quasar-v<version>` links provide the version-specific definition.
### Research Four Sources
Review these sources in order for the selected version pair:
1. **NiceGUI documentation:** identify the supported Python API and documented examples for the component.
2. **NiceGUI source code:** inspect constructor normalization, validation, props, bindings, events, helpers, and the wrapped frontend component.
3. **Quasar documentation:** identify the wrapped component's public props, slots, events, methods, accessibility behavior, and documented warnings.
4. **Quasar source code:** verify how those public APIs behave, especially popup mounting, model translation, event flow, rendering, and public methods.
Use current upstream sources only when the target version is unavailable, and state that fallback explicitly. If the installed package differs from its tag, follow the installed implementation and record the difference.
### Apply The Findings
For every component section:
1. Link the four version-matched sources under **Research Sources**.
2. Summarize which layer owns the behavior under **Ownership Result**.
3. Order the supported customization surfaces from highest-level NiceGUI API to lower-level Quasar or CSS mechanisms.
4. Include an example only after the owning APIs are established.
5. Curate a short caveat list from the four sources. Keep only constraints that change implementation, security, accessibility, performance, or testing decisions.
If the requirement is purely visual after this ownership check, continue in [visual styling and CSS](./styling-and-customization.md).
## Using Slots In NiceGUI ## Using Slots In NiceGUI
@@ -91,9 +302,9 @@ NiceGUI creates a default slot for every element. Entering an element as a conte
These mechanics are defined by the tagged [`Element.add_slot()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/element.py), the [`Slot` context manager](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/slot.py), and NiceGUI's [context-managed scoped-slot examples](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/table_documentation.py). These mechanics are defined by the tagged [`Element.add_slot()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/element.py), the [`Slot` context manager](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/slot.py), and NiceGUI's [context-managed scoped-slot examples](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/table_documentation.py).
### Prefer Context-Managed NiceGUI Elements ### Context-Managed NiceGUI Elements
Build slot content with ordinary NiceGUI elements by default: Ordinary NiceGUI elements can populate slot content:
```python ```python
name_input = ui.input("Name") name_input = ui.input("Name")
@@ -102,46 +313,94 @@ with name_input.add_slot("prepend"):
ui.icon("person") ui.icon("person")
``` ```
Use nested context managers to express the component hierarchy. This preserves NiceGUI element identity, event registration, updates, deletion, and test visibility. Pass a raw Vue template to `add_slot(name, template)` only when the slot requires client-side structure that ordinary NiceGUI elements cannot express cleanly, such as a `v-for` that creates a variable number of sibling elements. Nested context managers express the component hierarchy while preserving NiceGUI element identity, event registration, updates, deletion, and test visibility. The `add_slot(name, template)` form accepts a raw Vue template for client-side structures such as a `v-for` that creates a variable number of sibling elements.
### Use Scoped Props On The Client ### Scoped Props On The Client
A scoped slot receives a `props` object from its owning Vue component. Since NiceGUI `3.5.0`, NiceGUI elements inside a scoped-slot context can reference that object in dynamic `.props()` expressions and JavaScript event handlers: A scoped slot is a function whose argument is supplied by the component that renders the slot. Vue calls that argument the slot props; `props` is only NiceGUI's chosen local name for it. Since NiceGUI `3.5.0`, context-managed NiceGUI elements inside a scoped slot receive the current slot-props object as their frontend render context.
- use `.props(":label=props.value")` or another component-supported prop to display a scoped value The general `.props()` grammar and dynamic binding path are described under [Props](#props). In this context, the current scope object can be referenced by dynamic properties and JavaScript event handlers. For example:
- use `.props("v-bind=props.itemProps")` when the slot provides a bundle of required attributes and handlers
- use `.on(..., js_handler="... emit(...)", handler=...)` to transform and send serializable scoped values to Python ```python
ui.badge().props(
':label=props.label :color="props.selected ? \'primary\' : \'grey\'"'
)
```
corresponds conceptually to this Vue template:
```vue
<q-badge :label="props.label" :color="props.selected ? 'primary' : 'grey'" />
```
Static `.props()` values do not have access to the slot scope. Only colon-prefixed expressions and NiceGUI JavaScript event handlers are evaluated with `props` in scope.
#### Which `props.*` Names Exist
There is no global catalog of `props.*` attributes. The owner of each named slot chooses the keys it passes when invoking that slot, so the available names can differ between components and between slots on the same component. Find them in this order:
1. Open the wrapped component's version-matched Quasar API and inspect the **Slots** entry for the exact named slot.
2. Use the slot's `scope` table as the public contract, including each value's type and whether it is data, state, or a callable.
3. Inspect the version-matched Quasar source only when the API does not explain a bundle's contents or runtime behavior.
For example, the [`QSelect` `option` slot API at Quasar `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.json) exposes:
| Expression | Meaning |
| --- | --- |
| `props.index` | index in the options array |
| `props.opt` | original option from the `options` prop |
| `props.label` | label after `option-label` processing |
| `props.html` | whether the option content is marked as HTML |
| `props.selected` | whether this option is selected |
| `props.focused` | whether this option is the focused menu option |
| `props.toggleOption` | function that adds or removes an option from the model |
| `props.setOptionIndex` | function that changes the focused option index |
| `props.itemProps` | object of computed props and listeners intended for the root `QItem` |
The tagged [`QSelect` implementation](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.js) constructs `itemProps` with values such as `clickable`, `active`, `activeClass`, `manualFocus`, `focused`, `disable`, `tabindex`, `dense`, `dark`, `role`, `aria-selected`, `id`, `onClick`, and, when applicable, `onMousemove`. It is a behavior and accessibility bundle, not the original option object. Other QSelect slots expose different scopes: `no-option` only documents `inputValue`, while `selected-item` documents selection-oriented keys such as `index`, `opt`, `removeAtIndex`, `toggleOption`, and `tabindex`. A QTable body-cell slot's `props.value` is valid because QTable supplies `value`; that name should not be assumed in a QSelect option slot.
#### Sending Scoped Values To Python
NiceGUI also places the current slot object in scope while evaluating a `js_handler`. Use the event bridge's `emit(...)` function to select or transform JSON-serializable values before the Python callback runs:
```python
ui.button("Inspect").on(
"click",
handler=lambda event: print(event.args),
js_handler="() => emit({index: props.index, label: props.label})",
)
```
Scoped props exist only in the browser render context. They are not Python variables and cannot be read by a Python callback until a JavaScript handler emits the required values. Treat `innerHTML`, `v-html`, and raw template interpolation as untrusted HTML unless the source is explicitly sanitized. Scoped props exist only in the browser render context. They are not Python variables and cannot be read by a Python callback until a JavaScript handler emits the required values. Treat `innerHTML`, `v-html`, and raw template interpolation as untrusted HTML unless the source is explicitly sanitized.
### Preserve The Slot Contract ### Slot Contracts
Replacing default slot content also replaces the wrapped component's default rendering. Preserve any documented slot-prop bundle that carries behavior. For example, a `QSelect` option slot must bind `props.itemProps` to its root item; otherwise the custom row can lose click selection, disabled state, focus, active state, and keyboard navigation. Keep one root element per virtual-scroll item unless the component documents how to mark additional siblings. Replacing default slot content also replaces the wrapped component's default rendering. Documented slot-prop bundles can carry behavior as well as data. For example, a `QSelect` option slot binds `props.itemProps` to its root item; without that binding, the custom row can lose click selection, disabled state, focus, active state, and keyboard navigation. Quasar's virtual-scroll contract expects one root element per item unless additional siblings carry its documented marker class.
## `ui.select` ## `ui.select`
### Research Sources ### Versioned Source Definitions
- **NiceGUI documentation:** [`ui.select` documentation source at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/select_documentation.py) - **NiceGUI documentation:** [`ui.select` documentation source at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/select_documentation.py)
- **NiceGUI source code:** [`Select` implementation at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/select.py) - **NiceGUI source code:** [`Select` implementation at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/select.py)
- **Quasar documentation:** [`QSelect` documentation source at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/docs/src/pages/vue-components/select.md) - **Quasar documentation:** [`QSelect` documentation source at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/docs/src/pages/vue-components/select.md)
- **Quasar source code:** [`QSelect` implementation at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.js) - **Quasar source code:** [`QSelect` implementation at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.js)
### Ownership Result ### Layer Ownership
NiceGUI's `Select` wraps Quasar `QSelect` but owns important Python-side behavior. Its constructor handles options, labels, values, change callbacks, input filtering, new-value modes, multiple selection, clearing, validation, and key generation. Use those constructor parameters before adding equivalent Quasar props manually. NiceGUI's `Select` wraps Quasar `QSelect` but owns important Python-side behavior. Its constructor handles options, labels, values, change callbacks, input filtering, new-value modes, multiple selection, clearing, validation, and key generation. Use those constructor parameters before adding equivalent Quasar props manually.
### Customization Order ### Exposed Surfaces
1. Use `options`, `label`, `value`, `on_change`, `with_input`, `new_value_mode`, `multiple`, `clearable`, `validation`, and `key_generator` through the NiceGUI constructor. - The NiceGUI constructor exposes `options`, `label`, `value`, `on_change`, `with_input`, `new_value_mode`, `multiple`, `clearable`, `validation`, and `key_generator`.
2. Use `.props()` for additional documented `QSelect` behavior such as field design, chips, option density, popup classes, popup positioning, or menu/dialog behavior. - `.props()` carries additional documented `QSelect` behavior such as field design, chips, option density, popup classes, popup positioning, and menu/dialog behavior.
3. Use `.classes()` and Tailwind for the field's structural width and placement. - `.classes()` attaches structural width, placement, and other CSS utilities to the field element.
4. Use named slots for prepend, append, loading, no-option, selected, or option content when props are insufficient. - Named slots provide prepend, append, loading, no-option, selected, and option content.
5. Preserve the documented scoped-slot props when replacing option content so Quasar retains selection and keyboard behavior. - Scoped-slot props retain Quasar's selection and keyboard behavior when option content is replaced.
### Example: Custom Menu Options With A Scoped Slot ### Example: Custom Menu Options With A Scoped Slot
`QSelect` supplies each option as `props.opt` and its interaction contract as `props.itemProps`. NiceGUI elements can consume both inside the slot context without a raw Vue template: `QSelect` supplies each option as `props.opt`, its processed label as `props.label`, and its interaction contract as `props.itemProps`. Because the complete interaction bundle needs Vue's object form of `v-bind`, use a raw slot template for the root item:
```python ```python
from nicegui import ui from nicegui import ui
@@ -157,17 +416,24 @@ item_select = ui.select(
with item_select.add_slot("prepend"): with item_select.add_slot("prepend"):
ui.icon("search") ui.icon("search")
with item_select.add_slot("option"): item_select.add_slot(
with ui.item().props("v-bind=props.itemProps"): "option",
with ui.item_section().props("avatar"): r"""
ui.icon("inventory_2") <q-item v-bind="props.itemProps">
with ui.item_section(): <q-item-section avatar>
ui.badge().props(":label=props.opt.label outline color=primary") <q-icon name="inventory_2" />
</q-item-section>
<q-item-section>
<q-badge :label="props.label" outline color="primary" />
</q-item-section>
</q-item>
""",
)
``` ```
The `prepend` slot adds content around the field. The scoped `option` slot replaces every menu row with context-managed NiceGUI elements; the badge reads the browser-side option label through a dynamic Quasar prop. Keep `v-bind=props.itemProps` on the root `ui.item()` so the custom rendering retains the option's interaction and accessibility wiring. The `prepend` slot uses context-managed NiceGUI elements because it needs no scoped object spread. The raw `option` template is compiled by Vue, so `v-bind="props.itemProps"` forwards every computed property and listener to `QItem`; the badge reads the processed browser-side label. Keep that binding on the root item so the custom rendering retains the option's interaction and accessibility wiring.
### Curated Caveats ### Behavioral Caveats
These caveats are distilled from the four version-matched sources above: These caveats are distilled from the four version-matched sources above:
@@ -177,34 +443,34 @@ These caveats are distilled from the four version-matched sources above:
- A multiple select has a list value. NiceGUI normalizes a non-list initial value, but application state should still use the intended list shape. - A multiple select has a list value. NiceGUI normalizes a non-list initial value, but application state should still use the intended list shape.
- `map-options` has a Quasar performance cost. Do not add it to NiceGUI's mapped options without confirming that the wrapper's value translation requires it. - `map-options` has a Quasar performance cost. Do not add it to NiceGUI's mapped options without confirming that the wrapper's value translation requires it.
- `display-value-html` and `options-html` can create cross-site scripting risk. When using `selected`, `selected-item`, or `option` slots, the application owns sanitization. - `display-value-html` and `options-html` can create cross-site scripting risk. When using `selected`, `selected-item`, or `option` slots, the application owns sanitization.
- A custom `option` slot must bind `props.itemProps` to its root `ui.item()` so click, focus, active, disabled, and keyboard behavior remain connected. - A custom `option` slot must bind `props.itemProps` to its root `QItem` so click, focus, active, disabled, and keyboard behavior remain connected.
- Custom option slots use virtual scrolling. When one option renders multiple sibling elements, Quasar requires `q-virtual-scroll--with-prev` on every additional sibling. - Custom option slots use virtual scrolling. When one option renders multiple sibling elements, Quasar requires `q-virtual-scroll--with-prev` on every additional sibling.
- Buttons placed in `before`, `after`, `prepend`, or `append` field slots do not propagate clicks to the parent. A submit button in one of those slots needs its own submit handler. - Buttons placed in `before`, `after`, `prepend`, or `append` field slots do not propagate clicks to the parent. A submit button in one of those slots needs its own submit handler.
- `QSelect` renders its popup outside the field. Style it through `popup-content-class`; do not assume a descendant selector beneath the field will reach it. - `QSelect` renders its popup outside the field. Style it through `popup-content-class`; do not assume a descendant selector beneath the field will reach it.
- Quasar switches between menu and dialog popup behavior by platform. Verify forced `behavior=menu` carefully on iOS when input filtering is enabled. - Quasar switches between menu and dialog popup behavior by platform. Verify forced `behavior=menu` carefully on iOS when input filtering is enabled.
Use `.on()` or `run_method()` only after confirming the event or method in the installed Quasar API. Prefer NiceGUI's `on_change`, `set_options()`, value bindings, and `is_showing_popup` when they cover the behavior. `.on()` and `run_method()` address events and methods defined by the installed Quasar API. NiceGUI's `on_change`, `set_options()`, value bindings, and `is_showing_popup` provide wrapper-managed equivalents for their respective behaviors.
## `ui.icon` ## `ui.icon`
### Research Sources ### Versioned Source Definitions
- **NiceGUI documentation:** [`ui.icon` documentation source at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/icon_documentation.py) - **NiceGUI documentation:** [`ui.icon` documentation source at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/icon_documentation.py)
- **NiceGUI source code:** [`Icon` implementation at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/icon.py) - **NiceGUI source code:** [`Icon` implementation at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/icon.py)
- **Quasar documentation:** [`QIcon` documentation source at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/docs/src/pages/vue-components/icon.md) - **Quasar documentation:** [`QIcon` documentation source at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/docs/src/pages/vue-components/icon.md)
- **Quasar source code:** [`QIcon` implementation at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/icon/QIcon.js) - **Quasar source code:** [`QIcon` implementation at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/icon/QIcon.js)
### Ownership Result ### Layer Ownership
NiceGUI's `Icon` is a thin `QIcon` wrapper. Its constructor exposes `name`, `size`, and `color`; the source forwards these to a `q-icon` element. Use Quasar's icon naming and asset rules for anything beyond those parameters. NiceGUI's `Icon` is a thin `QIcon` wrapper. Its constructor exposes `name`, `size`, and `color`; the source forwards these to a `q-icon` element. Use Quasar's icon naming and asset rules for anything beyond those parameters.
### Customization Order ### Exposed Surfaces
1. Choose an icon family that is actually loaded by the application. - The application-loaded icon family determines which icon names can render.
2. Pass the documented icon name, size, and color to `ui.icon()`. - `ui.icon()` accepts the documented icon name, size, and color.
3. Use `.props()` for supported `QIcon` props such as `left`, `right`, or a custom render tag. - `.props()` carries supported `QIcon` props such as `left`, `right`, and a custom render tag.
4. Use `.classes()` for structural placement and an application class for stable visual variants. - `.classes()` controls structural placement and can attach application-defined visual variants.
5. Use a static stylesheet for Material Symbol axes, state variants, custom webfonts, or repeated effects. - Static stylesheets define Material Symbol axes, state variants, custom webfonts, and repeated effects.
### Example ### Example
@@ -232,7 +498,7 @@ ui.icon(
} }
``` ```
### Curated Caveats ### Behavioral Caveats
These caveats are distilled from the four version-matched sources above: These caveats are distilled from the four version-matched sources above:
@@ -245,15 +511,17 @@ These caveats are distilled from the four version-matched sources above:
- `QIcon` renders with `aria-hidden="true"`. For an action, use a semantic control such as `ui.button(icon=..., on_click=...)` and put the accessible name on that control; a tooltip is supplementary. - `QIcon` renders with `aria-hidden="true"`. For an action, use a semantic control such as `ui.button(icon=..., on_click=...)` and put the accessible name on that control; a tooltip is supplementary.
- Prefer `ui.icon(...).tooltip(...)` over manually constructing tooltip slot markup when NiceGUI's method covers the visual hint. - Prefer `ui.icon(...).tooltip(...)` over manually constructing tooltip slot markup when NiceGUI's method covers the visual hint.
## Completion Check ## Related Reference Index
Before accepting a special-component customization: - [NiceGUI component documentation](https://nicegui.io/documentation): Python constructors, callbacks, bindings, and wrapper methods
- [NiceGUI `Element` documentation](https://nicegui.io/documentation/element): common props, classes, styles, hierarchy, updates, and client methods
1. Record the target NiceGUI version and its declared Quasar version. - [NiceGUI generic events](https://nicegui.io/documentation/generic_events): `.on()`, event arguments, JavaScript handlers, and throttling
2. Link the version-matched NiceGUI documentation and source code. - [NiceGUI binding documentation](https://nicegui.io/documentation/section_binding_properties): one-way and two-way Python property binding
3. Link the version-matched Quasar documentation and source code. - [Quasar component documentation](https://quasar.dev/vue-components): per-component props, events, slots, and methods
4. Identify constructor arguments, Quasar props, slots, Tailwind classes, and stylesheet rules separately. - [Quasar breakpoints](https://quasar.dev/style/breakpoints): viewport names and pixel thresholds
5. Confirm detached popup or external asset behavior where applicable. - [Quasar spacing classes](https://quasar.dev/style/spacing): padding and margin class syntax and permutations
6. Keep the caveat list traceable to the four researched sources. - [Quasar visibility classes](https://quasar.dev/style/visibility): responsive, platform, orientation, and print visibility
7. Test keyboard interaction, focus, labels, and tooltips. - [Quasar helper classes](https://quasar.dev/style/other-helper-classes): pointer, scrolling, sizing, rotation, and border helpers
8. Test the supported mobile, landscape desktop, and portrait desktop viewports. - [Tailwind CSS documentation](https://tailwindcss.com/docs): complete utility-class categories and variant syntax
- [MDN CSS reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference): CSS properties accepted by `.style()` and application stylesheets
- [MDN event reference](https://developer.mozilla.org/en-US/docs/Web/Events): native browser event names and behavior
@@ -14,35 +14,38 @@ This reference was verified against the latest released NiceGUI stack at the tim
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. 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 ## Table Ownership Model
Treat an edit as a proposal, not a browser-side state mutation: This pattern combines [server-authoritative component events](./component-mechanics.md#server-authoritative-edit-proposals) with [bindable model projections](./binding-dataclasses.md#authoritative-models-and-projections):
1. A render function converts dataframe records into row-scoped [bindable dataclasses](./binding-dataclasses.md). 1. A render function converts dataframe records into row-scoped [bindable dataclasses](./binding-dataclasses.md).
2. Each editable dataclass field is bound to the corresponding serializable QTable row field. 2. Each bindable row owns its serializable QTable projection and whether an accepted edit has touched it.
3. A NiceGUI editor displays that projection through `props.value` in a QTable scoped slot. 3. A NiceGUI editor displays that projection through `props.value` in a QTable scoped slot.
4. The editor emits stable row identity, the field name, and the proposed value. 4. The editor emits stable row identity, the field name, and the proposed value.
5. Python locates the row dataclass, validates and assigns the value, persists the row to the dataframe or repository, and sends the resulting projection back with `table.update_rows(...)`. 5. Python locates the row, validates and assigns the value, marks it as touched, and sends the resulting projection back with `table.update_rows(...)`.
```mermaid ```mermaid
flowchart LR flowchart LR
A[Dataframe or repository] -->|render| B[Bindable row dataclasses] A[Dataframe or repository] -->|render| B[EditableTableState]
B -->|field bindings| C[QTable row payloads] B --> C[Bindable row: fields, payload, touched]
C -->|props.value| D[NiceGUI editor] C -->|field bindings| D[QTable row payloads]
D -->|row key, field, proposed value| E[Python handler] D -->|props.value| E[NiceGUI editor]
E --> F{validate} E -->|row key, field, proposed value| F[Python handler]
F -->|accept| B F --> G{validate}
B -->|persist| A G -->|accept and mark touched| C
F -->|reject| G[notify] C -->|persist touched rows later| A
G -->|reject| H[notify]
``` ```
The bindable dataclasses are the canonical page state in Python. The dataframe is the load and persistence boundary in this example; a production application can replace it with a service or repository. 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. 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 ## 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. [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 uses a render function to transform a dataframe into an `EditableTableState`. That state owns one `EditableRow` per stable identifier and one serializable QTable payload per row. NiceGUI's `binding.bind_to` links each bindable dataclass field to its corresponding payload field, so assigning `row_state.name`, `row_state.quantity`, or `row_state.status` updates the Python-side table projection immediately. 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](./binding-dataclasses.md#authoritative-models-and-projections).
`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. 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.
@@ -52,35 +55,11 @@ The complete runnable source is available as [`editable_table.py`](../examples/e
--8<-- "docs/skills/nicegui/examples/editable_table.py" --8<-- "docs/skills/nicegui/examples/editable_table.py"
``` ```
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 editor path uses the transformed-event pattern from [controlled values and model events](./component-mechanics.md#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`.
The `update:model-value` callback receives the emitted model value itself. Forward it with `(value) => emit(..., value)`; do not read `value.value`. For `ui.number`, the underlying Quasar input emits numeric text and NiceGUI normally performs the float conversion in its built-in value handler. Because this custom handler forwards the event, `normalize_edit` accepts numeric strings and performs the authoritative integer conversion in Python. ## Persistence And Row Refresh
The `:model-value="props.value"` prop is deliberately one-way at the client boundary. 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`; Python assignment to the selected bindable dataclass then updates the corresponding table-row payload. Keep `table.rows` as the serializable projection described in [bindable dataclasses](./binding-dataclasses.md#persistence-and-rollback), 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`.
## 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. The row-scoped bindable dataclasses are the page model, and the dataframe or repository is its persistence boundary. On acceptance:
1. validate and coerce into domain types
2. assign the normalized value to the matching bindable dataclass field
3. persist that dataclass through the dataframe adapter, service, or repository
4. call `table.update_rows(state.table_rows(), clear_selection=False)`
On validation rejection, leave the dataclass unchanged. On persistence failure, restore its previous value before re-raising or reporting the error. Perform step 4 in either case so the field binding and canonical Python state overwrite any temporary editor display. 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 ## QTable And QPopupEdit Escalation
@@ -92,7 +71,7 @@ That restriction changes the implementation boundary: a full `body` slot must re
1. confirm an ordinary NiceGUI editor or dialog cannot meet the interaction requirement 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 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` 3. keep popup draft state local rather than assigning into `props.row`, following the [explicit save/cancel proposal pattern](./component-mechanics.md#server-authoritative-edit-proposals)
4. emit the stable row key, field, and saved proposal to Python 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 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 6. test keyboard focus, save, cancel, validation failure, sorting, filtering, pagination, and selection
@@ -117,15 +96,6 @@ Replacing the full row template has a larger maintenance and accessibility surfa
- [`QPopupEdit` source](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/popup-edit/QPopupEdit.js) - [`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) - [`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 ## Completion Check
Before accepting an editable table: Before accepting an editable table:
@@ -133,12 +103,12 @@ Before accepting an editable table:
1. Pin the NiceGUI release and verify its bundled Quasar and Vue versions. 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. 2. Use an immutable, unique `row_key`; never persist by view index.
3. Transform dataframe records into row-scoped bindable dataclasses during rendering. 3. Transform dataframe records into row-scoped bindable dataclasses during rendering.
4. Bind each editable dataclass field to its corresponding serializable QTable row field. 4. Keep each dataclass, serializable QTable row, and touched flag together on one bindable row.
5. Display the projected value from QTable scoped props; do not bind one shared slot template to one Python row object. 5. Display the projected value from QTable scoped props; do not bind one shared slot template to one Python row object.
6. Attach the event listener directly to the editor and emit only row identity, field, and proposed value. 6. Apply the [controlled-value event proposal](./component-mechanics.md#controlled-values-and-model-events) directly to each editor and emit only row identity, field, and proposed value.
7. Validate field access, types, ranges, permissions, and record existence in Python. 7. Validate and normalize proposals in Python before assigning them.
8. Assign the dataclass field, persist through the owning adapter or service, and roll back that assignment on failure. 8. Mark accepted rows as touched and derive touched dataclasses from the rows still held by the container.
9. Reassert canonical rows after accepted and rejected proposals. 9. Reassert canonical rows after accepted and rejected proposals.
10. Test editing after sort, filter, pagination, and selection changes. 10. Test editing after sort, filter, pagination, and selection changes.
11. Test stale rows, invalid input, persistence failure, and concurrent edits. 11. Test stale rows, invalid input, persistence failure, concurrent edits, and removal of touched bindings.
12. Use a full `body` slot for `QPopupEdit`, never a `body-cell-*` slot. 12. Use a full `body` slot for `QPopupEdit`, never a `body-cell-*` slot.
+1 -1
View File
@@ -12,7 +12,7 @@ Skills are MCP resources. The client remains responsible for selecting guidance,
## Published Skill Surface ## Published Skill Surface
Each directory beneath `docs/skills/` publishes: Each directory beneath `src/personal_mcp/docs/skills/` publishes:
1. `skill://<name>/SKILL.md` for primary instructions 1. `skill://<name>/SKILL.md` for primary instructions
2. `skill://<name>/_manifest` for file discovery and integrity metadata 2. `skill://<name>/_manifest` for file discovery and integrity metadata