diff --git a/examples/session_storage/main.py b/examples/session_storage/main.py index 7f033da63..fa8d0e026 100644 --- a/examples/session_storage/main.py +++ b/examples/session_storage/main.py @@ -7,7 +7,7 @@ from nicegui.single_page import SinglePageRouter -@page('/') +@page('/', title="Welcome!") def index(): username = app.storage.session.get('username', '') if username == '': # redirect to login page @@ -19,7 +19,7 @@ def index(): ui.link('Logout', '/logout') -@page('/login') +@page('/login', title="Login") def login_page(): def login(): fake_pw_dict = {'user1': 'pw1', @@ -45,7 +45,7 @@ def handle_login(): ui.html("Psst... try user1/pw1, user2/pw2, user3/pw3") -@page('/logout') +@page('/logout', title="Logout") def logout(): app.storage.session['username'] = '' app.storage.session['password'] = '' diff --git a/nicegui/client.py b/nicegui/client.py index 5641f55eb..e25214be1 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -32,6 +32,9 @@ class Client: page_routes: Dict[Callable[..., Any], str] = {} """Maps page builders to their routes.""" + page_configs: Dict[Callable[..., Any], "page"] = {} + """Maps page builders to their page configuration.""" + single_page_routes: Dict[str, Any] = {} """Maps paths to the associated single page routers.""" diff --git a/nicegui/page.py b/nicegui/page.py index 08a089df0..49da96fa6 100644 --- a/nicegui/page.py +++ b/nicegui/page.py @@ -134,5 +134,5 @@ async def wait_for_result() -> None: self.api_router.get(self._path, **self.kwargs)(decorated) Client.page_routes[func] = self.path - func.__setattr__("__ng_page", self) + Client.page_configs[func] = self return func diff --git a/nicegui/single_page.py b/nicegui/single_page.py index ac834fe78..261046549 100644 --- a/nicegui/single_page.py +++ b/nicegui/single_page.py @@ -1,38 +1,50 @@ -import asyncio -import inspect from typing import Callable, Dict, Union from fastapi.routing import APIRoute from nicegui import background_tasks, helpers, ui, core, Client, app -from nicegui.app import AppConfig class RouterFrame(ui.element, component='single_page.js'): + """The RouterFrame is a special element which is used by the SinglePageRouter to exchange the content of the + current page with the content of the new page. It serves as container and overrides the browser's history + management to prevent the browser from reloading the whole page.""" + def __init__(self, base_path: str): + """ + :param base_path: The base path of the single page router which shall be tracked (e.g. when clicking on links) + """ super().__init__() self._props["base_path"] = base_path class SinglePageRouter: + """The SinglePageRouter allows the development of a Single Page Application (SPA) which maintains a + persistent connection to the server and only updates the content of the page instead of reloading the whole page. + + This enables the development of complex web applications with large amounts of per-user (per browser tab) data + which is kept alive for the duration of the connection.""" def __init__(self, path: str, **kwargs) -> None: + """ + :param path: the base path of the single page router. + """ super().__init__() self.routes: Dict[str, Callable] = {} - # async access lock self.base_path = path - self.find_api_routes() + self._find_api_routes() @ui.page(path, **kwargs) @ui.page(f'{path}' + '{_:path}', **kwargs) # all other pages async def root_page(client: Client): - await client.connected() if app.storage.session.get('__pageContent', None) is None: content: Union[ui.element, None] = RouterFrame(self.base_path).on('open', lambda e: self.open(e.args)) app.storage.session['__pageContent'] = content - def find_api_routes(self): + def _find_api_routes(self): + """Find all API routes already defined via the @page decorator, remove them and redirect them to the + single page router""" page_routes = set() for key, route in Client.page_routes.items(): if (route.startswith(self.base_path) and @@ -46,14 +58,19 @@ def find_api_routes(self): if route.path in page_routes: core.app.routes.remove(route) - def add(self, path: str): - def decorator(func: Callable): - self.routes[path] = func - return func + def add(self, path: str, builder: Callable) -> None: + """Add a new route to the single page router - return decorator + :param path: the path of the route + :param builder: the builder function""" + self.routes[path] = builder def open(self, target: Union[Callable, str], server_side=False) -> None: + """Open a new page in the browser by exchanging the content of the root page's slot element + + :param target: the target route or builder function + :param server_side: Defines if the call is made from the server side and should be pushed to the browser + history""" if isinstance(target, Callable): target = {v: k for k, v in self.routes.items()}[target] builder = target @@ -62,9 +79,9 @@ def open(self, target: Union[Callable, str], server_side=False) -> None: return builder = self.routes[target] - if "__ng_page" in builder.__dict__: - new_page = builder.__dict__["__ng_page"] - title = new_page.title + page_config = Client.page_configs.get(builder, None) + if page_config is not None: # if page was decorated w/ title, favicon etc. + title = page_config.title ui.run_javascript(f"document.title = '{title if title is not None else core.app.config.title}'") if server_side: