nicegui component pattern
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user