diff --git a/src/personal_mcp/docs/skills/nicegui/SKILL.md b/src/personal_mcp/docs/skills/nicegui/SKILL.md index 1a4fee2..f8dba44 100644 --- a/src/personal_mcp/docs/skills/nicegui/SKILL.md +++ b/src/personal_mcp/docs/skills/nicegui/SKILL.md @@ -60,9 +60,9 @@ Load [styling and customization](./references/styling-and-customization.md) for: - cosmetic treatment of controls, surfaces, typography, and visual states - visual validation at supported viewport sizes -### Component Mechanics And Customization +### Component Mechanics -Load [component mechanics and customization](./references/component-mechanics-and-customization.md) for: +Load [component mechanics](./references/component-mechanics.md) for: - the NiceGUI Python wrapper, element bridge, Quasar component, and Vue runtime boundaries - deciding between constructors, bindings, Quasar props, events, slots, and frontend methods @@ -70,13 +70,14 @@ Load [component mechanics and customization](./references/component-mechanics-an - detached content and external icon assets - source research against the installed NiceGUI and bundled Quasar versions - `ui.select` and `ui.icon` mechanics and caveats -- dialog scaling when detached popup geometry must be preserved +- scoped component slots and their interaction contracts ### Editable Tables Load [editable tables](./references/tables.md) for: - Python-authoritative editable `ui.table` state +- rendering dataframe records into row-scoped bindable dataclasses - stable row identity across sorting, filtering, and pagination - NiceGUI editors in Quasar `body-cell-*` scoped slots - validation, persistence, rejection, and canonical row refresh @@ -128,7 +129,7 @@ Load [source documentation](./references/source-documentation.md) when: 1. Load [application architecture](./references/architecture.md) for page and component ownership decisions. 2. Load [styling and customization](./references/styling-and-customization.md) for themes, layout, responsive presentation, utility classes, or CSS. -3. Load [component mechanics and customization](./references/component-mechanics-and-customization.md) when behavior must be mapped across NiceGUI, Quasar, and Vue, or when detached content and component-specific behavior are involved. +3. Load [component mechanics](./references/component-mechanics.md) when behavior must be mapped across NiceGUI, Quasar, and Vue, or when detached content and component-specific behavior are involved. 4. Load [editable tables](./references/tables.md) when table cells accept user changes or `QPopupEdit` is being considered. 5. Add [interaction patterns](./references/interaction-patterns.md) or [bindable dataclasses](./references/binding-dataclasses.md) according to the page behavior. diff --git a/src/personal_mcp/docs/skills/nicegui/references/component-mechanics-and-customization.md b/src/personal_mcp/docs/skills/nicegui/references/component-mechanics.md similarity index 50% rename from src/personal_mcp/docs/skills/nicegui/references/component-mechanics-and-customization.md rename to src/personal_mcp/docs/skills/nicegui/references/component-mechanics.md index 2a0b5b3..99db506 100644 --- a/src/personal_mcp/docs/skills/nicegui/references/component-mechanics-and-customization.md +++ b/src/personal_mcp/docs/skills/nicegui/references/component-mechanics.md @@ -1,4 +1,4 @@ -# NiceGUI Component Mechanics And Customization +# 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). @@ -48,31 +48,86 @@ Some Quasar components render menus, dialogs, tooltips, and similar content outs 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. -## Source Research Gate +## Component Customization Workflow -Research the target component before generating code or CSS. Do not rely on a remembered NiceGUI or Quasar API. +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. -For each component: +### Establish The Version Pair -1. Read its current NiceGUI documentation page. -2. Inspect the constructor and implementation in the target project's installed NiceGUI package. -3. Confirm the wrapped Quasar component in the NiceGUI source. -4. Read the matching Quasar guide and API definition for props, slots, events, and methods. -5. Check the target project's pinned NiceGUI version before using current upstream behavior. -6. Record which layer owns each proposed customization before writing it. +1. Read the target project's lockfile or installed package metadata to identify its exact NiceGUI version. +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` tag for both Quasar sources. -Use current upstream source only as a fallback when the target environment is unavailable. If installed and upstream behavior differ, follow the installed version and state the difference. +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`. + +### 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 + +A NiceGUI element is the Python-side representation of a browser component. Many elements wrap Quasar Vue components, whose insertion points are exposed as slots. A simple container normally uses one default slot; more complex components expose named slots such as `prepend`, `append`, `option`, `header`, or `body-cell-*`. The available names and their contracts belong to the wrapped component, so verify them in the version-matched Quasar documentation. + +NiceGUI creates a default slot for every element. Entering an element as a context manager enters that default slot, and entering `element.add_slot(name)` selects a named slot. NiceGUI keeps the active slots on a task-local stack; each element constructed inside the `with` block becomes a child of the innermost active slot. + +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 + +Build slot content with ordinary NiceGUI elements by default: + +```python +name_input = ui.input("Name") + +with name_input.add_slot("prepend"): + 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. + +### Use 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: + +- use `.props(":label=props.value")` or another component-supported prop to display a scoped value +- 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 + +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 + +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. + ## `ui.select` -### Source Map +### Research Sources -- [NiceGUI `ui.select` documentation](https://nicegui.io/documentation/select) -- [NiceGUI `Select` source](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/select.py) -- [Quasar `QSelect` guide](https://quasar.dev/vue-components/select/) -- [Quasar `QSelect` API source](https://github.com/quasarframework/quasar/blob/dev/ui/src/components/select/QSelect.json) +- **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) +- **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) + +### Ownership Result 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. @@ -82,35 +137,39 @@ NiceGUI's `Select` wraps Quasar `QSelect` but owns important Python-side behavio 2. Use `.props()` for additional documented `QSelect` behavior such as field design, chips, option density, popup classes, popup positioning, or menu/dialog behavior. 3. Use `.classes()` and Tailwind for the field's structural width and placement. 4. Use named slots for prepend, append, loading, no-option, selected, or option content when props are insufficient. -5. Use `popup-content-class` to attach an application class to the detached options popup, then fine-tune it in a static stylesheet. +5. Preserve the documented scoped-slot props when replacing option content so Quasar retains selection and keyboard behavior. + +### 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: ```python from nicegui import ui item_select = ui.select( options={"chair": "Chair", "desk": "Desk", "lamp": "Lamp"}, - label="Items", - multiple=True, + label="Item", + value="chair", clearable=True, with_input=True, -).props( - "outlined use-chips options-dense " - "popup-content-class=app-item-select-menu" -).classes( - "w-full md:max-w-md" -) +).props("outlined options-dense") with item_select.add_slot("prepend"): - ui.icon("inventory_2") + ui.icon("search") + +with item_select.add_slot("option"): + with ui.item().props("v-bind=props.itemProps"): + with ui.item_section().props("avatar"): + ui.icon("inventory_2") + with ui.item_section(): + ui.badge().props(":label=props.opt.label outline color=primary") ``` -```css -.app-item-select-menu { - max-height: min(24rem, 60dvh); -} -``` +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. -### Select-Specific Caveats +### Curated Caveats + +These caveats are distilled from the four version-matched sources above: - NiceGUI accepts a list of values or a dictionary mapping values to labels. Do not assume the Python options model is the same as Quasar's JavaScript object-array examples. - After mutating `options`, call `update()` or use `set_options()` so the client receives the change. @@ -118,6 +177,7 @@ with item_select.add_slot("prepend"): - 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. - `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. - 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. - `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. @@ -125,76 +185,16 @@ with item_select.add_slot("prepend"): 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. -### Worked Example: Responsive Dialog And Detached Select Popup - -This example is mechanics-sensitive because a `QSelect` popup is detached from the dialog card. Scale the complete card to preserve Quasar's internal field proportions, but style the popup through its own class without changing its coordinate system. - -Use normal field density and attach application classes through supported APIs: - -```python -ui.input("Name").props("outlined") -ui.number("Quantity").props("outlined") -ui.select(...).props("outlined popup-content-class=app-item-detail-menu") -ui.textarea("Description").props("outlined autogrow") -ui.card().classes("app-detail-card app-item-detail-card") -``` - -```css -:root { - --item-dialog-scale: 1; - --item-dialog-max-height: calc(100dvh - 3rem); -} - -.app-item-detail-card { - width: min(50rem, 50vw); - max-height: var(--item-dialog-max-height); - overflow-y: auto; - overscroll-behavior: contain; - zoom: var(--item-dialog-scale); -} - -/* Restore Quasar's baseline if a global rule overrides it. */ -.app-item-detail-card .q-field, -.app-item-detail-menu { - font-size: 14px; -} - -@media (max-width: 599px) { - :root { - --item-dialog-scale: 1.2; - /* 75dvh becomes 90dvh after 1.2x zoom. */ - --item-dialog-max-height: 75dvh; - } - - .app-item-detail-card { - width: 80vw; - } - - .app-item-detail-menu { - font-size: 16.8px; - } -} -``` - -Do not apply `zoom` or `transform: scale()` to `.app-item-detail-menu`. Quasar positions the detached menu from the unscaled anchor geometry; scaling the menu afterward separates it from its field. Enlarging its text preserves the positioning coordinate system. - -The card's pre-zoom maximum height must account for the scale: - -\[ -h_{\mathrm{pre}} = \frac{h_{\mathrm{visible}}}{s} -\] - -For a desired visual height of `90dvh` at \(1.2\times\), use `75dvh`. Apply scrolling to the card so the dimmed page remains stationary while the form reaches its final field. The `599px` media query matches the upper edge of Quasar's default extra-small breakpoint; verify it against custom Quasar breakpoint configuration. - ## `ui.icon` -### Source Map +### Research Sources -- [NiceGUI `ui.icon` documentation](https://nicegui.io/documentation/icon) -- [NiceGUI `Icon` source](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/icon.py) -- [Quasar `QIcon` guide](https://quasar.dev/vue-components/icon/) -- [Quasar `QIcon` API source](https://github.com/quasarframework/quasar/blob/dev/ui/src/components/icon/QIcon.json) -- [Google Material Symbols and Icons](https://fonts.google.com/icons) +- **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) +- **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) + +### Ownership Result 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. @@ -206,6 +206,8 @@ NiceGUI's `Icon` is a thin `QIcon` wrapper. Its constructor exposes `name`, `siz 4. Use `.classes()` for structural placement and an application class for stable visual variants. 5. Use a static stylesheet for Material Symbol axes, state variants, custom webfonts, or repeated effects. +### Example + ```python from nicegui import ui @@ -230,7 +232,9 @@ ui.icon( } ``` -### Icon-Specific Caveats +### Curated Caveats + +These caveats are distilled from the four version-matched sources above: - Material icon names use snake case. Material variants use prefixes such as `o_`, `r_`, `s_`, `sym_o_`, `sym_r_`, and `sym_s_`. - Other icon families have their own prefixes and require their webfont or stylesheet to be loaded. A valid name does not load the corresponding asset. @@ -238,16 +242,18 @@ ui.icon( - Icon color inherits text color unless the `color` prop or a CSS color overrides it. - Material Symbol variable axes apply to webfont icons, not static SVG icon exports. - Quasar also supports SVG path strings, `svguse:` references, and `img:` URLs. Confirm the exact `QIcon` name format and mount path before generating one of these forms. -- For an action, use a semantic control such as `ui.button(icon=..., on_click=...)` and give it an accessible label or tooltip. Do not turn a bare decorative icon into an unlabeled control. -- Prefer `ui.icon(...).tooltip(...)` over manually constructing tooltip slot markup when NiceGUI's method covers the requirement. +- `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. ## Completion Check Before accepting a special-component customization: -1. Cite the NiceGUI component page and implementation that were inspected. -2. Cite the matching Quasar guide or API source. -3. Identify constructor arguments, Quasar props, slots, Tailwind classes, and stylesheet rules separately. -4. Confirm detached popup or external asset behavior where applicable. -5. Test keyboard interaction, focus, labels, and tooltips. -6. Test the supported mobile, landscape desktop, and portrait desktop viewports. \ No newline at end of file +1. Record the target NiceGUI version and its declared Quasar version. +2. Link the version-matched NiceGUI documentation and source code. +3. Link the version-matched Quasar documentation and source code. +4. Identify constructor arguments, Quasar props, slots, Tailwind classes, and stylesheet rules separately. +5. Confirm detached popup or external asset behavior where applicable. +6. Keep the caveat list traceable to the four researched sources. +7. Test keyboard interaction, focus, labels, and tooltips. +8. Test the supported mobile, landscape desktop, and portrait desktop viewports. \ No newline at end of file diff --git a/src/personal_mcp/docs/skills/nicegui/references/styling-and-customization.md b/src/personal_mcp/docs/skills/nicegui/references/styling-and-customization.md index 6e6f777..4aad18e 100644 --- a/src/personal_mcp/docs/skills/nicegui/references/styling-and-customization.md +++ b/src/personal_mcp/docs/skills/nicegui/references/styling-and-customization.md @@ -1,6 +1,6 @@ # NiceGUI Visual Styling And CSS -Use this reference for cosmetic and presentational work: themes, color roles, utility classes, CSS properties, responsive layout, and static assets. For the mechanics of how a NiceGUI Python element maps to a Quasar Vue component, including props, events, slots, methods, teleported content, and wrapper-owned state, load [component mechanics and customization](./component-mechanics-and-customization.md). +Use this reference for cosmetic and presentational work: themes, color roles, utility classes, CSS properties, responsive layout, and static assets. For the mechanics of how a NiceGUI Python element maps to a Quasar Vue component, including props, events, slots, methods, teleported content, and wrapper-owned state, load [component mechanics](./component-mechanics.md). For package boundaries, dependency direction, and page or component ownership, load [application architecture](./architecture.md). @@ -14,7 +14,7 @@ This page owns how an element looks and fits into a page after the correct compo - responsive page composition and stable control dimensions - reusable application classes, CSS custom properties, and static assets -The companion [component mechanics and customization](./component-mechanics-and-customization.md) reference owns how behavior crosses framework boundaries. Use it when the question is whether a value belongs in a constructor, Quasar prop, Vue event, slot, method, binding, or teleported popup. +The companion [component mechanics](./component-mechanics.md) reference owns how behavior crosses framework boundaries. Use it when the question is whether a value belongs in a constructor, Quasar prop, Vue event, slot, method, binding, or teleported popup. ## Visual Styling Workflow @@ -273,7 +273,7 @@ ui.add_head_html( ## Mechanics-Sensitive Visual Cases -Some visual requests depend on framework behavior before CSS can be chosen safely. Use [component mechanics and customization](./component-mechanics-and-customization.md) for detached menus and dialogs, named slots, icon asset families, Quasar internal geometry, frontend methods, and server-client state synchronization. Its responsive dialog example explains why a card can be scaled while a detached `QSelect` popup must remain in its original positioning coordinate system. +Some visual requests depend on framework behavior before CSS can be chosen safely. Use [component mechanics](./component-mechanics.md) for detached menus and dialogs, named slots, icon asset families, Quasar internal geometry, frontend methods, and server-client state synchronization. Its select example shows how a NiceGUI scoped slot preserves QSelect's option interaction contract without custom CSS. ## Validation Checklist diff --git a/src/personal_mcp/docs/skills/nicegui/references/tables.md b/src/personal_mcp/docs/skills/nicegui/references/tables.md index 343cf9b..6892d36 100644 --- a/src/personal_mcp/docs/skills/nicegui/references/tables.md +++ b/src/personal_mcp/docs/skills/nicegui/references/tables.md @@ -18,158 +18,45 @@ Recheck the dependency manifest and tagged sources when the target application u Treat an edit as a proposal, not a browser-side state mutation: -1. Python projects canonical records into `table.rows`. -2. A NiceGUI editor displays `props.value` from a QTable scoped slot. -3. The editor emits stable row identity, the field name, and the proposed value. -4. Python locates the canonical record, validates and coerces the proposal, and persists the accepted record. -5. Python sends a fresh canonical projection back with `table.update_rows(...)` whether the proposal was accepted or rejected. +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. +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. +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(...)`. ```mermaid flowchart LR - A[Python records] -->|project rows| B[QTable] - B -->|props.value| C[NiceGUI editor] - C -->|row key, field, proposed value| D[Python handler] - D --> E{validate} - E -->|accept| F[persist] - E -->|reject| G[notify] - F --> A - G --> A + A[Dataframe or repository] -->|render| B[Bindable row dataclasses] + B -->|field bindings| C[QTable row payloads] + C -->|props.value| D[NiceGUI editor] + D -->|row key, field, proposed value| E[Python handler] + E --> F{validate} + F -->|accept| B + B -->|persist| A + F -->|reject| G[notify] ``` -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. +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. ## Recommended Cell-Slot Pattern [NiceGUI `ui.table`](https://nicegui.io/documentation/table) supports NiceGUI elements in scoped slots since `3.5.0`. The tagged [`Table.cell` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.py) creates the corresponding Quasar `QTd`, while the tagged [table client component](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.js) forwards QTable's scoped slot props. -The following example keeps canonical records in Python, uses a stable `id`, allows only known fields, validates every proposed value, and reasserts canonical rows after each event. `save_item` is the persistence boundary; replace its in-memory assignment with the application's service or repository call. +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. -```python -from dataclasses import dataclass, replace +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. -from nicegui import events, ui +The complete runnable source is available as [`editable_table.py`](../examples/editable_table.py) and as the supporting resource `skill://nicegui/examples/editable_table.py`. - -@dataclass(frozen=True, slots=True) -class Item: - id: int - name: str - quantity: int - status: str - - -STATUS_OPTIONS = ["draft", "active", "archived"] -items_by_id = { - 101: Item(id=101, name="Desk", quantity=4, status="active"), - 102: Item(id=102, name="Lamp", quantity=12, status="draft"), -} - - -def canonical_rows() -> list[dict[str, str | int]]: - return [ - { - "id": item.id, - "name": item.name, - "quantity": item.quantity, - "status": item.status, - } - for item in items_by_id.values() - ] - - -def validate_edit(item: Item, field: str, raw_value: object) -> Item: - match field: - case "name": - if not isinstance(raw_value, str) or not (name := raw_value.strip()): - raise ValueError("Name is required") - return replace(item, name=name) - case "quantity": - if isinstance(raw_value, bool) or not isinstance(raw_value, (int, float, str)): - raise ValueError("Quantity must be an integer") - if isinstance(raw_value, float) and not raw_value.is_integer(): - raise ValueError("Quantity must be an integer") - try: - quantity = int(raw_value) - except (ValueError, OverflowError) as error: - raise ValueError("Quantity must be an integer") from error - if not 0 <= quantity <= 1_000: - raise ValueError("Quantity must be between 0 and 1000") - return replace(item, quantity=quantity) - case "status": - if not isinstance(raw_value, str) or raw_value not in STATUS_OPTIONS: - raise ValueError("Unknown status") - return replace(item, status=raw_value) - case _: - raise ValueError(f"Field {field!r} is not editable") - - -def save_item(item: Item) -> None: - items_by_id[item.id] = item - - -def apply_edit(event: events.GenericEventArguments) -> None: - try: - raw_row_id, raw_field, raw_value = event.args - row_id = int(raw_row_id) - field = str(raw_field) - current = items_by_id.get(row_id) - if current is None: - raise ValueError("This row no longer exists") - save_item(validate_edit(current, field, raw_value)) - except (TypeError, ValueError) as error: - ui.notify(str(error), type="negative") - finally: - table.update_rows(canonical_rows(), clear_selection=False) - - -columns = [ - {"name": "name", "label": "Name", "field": "name", "align": "left"}, - {"name": "quantity", "label": "Quantity", "field": "quantity", "align": "right"}, - {"name": "status", "label": "Status", "field": "status", "align": "left"}, -] -table = ui.table( - columns=columns, - rows=canonical_rows(), - row_key="id", - selection="multiple", -).classes("w-full") - -with table.add_slot("body-cell-name"): - with table.cell("name"): - ui.input().props( - ':model-value="props.value" dense borderless debounce=400' - ).on( - "update:model-value", - handler=apply_edit, - js_handler="(e) => emit(props.row.id, props.col.name, e.value)", - ) - -with table.add_slot("body-cell-quantity"): - with table.cell("quantity"): - ui.number(min=0, max=1_000).props( - ':model-value="props.value" dense borderless debounce=400' - ).on( - "update:model-value", - handler=apply_edit, - js_handler="(e) => emit(props.row.id, props.col.name, e.value)", - ) - -with table.add_slot("body-cell-status"): - with table.cell("status"): - ui.select(STATUS_OPTIONS).props( - ':model-value="props.value" dense borderless options-dense' - ).on( - "update:model-value", - handler=apply_edit, - js_handler="(e) => emit(props.row.id, props.col.name, e.value)", - ) - -ui.run() +```python title="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 `:model-value="props.value"` prop is deliberately one-way. In Vue, component `v-model` expands to a `modelValue` prop plus an `update:modelValue` listener, as shown in the [Vue component `v-model` guide](https://vuejs.org/guide/components/v-model.html) and its tagged [compiler transform](https://github.com/vuejs/core/blob/v3.5.22/packages/compiler-core/src/transforms/vModel.ts). Here the update listener sends an intent to Python rather than assigning into `props.row`. +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. + +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. ## Commit Policy @@ -184,14 +71,14 @@ Do not rely on browser validation alone. Quasar editor constraints improve feedb ## Persistence And Refresh -Keep `table.rows` as a projection, not the business model. On acceptance: +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. call the service or repository that owns persistence -3. fetch or derive the resulting canonical records -4. call `table.update_rows(canonical_rows(), clear_selection=False)` +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 rejection, perform step 4 anyway. This overwrites any temporary editor display with the last accepted value. Preserve selection only when the selected row identities remain valid; otherwise use the default `clear_selection=True`. +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. @@ -245,11 +132,13 @@ Before accepting an editable table: 1. Pin the NiceGUI release and verify its bundled Quasar and Vue versions. 2. Use an immutable, unique `row_key`; never persist by view index. -3. Bind each editor's displayed value from QTable scoped props. -4. Attach the event listener directly to the editor and emit only row identity, field, and proposed value. -5. Validate field access, types, ranges, permissions, and record existence in Python. -6. Persist through the owning service or repository. -7. Reassert canonical rows after accepted and rejected proposals. -8. Test editing after sort, filter, pagination, and selection changes. -9. Test stale rows, invalid input, persistence failure, and concurrent edits. -10. Use a full `body` slot for `QPopupEdit`, never a `body-cell-*` slot. \ No newline at end of file +3. Transform dataframe records into row-scoped bindable dataclasses during rendering. +4. Bind each editable dataclass field to its corresponding serializable QTable row field. +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. +7. Validate field access, types, ranges, permissions, and record existence in Python. +8. Assign the dataclass field, persist through the owning adapter or service, and roll back that assignment on failure. +9. Reassert canonical rows after accepted and rejected proposals. +10. Test editing after sort, filter, pagination, and selection changes. +11. Test stale rows, invalid input, persistence failure, and concurrent edits. +12. Use a full `body` slot for `QPopupEdit`, never a `body-cell-*` slot. \ No newline at end of file