expanded other pages

This commit is contained in:
John Lancaster
2026-08-30 09:19:54 -05:00
parent 12f916455b
commit f6752313be
3 changed files with 540 additions and 262 deletions
@@ -1,110 +1,265 @@
# Interaction Patterns Reference
# NiceGUI Interaction Mechanics
## Reactive State
Use this reference for user-driven and live application behavior: page and client lifetime, value validation, form submission, uploads, explicit refreshes, timers, application events, background execution, and server-pushed updates. Component-specific event names, scoped event payloads, and Quasar model contracts are covered in [component mechanics](./component-mechanics.md). Binding graph behavior and typed projections are covered in [binding dataclasses](./binding-dataclasses.md).
Use bindable dataclasses for local page state.
The NiceGUI implementation details below are verified against NiceGUI `3.16.0`. FastAPI's native `EventSourceResponse` and `ServerSentEvent` APIs require FastAPI `0.135.0` or later. Check the target application's pinned versions before depending on those surfaces.
## Interaction Boundary Map
| Boundary | Owns | Does not own |
| --- | --- | --- |
| NiceGUI element | browser-facing value, enabled state, validation display, and registered UI callbacks | domain authorization, durable persistence, or cross-worker coordination |
| Page `Client` | one page visit's elements, UI context, socket connection, outbox, and client-scoped storage | durable user identity or shared application state |
| Page state | current filters, drafts, selections, busy flags, and serializable projections | database transactions or durable job state |
| Service or repository | domain validation, authorization, transactions, idempotency, and persistence | direct creation or mutation of NiceGUI elements |
| NiceGUI task utility | scheduling work in the event loop, a thread, or a process | durable delivery after process failure |
| FastAPI route | HTTP, SSE, or custom WebSocket protocol and authentication boundary | automatic synchronization with NiceGUI elements |
NiceGUI already uses a Socket.IO connection to carry element events and server updates for each client. Ordinary page interactions should use component callbacks, bindings, `Event`, and element updates rather than introducing a second transport.
## Page And Client Lifetime
A [`@ui.page`](https://nicegui.io/documentation/page) builder creates a private `Client` and element tree for each page visit. During initial page construction, Python can create elements before the browser socket exists. Code that requires JavaScript, tab storage, or post-response work must first await `ui.context.client.connected()`.
The tagged [`page` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/page.py) distinguishes two phases:
1. Before connection, the page builder must produce the initial response within `response_timeout`, which defaults to three seconds.
2. Once `connected()` is awaited, NiceGUI can send the initial HTML immediately and let the remaining async builder continue with a live client.
Long service calls should not delay initial page construction. Render a stable loading state, await the connection where necessary, then perform the asynchronous work and update or refresh the bounded result region.
### Disconnect, Reconnect, And Delete
The tagged [`Client` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py) treats a transient socket disconnect differently from client deletion:
- `on_disconnect` runs whenever the socket disconnects, including interruptions followed by reconnection.
- NiceGUI keeps the client alive for the page's `reconnect_timeout`.
- A successful handshake within that window cancels pending deletion.
- `on_delete` runs only when the client is actually removed after the reconnect window or explicit cleanup.
- Deletion removes the client's elements and bindings and stops its outbox.
Use `on_disconnect` for connection telemetry and reversible transport state. Use `on_delete` to release resources owned by the page visit. Do not close a page-owned resource on every disconnect if it must survive a short reconnect.
NiceGUI's tagged [`Outbox`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/outbox.py) retains recent messages according to `message_history_length` and the reconnect window. A reconnecting client supplies its next expected message ID; NiceGUI replays retained messages or reloads the page when the required history is unavailable. Message replay is transport recovery, not a durable event log or a substitute for idempotent service operations.
### State Scope
[`app.storage`](https://nicegui.io/documentation/storage) offers scopes with different navigation and process lifetimes:
| Scope | Shared with | Survives page navigation or reload | Persistence notes |
| --- | --- | --- | --- |
| `client` | current page visit only | no | server memory; appropriate for short-lived page resources |
| `tab` | current browser tab | yes | server memory by default; requires an established connection |
| `user` | tabs carrying the same signed session ID | yes | server-side persistent dictionary; requires `storage_secret` |
| `browser` | tabs sharing the session cookie | yes | cookie payload; writable only before the response is built; prefer `user` for most data |
| `general` | all users in the process or configured backend | yes | shared persistent dictionary; not a per-user boundary |
The tagged [`storage` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/storage.py) stores general and user data in local JSON files by default or Redis when configured. Tab storage is process memory unless Redis is configured. Multiple workers therefore require an explicitly shared backend for state that must cross processes.
## Values, Validation, And Submission
NiceGUI value elements mirror browser changes into Python and then invoke `on_change` or `on_value_change` handlers. For text input, [`ui.input`](https://nicegui.io/documentation/input) sends `on_change` on each value change unless a Quasar `debounce` prop delays the model update. Use an enter, blur, or explicit submit event when every keystroke should not trigger application work.
The tagged [`ValidationElement`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/validation_element.py) implements NiceGUI's Python validation:
- a callable returns an error string or `None`
- a dictionary maps error strings to predicates and stops at the first failed predicate
- automatic validation runs after each handled value change unless `without_auto_validation()` is set
- `validate()` updates the element's `error` and `error-message` props
- asynchronous validation runs as a background task; `validate(return_result=True)` is not supported for an async validator
NiceGUI validation is suitable for field feedback, but a submit operation still needs service-level validation and authorization. Browser values, client-side Quasar rules, file metadata, and hidden or disabled controls are not trust boundaries.
NiceGUI does not require a transport-level HTML form for ordinary page submission: current element values already exist in Python. A submit handler can validate relevant fields, construct an immutable command or DTO, call the service boundary, and update the page from the accepted result. Clear draft state only after persistence succeeds.
```python
from dataclasses import field
from nicegui import binding, ui
async def submit() -> None:
if not all(field.validate() for field in (name, email)):
return
@binding.bindable_dataclass
class PageState:
selected_id: int | None = None
items: list = field(default_factory=list)
state = PageState()
ui.label().bind_text_from(state, "selected_id")
```
## File Upload Pattern
- Validate extension and size before storing.
- Delegate storage to a service method.
- Notify success and failure explicitly.
```python
async def handle_upload(e: ui.events.UploadEventArguments):
submit_button.disable()
try:
if e.size > 10 * 1024 * 1024:
raise ValueError("File too large")
if not e.name.endswith(".pdf"):
raise ValueError("Only PDF allowed")
await file_service.store(e.content.read(), e.name)
ui.notify(f"Uploaded: {e.name}", type="positive")
except ValueError as err:
ui.notify(str(err), type="negative")
ui.upload(on_upload=handle_upload, auto_upload=True)
user = await user_service.create(name=name.value, email=email.value)
ui.notify(f"Created {user.display_name}", type="positive")
name.set_value("")
email.set_value("")
except DuplicateEmailError:
email.error = "This email is already registered"
finally:
submit_button.enable()
```
## Form Submission Pattern
For asynchronous field validators, await the validator at the service boundary or maintain an explicit validation state; do not use the synchronous return value of `validate()` as proof that asynchronous validation completed.
- Bind UI inputs to dataclass fields.
- Perform validation in the service layer.
- Clear form state on success.
## Upload Mechanics
[`ui.upload`](https://nicegui.io/documentation/upload) wraps Quasar's `QUploader`. The tagged [`Upload` wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload.py) registers a POST route scoped to the current client and element. Its event order is:
1. `on_rejected` during browser-side file selection for Quasar restrictions.
2. `on_begin_upload` when the client starts a request.
3. `on_upload` once for each server-received file.
4. `on_multi_upload` after all files in that request have been converted.
`max_file_size`, `max_total_size`, `max_files`, and an `accept` prop improve client feedback, but NiceGUI's [security guidance](https://nicegui.io/documentation/section_security#examples_are_starting_points) identifies those restrictions as browser-side checks. Revalidate size, media type, content signature, filename policy, authorization, and storage quota on the server before persisting or parsing data.
In NiceGUI 3.16, `event.file` is a [`FileUpload`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload_files.py):
| Surface | Behavior |
| --- | --- |
| `name` | basename sanitized by NiceGUI; still untrusted display metadata |
| `content_type` | request-provided media type; not content verification |
| `size()` | synchronous byte count |
| `read()`, `text()`, `json()` | asynchronous full-content reads |
| `iterate(chunk_size=...)` | asynchronous chunks for bounded-memory processing |
| `save(path)` | asynchronous save to an application-selected path |
NiceGUI reads the incoming Starlette upload and keeps it in memory up to `MultiPartParser.spool_max_size`; larger files spill to a temporary file. This spool threshold controls memory versus disk, not the allowed upload size. Raising it increases per-upload memory pressure and should not be used as a validation mechanism.
```python
@binding.bindable_dataclass
class FormData:
name: str = ""
email: str = ""
from nicegui import events, ui
data = FormData()
ui.input("Name").bind_value(data, "name")
ui.input("Email").bind_value(data, "email")
async def on_submit():
async def handle_upload(event: events.UploadEventArguments) -> None:
file = event.file
if file.size() > 10 * 1024 * 1024:
ui.notify("File exceeds 10 MB", type="negative")
return
if file.content_type != "application/pdf":
ui.notify("Only PDF files are accepted", type="negative")
return
try:
await user_service.create_user(name=data.name, email=data.email)
ui.notify("User created", type="positive")
data.name = data.email = ""
except ValueError as err:
ui.notify(str(err), type="negative")
await file_service.store(chunks=file.iterate(), original_name=file.name)
except StorageQuotaError:
ui.notify("Storage quota exceeded", type="negative")
else:
ui.notify(f"Uploaded {file.name}", type="positive")
ui.button("Submit").on_click(on_submit)
uploader = ui.upload(
on_upload=handle_upload,
on_rejected=lambda: ui.notify("File rejected", type="negative"),
max_file_size=10 * 1024 * 1024,
auto_upload=True,
).props("accept=application/pdf")
```
## Real-Time Updates Decision
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.
Use SSE for one-way status streaming.
Use WebSocket for bidirectional messaging.
## Element Updates And Refreshable Regions
SSE endpoint example:
Use the narrowest update mechanism that represents the change:
```python
@app.get("/events/status")
async def status_stream():
async def gen():
while True:
yield f"data: {await get_status()}\\n\\n"
await asyncio.sleep(1)
return StreamingResponse(gen(), media_type="text/event-stream")
```
| Change | Appropriate surface |
| --- | --- |
| one wrapper property | setter, binding, or property assignment supported by that wrapper |
| mutated option or row collection | wrapper helper or explicit `element.update()` |
| a bounded subtree whose structure changed | `@ui.refreshable` or `@ui.refreshable_method` |
| navigation to a different page | `ui.navigate` or `ui.sub_pages` |
## Background Work Pattern
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.
- Start long jobs in FastAPI background tasks.
- Expose status via endpoint or streaming channel.
- Guard buttons against duplicate submissions during in-flight tasks.
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.
## Explicit Refresh Pattern
For asynchronous refreshable functions:
Use @ui.refreshable and call refresh intentionally instead of polling unrelated state.
- `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
```python
@ui.refreshable
async def item_list():
items = await service.list()
for item in items:
ui.label(item.name)
`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.
ui.button("Refresh").on_click(lambda: item_list.refresh())
```
## Timers And Application Events
## Links
[`ui.timer`](https://nicegui.io/documentation/timer) is client-scoped. Its tagged [element implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/timer.py) waits for the client connection and cancels the current invocation when the element is deleted. `app.timer` is application-scoped and has no UI context of its own.
!!! info "Primary sources"
- [NiceGUI action events](https://nicegui.io/documentation/section_action_events)
- [FastAPI server-sent events](https://fastapi.tiangolo.com/advanced/server-sent-events/)
- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
The tagged base [`Timer`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/timer.py) awaits each callback before scheduling the remainder of the interval, so one timer does not overlap its own invocations. A callback that takes longer than the interval causes the next iteration to begin without an additional delay. `deactivate()` pauses future invocations, while `cancel(with_current_invocation=True)` also cancels the current callback task and cannot be reversed.
Use timers for truly periodic observation, not to compensate for a missing event or explicit refresh. Polling intervals must account for query cost, number of connected clients, and process-local duplication under multiple workers.
[`Event`](https://nicegui.io/documentation/event) decouples long-lived Python producers from UI subscribers:
- `emit()` invokes subscribers without waiting for async callbacks to complete
- `call()` awaits all subscribers and propagates their failures to the caller
- `emitted(timeout=...)` waits for the next emission
- subscriptions created in a UI context are automatically removed when that client is deleted unless configured otherwise
The automatic unsubscribe behavior in the tagged [`Event` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/event.py) makes an application event suitable for connecting longer-lived models to page-local UI without retaining deleted clients. It remains process-local; use a broker or shared service for cross-worker fan-out.
## Execution Contexts
Choose an execution surface by workload and lifetime:
| Surface | Execution | Suitable for | Important constraint |
| --- | --- | --- | --- |
| async UI handler | event loop | non-blocking clients and short orchestration | blocking calls freeze all clients on that loop |
| `run.io_bound()` | shared thread pool | blocking file, HTTP, or SDK calls | cancellation does not necessarily stop the underlying thread operation |
| `run.cpu_bound()` | process pool | CPU-heavy pure computation | callable, arguments, result, and failures cross a pickle boundary |
| `background_tasks.create()` | event-loop task | detached async work owned by this process | canceled during shutdown unless tagged with `await_on_shutdown` |
| FastAPI `BackgroundTasks` | after an HTTP response | small route-triggered work | still belongs to the web process; not a durable queue |
| external worker or job queue | separate process or service | durable, retryable, resource-heavy jobs | requires explicit status, cancellation, and result contracts |
The tagged [`run` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/run.py) uses a thread pool for `io_bound` and a process pool for `cpu_bound`. For CPU work, prefer a module-level function with simple serializable arguments and return data rather than UI objects or closures. NiceGUI 3.16 inherits the platform multiprocessing start method unless `run.process_pool_start_method` is set before startup; `spawn` avoids unsafe fork behavior in a threaded process but does not inherit module state.
The tagged [`background_tasks` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/background_tasks.py) keeps strong references to running tasks, forwards unhandled exceptions to global exception handlers, and cancels ordinary tasks during shutdown. `create_lazy()` coalesces repeated work by name into the current run plus only the latest waiting coroutine; it is useful for refresh-style invalidation, not for work where every event must be processed.
## Live Update Transports
| Requirement | Default surface |
| --- | --- |
| update the initiating NiceGUI page | mutate elements or bound page state in its client context |
| notify all local clients of a page | iterate `app.clients(path)` and enter each `with client:` context |
| connect a long-lived Python producer to page subscribers | NiceGUI `Event` with page-local subscriptions |
| one-way HTTP event stream for an external/browser consumer | FastAPI SSE endpoint |
| custom bidirectional protocol independent of NiceGUI elements | FastAPI WebSocket endpoint |
| cross-worker or cross-instance broadcast | external broker plus a subscriber in each process |
FastAPI's [SSE support](https://fastapi.tiangolo.com/tutorial/server-sent-events/) uses a yielding route with `response_class=EventSourceResponse`. `ServerSentEvent` adds `event`, `id`, `retry`, and comment fields; event IDs support application-defined resume behavior through `Last-Event-ID`. FastAPI supplies keep-alive comments and headers that discourage proxy buffering and caching. The stream producer still owns authorization, disconnect-aware resource cleanup, replay semantics, and bounded buffering.
FastAPI [WebSockets](https://fastapi.tiangolo.com/advanced/websockets/) support text, bytes, and JSON in both directions. Catch `WebSocketDisconnect`, remove the connection from any local registry, and remember that an in-memory connection manager reaches only clients attached to the same process.
Do not use SSE or a custom WebSocket merely to update NiceGUI elements. Those transports do not automatically establish the target NiceGUI client context or synchronize its element tree.
## Concurrency And Feedback State
Disabling the initiating control communicates that work is active, but it is not a server-side concurrency guarantee. Also guard the handler or service with one of these policies:
- reject a second request while the operation is in flight
- coalesce duplicate refresh requests and keep only the latest invalidation
- serialize operations with a lock scoped to the affected entity or user
- make the service operation idempotent and return the existing result
For search, filtering, and other replaceable reads, an older request can complete after a newer request. Associate each request with a monotonically increasing generation or cancel the previous task, and only publish a result that still matches the current generation. Cancellation must still restore enabled/loading state in `finally`.
Every user-triggered asynchronous operation should expose a bounded state model such as `idle`, `running`, `succeeded`, `failed`, or `canceled`. Keep the error message near the action, preserve user input after expected failure, and do not convert unexpected programming errors into a generic success-like state.
## Source Index
!!! info "NiceGUI public documentation"
- [Pages and client connection](https://nicegui.io/documentation/page)
- [Action, events, execution, and error handling](https://nicegui.io/documentation/section_action_events)
- [Input and validation](https://nicegui.io/documentation/input)
- [Upload](https://nicegui.io/documentation/upload)
- [Refreshable UI](https://nicegui.io/documentation/refreshable)
- [Timer](https://nicegui.io/documentation/timer)
- [Application events](https://nicegui.io/documentation/event)
- [Storage scopes](https://nicegui.io/documentation/storage)
!!! info "NiceGUI `3.16.0` implementation"
- [Page builder and response phases](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/page.py)
- [Client lifecycle](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/client.py)
- [Outbox and reconnect replay](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/outbox.py)
- [Validation elements](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/validation_element.py)
- [Upload wrapper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload.py)
- [Uploaded-file storage and access](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/upload_files.py)
- [Refreshable targets and local state](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/functions/refreshable.py)
- [Timer scheduling](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/timer.py)
- [Application event dispatch](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/event.py)
- [Thread and process execution](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/run.py)
- [Background-task lifecycle](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/background_tasks.py)
!!! info "FastAPI transports and tasks"
- [Server-sent events](https://fastapi.tiangolo.com/tutorial/server-sent-events/)
- [WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
- [Response background tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/)