using tab panels in the spa
This commit is contained in:
@@ -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__"}:
|
||||
|
||||
Reference in New Issue
Block a user