tabbed spa reference page
This commit is contained in:
@@ -29,6 +29,7 @@ Use this skill to choose the smallest supporting reference for a NiceGUI task. T
|
|||||||
| Customize `ui.table` or QTable columns, formatting, classes, props, responsive density, toolbar controls, visible columns, empty states, named slots, or frontend methods | [table customization](./references/table-customization.md) | Add [editable tables](./references/tables.md) only when cells also accept server-authoritative edits. |
|
| Customize `ui.table` or QTable columns, formatting, classes, props, responsive density, toolbar controls, visible columns, empty states, named slots, or frontend methods | [table customization](./references/table-customization.md) | Add [editable tables](./references/tables.md) only when cells also accept server-authoritative edits. |
|
||||||
| 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. |
|
| 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 `@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. |
|
| 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. |
|
||||||
|
| Build or explain URL-backed tabs, persistent tab panels, `ui.sub_pages` route adapters, browser-history synchronization, or parameterized routes that share one tab | [URL-backed tabs with sub pages](./references/tabbed-subpages.md) | Add [binding dataclasses](./references/binding-dataclasses.md) only when the route-backed state grows beyond the single field shown in the example. |
|
||||||
| 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. |
|
| 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. |
|
| 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. |
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ Load an example only when its exact mechanic matches the task:
|
|||||||
- [select events](./examples/select_events.py): `on_change`, generic Quasar events, `update:model-value`, browser-to-Python payload forwarding, and programmatic value changes.
|
- [select events](./examples/select_events.py): `on_change`, generic Quasar events, `update:model-value`, browser-to-Python payload forwarding, and programmatic value changes.
|
||||||
- [table customization](./examples/table_customization.py): raw-value sorting with cosmetic prefix, suffix, and datetime formatting; dynamic classes; QTable props; toolbar and cell slots; filtering; visible columns; and empty states.
|
- [table customization](./examples/table_customization.py): raw-value sorting with cosmetic prefix, suffix, and datetime formatting; dynamic classes; QTable props; toolbar and cell slots; filtering; visible columns; and empty states.
|
||||||
- [editable table](./examples/editable_table.py): dataframe-to-row state, named QTable cell slots, controlled editors, dialog-based whole-row save/cancel edits, Python validation, touched rows, and canonical row refresh.
|
- [editable table](./examples/editable_table.py): dataframe-to-row state, named QTable cell slots, controlled editors, dialog-based whole-row save/cancel edits, Python validation, touched rows, and canonical row refresh.
|
||||||
|
- [tabbed sub-pages](./examples/tab_spa.py): a persistent shell with URL-backed tabs, tab panels, browser-history navigation, and retained state for a parameterized report route. See the [reference explanation](./references/tabbed-subpages.md) for the ownership model and behavioral boundaries.
|
||||||
|
|
||||||
## Defaults That Span References
|
## Defaults That Span References
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# URL-Backed Tabs with Sub Pages
|
||||||
|
|
||||||
|
The [`tab_spa.py` example](../examples/tab_spa.py) combines [NiceGUI tabs](https://nicegui.io/documentation/tabs) with [sub-page routing](https://nicegui.io/documentation/sub_pages). It keeps one application shell and one set of tab panels mounted while the browser URL identifies the active view.
|
||||||
|
|
||||||
|
The example targets NiceGUI `3.16.0`. In this design, tabs are the visible navigation and content mechanism; `ui.sub_pages` is a URL-matching adapter whose route builders update the tab state instead of rendering route content inside the router.
|
||||||
|
|
||||||
|
## Responsibility Map
|
||||||
|
|
||||||
|
| Surface | Responsibility |
|
||||||
|
| --- | --- |
|
||||||
|
| `root()` | Creates one client-local shell, reads the initial URL, and connects the tabs, panels, and router. |
|
||||||
|
| `ui.tabs` | Holds the selected tab name and emits user selection changes. |
|
||||||
|
| `ui.tab_panels` | Displays the panel whose name matches the selected tab. |
|
||||||
|
| `ui.sub_pages` | Matches URL paths, extracts route parameters, and invokes the corresponding route callback without a full page reload. |
|
||||||
|
| `NavigationState` | Retains the concrete report path represented by the shared `reports` tab. |
|
||||||
|
| `ui.navigate.to()` | Changes the browser location so the sub-pages router can resolve the destination. |
|
||||||
|
|
||||||
|
The separation matters because a tab name is not always a URL. Static tabs use their route as their name, but every `/reports/{report_id}` URL maps to the single `reports` tab and panel.
|
||||||
|
|
||||||
|
## Route and Panel Mapping
|
||||||
|
|
||||||
|
| Browser path | Tab value | Panel value | Route callback effect |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `/` | `/` | `/` | Selects the overview panel. |
|
||||||
|
| `/projects` | `/projects` | `/projects` | Selects the projects panel. |
|
||||||
|
| `/reports/a` | `reports` | `reports` | Stores `/reports/a` and selects the reports panel. |
|
||||||
|
| `/reports/b` | `reports` | `reports` | Stores `/reports/b` and selects the reports panel. |
|
||||||
|
| `/settings` | `/settings` | `/settings` | Selects the settings panel. |
|
||||||
|
|
||||||
|
`tab_name_for_route()` is the translation boundary. It preserves static route names, collapses concrete report routes to `reports`, and returns `/` for other paths.
|
||||||
|
|
||||||
|
## Initial Page Construction
|
||||||
|
|
||||||
|
`root()` reads `ui.context.client.sub_pages_router.current_path` before creating the navigation controls. `normalize_route()` removes query strings, fragments, and trailing slashes so `/projects/` and `/projects` select the same tab.
|
||||||
|
|
||||||
|
For a direct request to `/reports/b`, the initial route produces two values:
|
||||||
|
|
||||||
|
- `active_report_path` becomes `/reports/b`.
|
||||||
|
- the selected tab and panel become `reports`.
|
||||||
|
|
||||||
|
The initial `tabs.set_value(...)` call occurs before `tabs.on_value_change(navigate)` is registered. Initial selection therefore establishes the shell state without treating page construction as a user navigation. Passing the same initial tab value to `ui.tab_panels` aligns the content container with the tabs from the first render.
|
||||||
|
|
||||||
|
All panel builders run during shell construction. Switching tabs changes the selected panel; it does not rerun `overview_page()`, `projects_page()`, `report_page()`, or `settings_page()`. Their element state remains client-local for the lifetime of that shell.
|
||||||
|
|
||||||
|
## Tab-Originated Navigation
|
||||||
|
|
||||||
|
The tab change handler receives the selected tab name. Static tab names are already destinations. The reports tab resolves through `state.active_report_path`, which supplies the last concrete report URL:
|
||||||
|
|
||||||
|
```python
|
||||||
|
destination = state.active_report_path if tabname == REPORTS_TAB else tabname
|
||||||
|
ui.navigate.to(destination)
|
||||||
|
```
|
||||||
|
|
||||||
|
[`ui.navigate.to()`](https://nicegui.io/documentation/navigate) performs the route transition. The sub-pages router then matches the new location and invokes a callback that selects the corresponding tab. Because the panel container is associated with `tabs`, the visible panel follows that selected value.
|
||||||
|
|
||||||
|
## URL-Originated Navigation
|
||||||
|
|
||||||
|
The route callbacks contain no page markup. They translate router matches back into the visible state:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def route_reports(report_id: str) -> None:
|
||||||
|
state.active_report_path = f"/reports/{report_id}"
|
||||||
|
tabs.set_value(REPORTS_TAB)
|
||||||
|
```
|
||||||
|
|
||||||
|
This direction handles direct links and browser back or forward navigation. A URL such as `/reports/b` supplies `report_id="b"`; the callback reconstructs the normalized concrete path, updates report-bound labels through `NavigationState`, and selects the shared reports panel.
|
||||||
|
|
||||||
|
The router element is hidden because it is not the content container in this example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ui.sub_pages(routes).classes("hidden")
|
||||||
|
```
|
||||||
|
|
||||||
|
Normally, `ui.sub_pages` clears and rebuilds its own children when a route changes. Here its builders only mutate state outside that container, so hiding the empty routing element does not hide the tab-panel content.
|
||||||
|
|
||||||
|
## Parameterized Report State
|
||||||
|
|
||||||
|
`NavigationState.active_report_path` separates tab identity from route identity. The reports tab always has the stable value `reports`, while the state records `/reports/a`, `/reports/b`, or another matched report route.
|
||||||
|
|
||||||
|
This provides two forms of continuity:
|
||||||
|
|
||||||
|
- A direct report URL selects the correct tab and report during initial construction.
|
||||||
|
- Leaving the reports tab and selecting it again during the same client lifetime returns to the last visited report.
|
||||||
|
|
||||||
|
The state is page-local, not durable storage. Reloading a non-report URL creates a new `NavigationState` and restores `DEFAULT_REPORT_PATH`. Shareable report identity remains durable because report pages encode it in the URL.
|
||||||
|
|
||||||
|
## Behavioral Boundaries
|
||||||
|
|
||||||
|
- `TAB_ROUTES` contains concrete routes whose path and tab identity are the same. Parameterized route families require a stable synthetic tab name such as `reports`.
|
||||||
|
- `normalize_route()` intentionally ignores query parameters and fragments for tab selection. Route callbacks would need matching parameters if those values affected panel state.
|
||||||
|
- The hidden router also hides its built-in 404 output. As written, an unmatched path selects the overview panel through `tab_name_for_route()` while the router's not-found content remains invisible.
|
||||||
|
- Panels are mounted together, so expensive panel construction still occurs during the initial shell build. Lazy or route-specific construction requires a different content ownership model.
|
||||||
|
- The pattern preserves the shell only for navigation handled by the current `ui.sub_pages` router. A full reload creates a new client and rebuilds all page-local state.
|
||||||
|
|
||||||
|
## Source Index
|
||||||
|
|
||||||
|
!!! info "NiceGUI sources"
|
||||||
|
- [Sub pages and URL parameters](https://nicegui.io/documentation/sub_pages)
|
||||||
|
- [Tabs, tab names, and tab panels](https://nicegui.io/documentation/tabs)
|
||||||
|
- [Navigation and browser history](https://nicegui.io/documentation/navigate)
|
||||||
|
- [NiceGUI `3.16.0` sub-pages implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/sub_pages.py)
|
||||||
Reference in New Issue
Block a user