using tab panels in the spa
This commit is contained in:
@@ -7,20 +7,22 @@
|
|||||||
|
|
||||||
"""Demonstrate URL-backed tabs with persistent parameterized-route state."""
|
"""Demonstrate URL-backed tabs with persistent parameterized-route state."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
from nicegui import binding
|
||||||
from nicegui import events
|
from nicegui import events
|
||||||
from nicegui import ui
|
from nicegui import ui
|
||||||
from nicegui.elements.tabs import Tab
|
from nicegui.elements.tabs import Tab
|
||||||
from nicegui.elements.tabs import TabPanel
|
from nicegui.elements.tabs import TabPanel
|
||||||
from nicegui.sub_pages_router import SubPagesRouter
|
|
||||||
|
|
||||||
type PageBuilder = Callable[..., None]
|
type PageBuilder = Callable[..., None]
|
||||||
|
|
||||||
DEFAULT_REPORT_PATH = "/reports/a"
|
DEFAULT_REPORT_PATH = "/reports/a"
|
||||||
REPORTS_TAB = "reports"
|
REPORTS_TAB = "reports"
|
||||||
|
TAB_ROUTES = frozenset({"/", "/projects", "/settings"})
|
||||||
|
|
||||||
|
|
||||||
def page_heading(title: str, description: str) -> None:
|
def page_heading(title: str, description: str) -> None:
|
||||||
@@ -70,14 +72,24 @@ def projects_page() -> None:
|
|||||||
ui.badge(status, color=color)
|
ui.badge(status, color=color)
|
||||||
|
|
||||||
|
|
||||||
def report_page(report_id: str) -> None:
|
def report_page(state: NavigationState) -> None:
|
||||||
"""Render the report selected by the route parameter."""
|
"""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"):
|
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"):
|
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("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"):
|
with ui.row().classes("gap-2"):
|
||||||
ui.button("Report A", on_click=lambda: ui.navigate.to("/reports/a")).props("outline")
|
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")
|
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."""
|
"""Return the tab name associated with a concrete route."""
|
||||||
if route.startswith("/reports/"):
|
if route.startswith("/reports/"):
|
||||||
return REPORTS_TAB
|
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:
|
class NavigationState:
|
||||||
"""Store client-local navigation state for parameterized tabs."""
|
"""Store client-local navigation state for parameterized tabs."""
|
||||||
|
|
||||||
@@ -117,12 +129,13 @@ class NavigationState:
|
|||||||
type TabHandler = events.ValueChangeEventArguments[str | Tab | TabPanel | None]
|
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."""
|
"""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:
|
def navigate(event: TabHandler) -> None:
|
||||||
"""Navigate to the route represented by the selected tab."""
|
"""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.set_value(tab_name_for_route(initial_route))
|
||||||
tabs.on_value_change(navigate)
|
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
|
return tabs
|
||||||
|
|
||||||
|
|
||||||
ROUTES: dict[str, PageBuilder] = {
|
def render_tab_panels(tabs: ui.tabs, state: NavigationState, active_tab: str) -> None:
|
||||||
"/": overview_page,
|
"""Render all tabbed page content inside a tab panels container."""
|
||||||
"/projects": projects_page,
|
with ui.tab_panels(tabs, value=active_tab, animated=True).classes("w-full"):
|
||||||
"/reports/{report_id}": report_page,
|
with ui.tab_panel("/"):
|
||||||
"/settings": settings_page,
|
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:
|
def root() -> None:
|
||||||
"""Build the persistent application shell and sub-page container."""
|
"""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"):
|
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.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__"}:
|
if __name__ in {"__main__", "__mp_main__"}:
|
||||||
|
|||||||
Reference in New Issue
Block a user