Skip to content

Commit

Permalink
Refactoring of outlet structure and URL resolving, backup
Browse files Browse the repository at this point in the history
  • Loading branch information
Alyxion committed Apr 14, 2024
1 parent 215e493 commit 3167521
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 50 deletions.
4 changes: 2 additions & 2 deletions examples/outlet/main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from nicegui import ui


@ui.outlet('/')
ui.outlet('/')
def spa1():
ui.label("spa1 header")
yield
ui.label("spa1 footer")


# SPA outlet routers can be defined side by side
@ui.outlet('/spa2')
ui.outlet('/spa2')
def spa2():
ui.label('spa2')
yield
Expand Down
11 changes: 7 additions & 4 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

if TYPE_CHECKING:
from .page import page
from .outlet import Outlet as outlet

templates = Jinja2Templates(Path(__file__).parent / 'templates')

Expand Down Expand Up @@ -81,7 +82,7 @@ def __init__(self, page: page, *, shared: bool = False) -> None:

self.page = page
self.storage = ObservableDict()
self.single_page_content = None
self.outlets: Dict[str, "outlet"] = {}

self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
Expand Down Expand Up @@ -226,9 +227,11 @@ async def send_and_wait():
def open(self, target: Union[Callable[..., Any], str], new_tab: bool = False) -> None:
"""Open a new page in the client."""
path = target if isinstance(target, str) else self.page_routes[target]
if path in self.single_page_routes and self.single_page_content is not None: # moving from SPR to SPR?
self.single_page_routes[path].navigate_to(target)
return
for cur_outlet in self.outlets.values():
target = cur_outlet.resolve_target(target)
if target.valid:
cur_outlet.navigate_to(path)
return
self.outbox.enqueue_message('open', {'path': path, 'new_tab': new_tab}, self.id)

def download(self, src: Union[str, bytes], filename: Optional[str] = None, media_type: str = '') -> None:
Expand Down
15 changes: 7 additions & 8 deletions nicegui/elements/router_frame.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Union, Callable, Tuple, Any, Optional, Self

from nicegui import ui, helpers, context, background_tasks, core
from nicegui.router_frame_url import SinglePageUrl
from nicegui.router_frame_url import SinglePageTarget


class RouterFrame(ui.element, component='router_frame.js'):
Expand All @@ -22,17 +22,17 @@ def __init__(self, valid_path_masks: list[str],
self._props['browser_history'] = use_browser_history
self.use_browser_history = use_browser_history
self.change_title = False
self._on_resolve: Optional[Callable[[Any], SinglePageUrl]] = None
self._on_resolve: Optional[Callable[[Any], SinglePageTarget]] = None
self.on('open', lambda e: self.navigate_to(e.args))

def on_resolve(self, on_resolve: Callable[[Any], SinglePageUrl]) -> Self:
def on_resolve(self, on_resolve: Callable[[Any], SinglePageTarget]) -> Self:
"""Set the on_resolve function which is used to resolve the target to a SinglePageUrl
:param on_resolve: The on_resolve function which receives a target object such as an URL or Callable and
returns a SinglePageUrl object."""
self._on_resolve = on_resolve
return self

def get_target_url(self, target: Any) -> SinglePageUrl:
def resolve_target(self, target: Any) -> SinglePageTarget:
if self._on_resolve is not None:
return self._on_resolve(target)
raise NotImplementedError
Expand All @@ -43,7 +43,7 @@ def navigate_to(self, target: Any, _server_side=False) -> None:
indicating whether the navigation should be server side only and not update the browser.
:param _server_side: Optional flag which defines if the call is originated on the server side and thus
the browser history should be updated. Default is False."""
target_url = self.get_target_url(target)
target_url = self.resolve_target(target)
entry = target_url.entry
if entry is None:
if target_url.fragment is not None:
Expand All @@ -64,7 +64,6 @@ async def build(content_element, fragment, kwargs) -> None:
if fragment is not None:
await ui.run_javascript(f'window.location.href = "#{fragment}";')

content = context.get_client().single_page_content
content.clear()
self.clear()
combined_dict = {**target_url.path_args, **target_url.query_args}
background_tasks.create(build(content, target_url.fragment, combined_dict))
background_tasks.create(build(self, target_url.fragment, combined_dict))
77 changes: 43 additions & 34 deletions nicegui/outlet.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from nicegui import background_tasks, helpers, ui, core, context
from nicegui.elements.router_frame import RouterFrame
from nicegui.router_frame_url import SinglePageUrl
from nicegui.router_frame_url import SinglePageTarget


class SinglePageRouterEntry:
Expand Down Expand Up @@ -48,7 +48,7 @@ def open(self, target: Union[Callable, str, Tuple[str, bool]]) -> None:
target, server_side = target


class outlet:
class Outlet:
"""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.
Expand Down Expand Up @@ -105,17 +105,9 @@ def _setup_routing_pages(self):
@ui.page(self.base_path, **self.page_kwargs)
@ui.page(f'{self.base_path}' + '{_:path}', **self.page_kwargs) # all other pages
async def root_page():
content = self.outlet_builder()
client = context.get_client()
while True:
try:
next(content)
if client.single_page_content is None:
client.single_page_content = self._setup_content_area()
except StopIteration:
break

def view(self, path: str) -> "OutletView":
self._setup_content_area()

def view(self, path: str) -> 'OutletView':
"""Decorator for the view function"""
return OutletView(path, self)

Expand All @@ -130,16 +122,6 @@ def setup_page_routes(self, **kwargs):
self._update_masks()
self._find_api_routes()

def _setup_content_area(self) -> RouterFrame:
"""Setups the content area for the single page router
:return: The content area element
"""
content = RouterFrame(list(self.included_paths), self.use_browser_history)
content.on_resolve(self.get_target_url)
context.get_client().single_page_content = content
return content

def add_page(self, path: str, builder: Callable, title: Optional[str] = None) -> None:
"""Add a new route to the single page router
Expand All @@ -157,19 +139,46 @@ def add_router_entry(self, entry: SinglePageRouterEntry) -> None:
"""
self.routes[entry.path] = entry.verify()

def get_target_url(self, path: Union[Callable, str]) -> SinglePageUrl:
"""Returns the SinglePageRouterEntry for the given URL path or builder function
def resolve_target(self, target: Union[Callable, str]) -> SinglePageTarget:
"""Tries to resolve a target such as a builder function or an URL path w/ route and query parameters.
:param path: The URL path to open or a builder function
:return: The SinglePageUrl object which contains the parsed route, query arguments and fragment
:param target: The URL path to open or a builder function
:return: The resolved target. Defines .valid if the target is valid
"""
if isinstance(path, Callable):
for path, entry in self.routes.items():
if entry.builder == path:
return SinglePageUrl(entry=entry)
if isinstance(target, Callable):
for target, entry in self.routes.items():
if entry.builder == target:
return SinglePageTarget(entry=entry)
else:
parser = SinglePageUrl(path)
return parser.parse_single_page_route(self.routes, path)
parser = SinglePageTarget(target)
return parser.parse_single_page_route(self.routes, target)

def navigate_to(self, target: Union[Callable, str, SinglePageTarget]) -> bool:
"""Navigate to a target
:param target: The target to navigate to
"""
if not isinstance(target, SinglePageTarget):
target = self.resolve_target(target)
if not target.valid:
return False
# TODO find right content area
return True

def _setup_content_area(self):
"""Setups the content area for the single page router
:return: The content area element
"""
frame = self.outlet_builder()
next(frame) # execute top layout components till first yield
content = RouterFrame(list(self.included_paths), self.use_browser_history) # exchangeable content of the page
content.on_resolve(self.resolve_target)
while True: # execute the rest of the outlets ui setup yield by yield
try:
next(frame)
except StopIteration:
break

def _is_excluded(self, path: str) -> bool:
"""Checks if a path is excluded by the exclusion masks
Expand Down Expand Up @@ -223,7 +232,7 @@ def _find_api_routes(self) -> None:

class OutletView:

def __init__(self, path: str, parent_outlet: outlet):
def __init__(self, path: str, parent_outlet: Outlet):
self.path = path
self.parent_outlet = parent_outlet

Expand Down
5 changes: 4 additions & 1 deletion nicegui/router_frame_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from nicegui.outlet import SinglePageRouterEntry


class SinglePageUrl:
class SinglePageTarget:
"""Aa helper class which is used to parse the path and query parameters of an URL to find the matching
SinglePageRouterEntry and convert the parameters to the expected types of the builder function"""

Expand All @@ -24,6 +24,7 @@ def __init__(self, path: Optional[str] = None, entry: Optional['SinglePageRouter
self.path_args = {}
self.query_args = urllib.parse.parse_qs(self.query_string)
self.entry = entry
self.valid = entry is not None

def parse_single_page_route(self, routes: Dict[str, 'SinglePageRouterEntry'], path: str) -> Self:
"""
Expand All @@ -38,10 +39,12 @@ def parse_single_page_route(self, routes: Dict[str, 'SinglePageRouterEntry'], pa
self.path_args = {}
self.query_args = urllib.parse.parse_qs(self.query_string)
if self.fragment is not None and len(self.path) == 0:
self.valid = True
return self
self.entry = self.parse_path()
if self.entry is not None:
self.convert_arguments()
self.valid = True
return self

def parse_path(self) -> Optional['SinglePageRouterEntry']:
Expand Down
2 changes: 1 addition & 1 deletion nicegui/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@
from .functions.style import add_css, add_sass, add_scss, add_style
from .functions.update import update
from .page import page
from .outlet import outlet
from .outlet import Outlet as outlet
from .page_layout import Drawer as drawer
from .page_layout import Footer as footer
from .page_layout import Header as header
Expand Down

0 comments on commit 3167521

Please sign in to comment.