using tab panels in the spa

This commit is contained in:
John Lancaster
2026-09-03 23:45:10 -05:00
parent 86c7d54244
commit 09d2a4bcaf
@@ -7,20 +7,22 @@
"""Demonstrate URL-backed tabs with persistent parameterized-route state."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from urllib.parse import urlsplit
from nicegui import binding
from nicegui import events
from nicegui import ui
from nicegui.elements.tabs import Tab
from nicegui.elements.tabs import TabPanel
from nicegui.sub_pages_router import SubPagesRouter
type PageBuilder = Callable[..., None]
DEFAULT_REPORT_PATH = "/reports/a"
REPORTS_TAB = "reports"
TAB_ROUTES = frozenset({"/", "/projects", "/settings"})
def page_heading(title: str, description: str) -> None:
@@ -70,14 +72,24 @@ def projects_page() -> None:
ui.badge(status, color=color)
def report_page(report_id: str) -> None:
"""Render the report selected by the route parameter."""
def report_page(state: NavigationState) -> None:
"""Render report content bound to the active route parameter."""
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"):
page_heading(f"Report {report_id.upper()}", "The report ID is injected from the URL path.")
with ui.column().classes("w-full gap-1"):
ui.label().bind_text_from(
state,
"active_report_path",
backward=lambda path: f"Report {report_id_from_path(path).upper()}",
).classes("text-3xl font-semibold text-stone-900")
ui.label("The report ID is injected from the URL path.").classes("text-base text-stone-600")
with ui.card().classes("w-full max-w-2xl p-5 gap-4"):
ui.label("Parameterized route").classes("text-xl font-semibold text-stone-900")
ui.label(f"Loaded /reports/{report_id}").classes("text-stone-600")
ui.label().bind_text_from(
state,
"active_report_path",
backward=lambda path: f"Loaded {path}",
).classes("text-stone-600")
with ui.row().classes("gap-2"):
ui.button("Report A", on_click=lambda: ui.navigate.to("/reports/a")).props("outline")
ui.button("Report B", on_click=lambda: ui.navigate.to("/reports/b")).props("outline")
@@ -104,10 +116,10 @@ def tab_name_for_route(route: str) -> str:
"""Return the tab name associated with a concrete route."""
if route.startswith("/reports/"):
return REPORTS_TAB
return route if route in ROUTES else "/"
return route if route in TAB_ROUTES else "/"
@dataclass(slots=True)
@binding.bindable_dataclass
class NavigationState:
"""Store client-local navigation state for parameterized tabs."""
@@ -117,12 +129,13 @@ class NavigationState:
type TabHandler = events.ValueChangeEventArguments[str | Tab | TabPanel | None]
def create_tabs(router: SubPagesRouter) -> ui.tabs:
def report_id_from_path(path: str) -> str:
"""Extract the report ID from a normalized report route."""
return path.rsplit("/", maxsplit=1)[-1]
def create_tabs(state: NavigationState, initial_route: str) -> ui.tabs:
"""Create route-aware tabs and retain the last selected report."""
state = NavigationState()
initial_route = normalize_route(router.current_path)
if tab_name_for_route(initial_route) == REPORTS_TAB:
state.active_report_path = initial_route
def navigate(event: TabHandler) -> None:
"""Navigate to the route represented by the selected tab."""
@@ -142,36 +155,55 @@ def create_tabs(router: SubPagesRouter) -> ui.tabs:
tabs.set_value(tab_name_for_route(initial_route))
tabs.on_value_change(navigate)
def sync_tab(path: str) -> None:
"""Synchronize tab and report state after the route changes."""
route = normalize_route(path)
tab_name = tab_name_for_route(route)
if tab_name == REPORTS_TAB:
state.active_report_path = route
tabs.set_value(tab_name)
router.on_path_changed(sync_tab)
return tabs
ROUTES: dict[str, PageBuilder] = {
"/": overview_page,
"/projects": projects_page,
"/reports/{report_id}": report_page,
"/settings": settings_page,
}
def render_tab_panels(tabs: ui.tabs, state: NavigationState, active_tab: str) -> None:
"""Render all tabbed page content inside a tab panels container."""
with ui.tab_panels(tabs, value=active_tab, animated=True).classes("w-full"):
with ui.tab_panel("/"):
overview_page()
with ui.tab_panel("/projects"):
projects_page()
with ui.tab_panel(REPORTS_TAB):
report_page(state)
with ui.tab_panel("/settings"):
settings_page()
def root() -> None:
"""Build the persistent application shell and sub-page container."""
router = ui.context.client.sub_pages_router
initial_route = normalize_route(ui.context.client.sub_pages_router.current_path)
state = NavigationState()
if tab_name_for_route(initial_route) == REPORTS_TAB:
state.active_report_path = initial_route
with ui.header(elevated=True).classes("py-0 items-center"):
create_tabs(router)
tabs = create_tabs(state, initial_route)
ui.button(icon="settings").classes("text-white").props("round flat").tooltip("Settings")
ui.sub_pages(ROUTES).classes("w-full")
render_tab_panels(tabs, state, tab_name_for_route(initial_route))
def route_overview() -> None:
tabs.set_value("/")
def route_projects() -> None:
tabs.set_value("/projects")
def route_reports(report_id: str) -> None:
state.active_report_path = f"/reports/{report_id}"
tabs.set_value(REPORTS_TAB)
def route_settings() -> None:
tabs.set_value("/settings")
routes: dict[str, PageBuilder] = {
"/": route_overview,
"/projects": route_projects,
"/reports/{report_id}": route_reports,
"/settings": route_settings,
}
ui.sub_pages(routes).classes("hidden")
if __name__ in {"__main__", "__mp_main__"}: