From b2ac4102f77e6ca29f08432dd6ab88f213719299 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:52:40 -0500 Subject: [PATCH] nicegui component pattern --- src/personal_mcp/docs/skills/nicegui/SKILL.md | 10 +- .../skills/nicegui/references/architecture.md | 96 ++++++++++++++++++- .../nicegui/references/binding-dataclasses.md | 18 ++++ .../references/interaction-patterns.md | 49 +++++++++- 4 files changed, 162 insertions(+), 11 deletions(-) diff --git a/src/personal_mcp/docs/skills/nicegui/SKILL.md b/src/personal_mcp/docs/skills/nicegui/SKILL.md index d592e94..d4dae10 100644 --- a/src/personal_mcp/docs/skills/nicegui/SKILL.md +++ b/src/personal_mcp/docs/skills/nicegui/SKILL.md @@ -1,6 +1,6 @@ --- name: nicegui -description: 'Build, review, and debug NiceGUI applications. Use for FastAPI or Uvicorn integration, app factories and lifespan, ui.* components, Quasar props/events/slots, Tailwind page layout, colors and dark mode, bindings and bindable_dataclass, editable ui.table cells, uploads/forms/live updates, or version-specific NiceGUI source research.' +description: 'Build, review, and debug NiceGUI applications. Use for FastAPI or Uvicorn integration, app factories and lifespan, thin pages and reusable component factories, returned bindable dataclass handles, ui.refreshable methods, ui.* components, Quasar props/events/slots, Tailwind layout, colors, bindings, editable ui.table cells, uploads/forms/live updates, or version-specific source research.' --- # NiceGUI Application Guide @@ -19,20 +19,21 @@ Use this skill to choose the smallest supporting reference for a NiceGUI task. T | Task or symptom | Load first | Add only when | | --- | --- | --- | -| Choose package boundaries, dependency direction, page registration, health routes, or optional persistence, LangGraph, and mounted-docs placement | [application architecture](./references/architecture.md) | Add [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) for concrete ASGI ownership or startup code. | +| Choose package boundaries, dependency direction, thin page composition, reusable component factories, returned dataclass component handles, page registration, health routes, or optional subsystem placement | [application architecture](./references/architecture.md) | Add [binding dataclasses](./references/binding-dataclasses.md) for the component handle's binding graph or [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) for concrete ASGI ownership. | | Decide between `ui.run()` and `ui.run_with()`, compose a parent FastAPI app, define lifespan ordering, build an app factory, configure typed settings, expose a project script, or handle reload/workers | [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) | Add [application architecture](./references/architecture.md) only for wider package placement. | | Choose a `ui.*` constructor, binding, Quasar prop, event, slot, or frontend method; diagnose model events, event payloads, scoped-slot props, detached popups, `ui.select`, or `ui.icon` | [component mechanics](./references/component-mechanics.md) | Add [source documentation](./references/source-documentation.md) when the installed wrapper or bundled Quasar version must be verified. | | Build page shells, rows, columns, grids, widths, overflow, responsive reflow, typography, font loading, static assets, or deliberate scaling | [page structure, typography, and scaling](./references/styling-and-customization.md) | Add [component mechanics](./references/component-mechanics.md) when layout depends on a Quasar prop, slot, popup, or generated component structure. | | Configure `app.colors()`, `ui.colors()`, semantic or fixed Quasar colors, custom color names, component color values, CSS color variables, or `ui.dark_mode()` | [NiceGUI and Quasar color theming](./references/colors-and-quasar-theming.md) | Add [page structure, typography, and scaling](./references/styling-and-customization.md) only when the task also changes physical layout or CSS loading. | -| Model typed page state with `binding.bindable_dataclass`, understand propagation and transform direction, bind nested values, avoid active-link polling, or design projection/persistence rollback | [binding dataclasses](./references/binding-dataclasses.md) | Add [component mechanics](./references/component-mechanics.md) for browser-originated event proposals. | +| Model typed page or component state with `binding.bindable_dataclass`, return a bound component handle, understand propagation and transform direction, bind nested values, avoid active-link polling, or design projection/persistence rollback | [binding dataclasses](./references/binding-dataclasses.md) | Add [application architecture](./references/architecture.md) for the render-factory and thin-page boundary or [component mechanics](./references/component-mechanics.md) for browser-originated proposals. | | Make `ui.table` cells editable with stable row keys, dataframe projections, row-scoped dataclasses, validation, touched rows, selection-preserving refresh, or `QPopupEdit` | [editable tables](./references/tables.md) | Follow its links to binding or component mechanics only when changing the underlying projection or event bridge. | -| Implement uploads, form submission, SSE versus WebSockets, background jobs, duplicate-submit guards, or explicit `@ui.refreshable` refreshes | [interaction patterns](./references/interaction-patterns.md) | Add [binding dataclasses](./references/binding-dataclasses.md) when state propagation itself is the problem. | +| Implement uploads, form submission, SSE versus WebSockets, background jobs, duplicate-submit guards, or `@ui.refreshable` and `@ui.refreshable_method` component regions | [interaction patterns](./references/interaction-patterns.md) | Add [application architecture](./references/architecture.md) for the reusable component contract or [binding dataclasses](./references/binding-dataclasses.md) when state propagation itself is the problem. | | Investigate upload errors, async UI races, stale assets, navigation/state drift, or perform a compact production-readiness review | [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md) | Follow the symptom to one detailed reference above. | | Verify a framework claim against primary NiceGUI, FastAPI, Uvicorn, Tailwind, Quasar, SQLAlchemy, Pydantic, or LangGraph documentation | [source documentation](./references/source-documentation.md) | Use a task page first when implementation guidance, not source lookup, is needed. | ## Boundary Rules - Use [application architecture](./references/architecture.md) for module ownership, not for page geometry or low-level component behavior. +- Keep page functions thin: compose page shells and returned component handles there; keep each component's element tree, bindings, callbacks, and bounded refreshes in its render factory or component object. - Use [page structure, typography, and scaling](./references/styling-and-customization.md) for physical layout. Use [component mechanics](./references/component-mechanics.md) for the behavior crossing NiceGUI, Quasar, Vue, and browser boundaries. - Use [binding dataclasses](./references/binding-dataclasses.md) for the binding graph and Python model projections. Use [interaction patterns](./references/interaction-patterns.md) for user workflows such as upload, submit, refresh, streaming, and background work. - Start editable-table work in [editable tables](./references/tables.md). It already identifies the exact binding and event sections needed by that pattern. @@ -49,6 +50,7 @@ Load an example only when its exact mechanic matches the task: ## Defaults That Span References - Keep composition, transport, services, pages, and components directionally separated. +- Prefer reusable render functions that return typed component handles; use bindable dataclass fields for the state intentionally exposed to composition code. - Keep business logic out of UI components and event handlers. - Avoid blocking I/O and CPU-heavy work in the UI event loop. - Prefer event-driven updates and explicit refreshes to unrelated polling. diff --git a/src/personal_mcp/docs/skills/nicegui/references/architecture.md b/src/personal_mcp/docs/skills/nicegui/references/architecture.md index 88cf960..1fd1991 100644 --- a/src/personal_mcp/docs/skills/nicegui/references/architecture.md +++ b/src/personal_mcp/docs/skills/nicegui/references/architecture.md @@ -70,12 +70,104 @@ Avoid imports from services back into API or UI modules. ## Page And Component Ownership -Page modules compose routes from presentation components and service calls. They should not own domain rules, persistence, or long-running synchronous work. +Page modules should be a thin route-level composition layer. A page resolves route inputs and page-scoped dependencies, establishes the page shell, composes reusable components, and wires only the interactions that cross component boundaries. It should not contain a component's internal element tree, field bindings, refresh logic, domain rules, persistence, or long-running synchronous work. -Extract a presentation pattern to `ui/components/` when it appears on two or more pages or owns a meaningful interaction boundary. Keep one-off route composition in the page module. Reusable components should accept data and event callbacks instead of importing page state or business services implicitly. +Extract a presentation pattern to `ui/components/` when it appears on two or more pages or owns a meaningful state or interaction boundary. Reusable components should accept initial data, use-case functions, and event callbacks explicitly instead of importing page state or business services implicitly. For page composition, responsive layout, Quasar props, and CSS customization, load [styling and customization](./styling-and-customization.md). +## Reusable Component Contract + +In this architecture, a "component" is an application-level composition pattern, not necessarily a custom Vue component or a subclass of NiceGUI `Element`. Its usual shape is: + +1. A typed dataclass represents the component's public handle and local UI state. +2. A render or factory function creates one component instance, builds its element subtree, and binds elements to that instance. +3. The function returns the instance so its caller can read or change intentional state, invoke public actions, or coordinate it with another component. +4. Internal elements, event handlers, validation feedback, and refreshable regions remain private to the component unless an imperative element handle is intentionally part of its API. + +Use [`binding.bindable_dataclass`](./binding-dataclasses.md) for fields that drive or receive element properties. Plain `@dataclass` is sufficient when the returned object only groups element handles or callbacks and does not need immediate field propagation. Use `bindable_fields` when the dataclass also stores injected dependencies or other fields that should not participate in NiceGUI's binding graph. + +```python +from collections.abc import Awaitable, Callable +from dataclasses import field + +from nicegui import binding, ui + + +Search = Callable[[str], Awaitable[list[str]]] + + +@binding.bindable_dataclass(bindable_fields={"query", "busy", "items"}) +class SearchPanel: + search: Search = field(repr=False) + query: str = "" + busy: bool = False + items: list[str] = field(default_factory=list) + + @ui.refreshable_method + def render_results(self) -> None: + if not self.items: + ui.label("No results") + for item in self.items: + ui.label(item) + + async def submit(self) -> None: + if self.busy: + return + self.busy = True + try: + self.items = await self.search(self.query) + await self.render_results.refresh() + finally: + self.busy = False + + +def render_search_panel(search: Search) -> SearchPanel: + panel = SearchPanel(search=search) + with ui.column().classes("w-full gap-3"): + ui.input("Search").bind_value(panel, "query") + ui.button("Search", on_click=panel.submit).bind_enabled_from( + panel, + "busy", + backward=lambda busy: not busy, + ) + ui.label().bind_text_from( + panel, + "items", + backward=lambda items: f"{len(items)} results", + ) + panel.render_results() + return panel +``` + +The returned dataclass is the component API. Its bindable fields synchronize stable element properties, while `render_results()` owns a bounded region whose child structure changes with `items`. The injected `search` callable preserves dependency direction: the component can invoke a use case without locating a service globally. + +[`@ui.refreshable_method`](https://nicegui.io/documentation/refreshable) is the instance-oriented refresh surface for this pattern. NiceGUI records refresh targets by method instance, allowing each page-created component object to refresh independently. Detailed target, argument, async, and lifecycle behavior is documented under [refreshable component regions](./interaction-patterns.md#refreshable-component-regions). + +### Thin Page Example + +```python +from nicegui import ui + +from app.services.catalog import search_catalog +from app.ui.components.search_panel import render_search_panel + + +@ui.page("/catalog") +def catalog_page() -> None: + with ui.column().classes("mx-auto w-full max-w-5xl gap-6"): + ui.label("Catalog").classes("text-2xl font-semibold") + render_search_panel(search_catalog) +``` + +The page owns the route and composition. The component owns its controls, binding graph, feedback state, and structural refresh. The service owns search rules and data access. If two component handles must coordinate, keep the page wiring declarative, such as subscribing one component's public event to another component's public refresh action; move orchestration with domain meaning into a service. + +### Component Lifetime + +Create component state during each page build unless sharing is deliberate. A module-global component dataclass can leak UI state across clients, and a module-global `@ui.refreshable` function can refresh every recorded target. Do not retain returned handles beyond their owning client without an explicit cleanup and stale-client policy. + +Bindings to elements are removed with NiceGUI's element lifecycle. Refreshing a region deletes and recreates the elements inside that region, so external code should retain the component handle rather than private child element references. Component-owned subscriptions, timers, and background tasks must follow the client deletion rules in [interaction mechanics](./interaction-patterns.md#page-and-client-lifetime). + ## Optional Persistence Use only when the product requires durable data. diff --git a/src/personal_mcp/docs/skills/nicegui/references/binding-dataclasses.md b/src/personal_mcp/docs/skills/nicegui/references/binding-dataclasses.md index bc6f66b..24a02ed 100644 --- a/src/personal_mcp/docs/skills/nicegui/references/binding-dataclasses.md +++ b/src/personal_mcp/docs/skills/nicegui/references/binding-dataclasses.md @@ -51,6 +51,24 @@ class SearchState: A bound field omitted from `bindable_fields` still works, but NiceGUI must treat it as an active link and poll it for changes. +## Bindable Dataclasses As Component Handles + +A reusable NiceGUI component can expose a bindable dataclass as its typed public handle. The component's render function creates the dataclass instance, builds the element subtree, establishes bindings against that instance, and returns it to the page. This keeps the page at the composition level while the component owns its field wiring and internal elements. See the complete [reusable component contract](./architecture.md#reusable-component-contract). + +Choose binding direction according to what the public field represents: + +| Component field | Typical element relationship | Binding surface | +| --- | --- | --- | +| editable value such as `query` | control and component share the value | `element.bind_value(handle, "query")` | +| rendered status such as `busy` or `count` | component state drives text, visibility, or enabled state | `bind_*_from` with a pure transform when needed | +| browser proposal requiring validation | event handler validates before assignment | explicit callback, then assign the accepted bindable field | +| collection controlling child count or layout | component state is read while rebuilding a bounded subtree | `@ui.refreshable_method`, not a binding to the child list itself | +| injected service or callback | component implementation dependency | ordinary dataclass field omitted from `bindable_fields` | + +The returned handle should expose intentional component state and actions, not every child element. Bindable fields are effective for stable element properties because assignments propagate immediately. They do not create or delete elements when collection structure changes; a component-owned refreshable method should rebuild that region after the authoritative field is replaced. The target and instance behavior is defined under [refreshable component regions](./interaction-patterns.md#refreshable-component-regions). + +Create one handle during each page build unless shared state is deliberate. A module-global bindable component model propagates across clients that bind to it, just as a module-level refreshable function can own targets from multiple clients. + ## 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. diff --git a/src/personal_mcp/docs/skills/nicegui/references/interaction-patterns.md b/src/personal_mcp/docs/skills/nicegui/references/interaction-patterns.md index 6492b20..5ebb868 100644 --- a/src/personal_mcp/docs/skills/nicegui/references/interaction-patterns.md +++ b/src/personal_mcp/docs/skills/nicegui/references/interaction-patterns.md @@ -146,7 +146,9 @@ uploader = ui.upload( Generate the durable storage name independently from `file.name`, keep user-uploaded active content off the application origin, and apply content-specific scanning before downstream parsers consume the file. Call `uploader.reset()` when the product should clear QUploader's client-side queue after a completed or abandoned operation. -## Element Updates And Refreshable Regions +## Refreshable Component Regions + +The reusable [component factory pattern](./architecture.md#reusable-component-contract) combines stable bindable fields with bounded structural refreshes. Use bindings and setters while an existing element can represent the change; use a refreshable region when the number, type, order, or nesting of child elements must be rebuilt. Use the narrowest update mechanism that represents the change: @@ -157,18 +159,55 @@ Use the narrowest update mechanism that represents the change: | a bounded subtree whose structure changed | `@ui.refreshable` or `@ui.refreshable_method` | | navigation to a different page | `ui.navigate` or `ui.sub_pages` | -The tagged [`refreshable` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py) records every invocation as a target container. `refresh()` clears and recreates each matching target; it does not diff children. Arguments passed to `refresh()` replace prior positional arguments when non-empty and update prior keyword arguments. +The tagged [`refreshable` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py) records every invocation as a target containing a `RefreshableContainer`, the function arguments, and the associated object instance when applicable. The initial call both renders the region and registers that target. Calling `refresh()` before the decorated function or method has rendered does nothing because no target exists yet. -A module-level refreshable called by multiple clients has multiple targets, so refreshing it can update all surviving targets. Define the decorated function inside the page, create a page-local decorated wrapper, or use a per-page object with `@ui.refreshable_method` when clients need independent refresh behavior. +For each matching target, `refresh()` clears the container, updates its remembered arguments, and invokes the function again inside that same container. It recreates the subtree rather than diffing children. Bindings and event handlers owned by deleted elements follow normal element cleanup; a retained reference to a former child does not become the new child. Expose component state and public actions through the returned component handle instead of leaking refresh-owned element references. -For asynchronous refreshable functions: +### Function And Method Scope + +Choose the decorator according to state ownership: + +| Form | Target identity | Appropriate scope | +| --- | --- | --- | +| module-level `@ui.refreshable` | every surviving call target of that decorated function | deliberate multicast or shared rendering | +| page-local `@ui.refreshable` | calls recorded by the function created during that page build | one page client | +| page-created `ui.refreshable(function)` | calls recorded by that decorated wrapper | one page client or component factory call | +| `@ui.refreshable_method` | targets whose recorded instance equals the accessed object | reusable component instances with independent state | + +A module-level refreshable called by multiple clients has multiple targets, so one refresh can update every surviving target. The official [global and local scope examples](https://nicegui.io/documentation/refreshable#global_scope) demonstrate this distinction. For reusable components returned as dataclass handles, prefer a page-created instance with `@ui.refreshable_method`; NiceGUI's tagged [multi-instance tests](https://github.com/zauberzeug/nicegui/blob/v3.16.0/tests/test_refreshable.py) verify that refreshing one instance selects its own targets. + +Calling the same refreshable function more than once creates more than one target. For `@ui.refreshable_method`, every call made on the same instance belongs to that instance, so `instance.region.refresh()` refreshes all surviving targets for that method and instance. Use separate methods or separate component instances when independently refreshing two regions is required. + +### Arguments And Return Behavior + +Targets remember their initial positional and keyword arguments: + +- no refresh arguments reuse all remembered values +- non-empty positional refresh arguments replace the remembered positional tuple +- keyword refresh arguments update the remembered keyword dictionary +- arguments must remain consistently positional or keyword; supplying the same parameter through both paths raises `TypeError` +- the initial call and each refresh return the decorated function's normal result; the `refresh()` wrapper itself exposes NiceGUI's awaitable response behavior + +Parameters should describe render input, not hide durable state. On a reusable component, fields on the returned dataclass usually provide a clearer interface than repeatedly replacing a long refresh argument list. + +### Async Refresh + +An async refreshable's initial invocation returns its coroutine and should be awaited when page construction depends on its output. For subsequent refreshes: - `await region.refresh()` waits for all matching async refreshes to finish - calling `region.refresh()` without awaiting schedules the async work in the background - awaiting is appropriate when a button must remain disabled until rendering completes - each refresh clears the old target before the new async render finishes, so provide a stable outer loading surface when an empty interval would be disruptive -`ui.state()` is local storage indexed by call order inside one refreshable target. It can only be called inside a refreshable function, and conditional changes to state-call order can associate values with the wrong logical state. Use typed page state or bindable dataclasses when state identity must remain explicit. +Multiple matching targets are refreshed together; awaiting waits for all async results through `asyncio.gather`. That coordinates completion but does not serialize competing refresh calls. Apply the generation, lock, or coalescing policy described under [concurrency and feedback state](#concurrency-and-feedback-state) when two operations can refresh the same target concurrently. + +### Target And Local-State Lifetime + +Before every invocation or refresh, NiceGUI prunes targets whose container was deleted. Clearing an ancestor, navigating away, deleting the client, or replacing an outer refreshable region can therefore remove an inner target. A later call to the inner region's `refresh()` cannot recreate a pruned outer placement; the owning outer render must invoke it again. + +`ui.state()` stores values in a list owned by one refreshable target and identifies each value by call order. Its setter automatically refreshes the associated instance target. Conditional or reordered `ui.state()` calls can associate stored values with a different logical variable, so keep their call order stable. + +For reusable application components, a bindable dataclass is usually the clearer state owner: fields have explicit names, can bind directly to stable elements, and remain available to the page through the returned handle. Reserve `ui.state()` for small render-local values that do not need a typed component API, cross-component coordination, service persistence, or independent tests. ## Timers And Application Events