Skip to content

Commit

Permalink
* Removed on_resolve and on_open events from outlet and SinglePageRou…
Browse files Browse the repository at this point in the history
…terConfig

* Extended on_navigate so it can also return SinglePageTargets
  • Loading branch information
Alyxion committed Jun 12, 2024
1 parent 94eed81 commit 8f61006
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 97 deletions.
10 changes: 6 additions & 4 deletions examples/single_page_app_complex/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ def update_title(target: SinglePageTarget,
return target


@services_router.view('/', on_open=update_title) # service index page
def show_index(service: ServiceDefinition):
@services_router.view('/') # service index page
def show_index(target: SinglePageTarget, service: ServiceDefinition):
update_title(target, service, None)
with ui.row() as row:
ui.label(service.emoji).classes('text-h4 vertical-middle')
with ui.column():
Expand All @@ -142,8 +143,9 @@ def sub_service_router(service: ServiceDefinition, sub_service_name: str):
yield {'sub_service': sub_service} # pass sub_service object to all sub elements (views and outlets)


@sub_service_router.view('/', on_open=update_title) # sub service index page
def sub_service_index(sub_service: SubServiceDefinition):
@sub_service_router.view('/') # sub service index page
def sub_service_index(target: SinglePageTarget, service: ServiceDefinition, sub_service: SubServiceDefinition):
update_title(target, service, sub_service)
ui.label(sub_service.emoji).classes('text-h1')
ui.html('<br>')
ui.label(sub_service.description)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import html
import uuid
from typing import Optional
from typing import Optional, Union

from nicegui import ui, app
from nicegui.single_page_target import SinglePageTarget
Expand Down Expand Up @@ -71,7 +71,7 @@ def logout(): # logs the user out and redirects to the login page
ui.navigate.to(INDEX_URL)


def check_login(url) -> Optional[SinglePageTarget]:
def check_login(url) -> Optional[Union[str, SinglePageTarget]]:
def error_page():
with ui.column().style('align-items: center; justify-content: center; left: 50%; top: 50%; '
'transform: translate(-50%, -50%); position: absolute;'):
Expand All @@ -82,10 +82,10 @@ def error_page():

if 'login_token' not in app.storage.tab: # check if the user is not logged in
return SinglePageTarget(url, builder=error_page, title='Not logged in')
return None # default behavior
return url # default behavior


@main_router.outlet(SECRET_AREA_URL, on_resolve=check_login)
@main_router.outlet(SECRET_AREA_URL, on_navigate=check_login)
def secret_area_router():
yield

Expand Down
29 changes: 6 additions & 23 deletions nicegui/outlet.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
from typing import Callable, Any, Self, Optional, Generator
from typing import Callable, Any, Self, Optional, Generator, Union

from nicegui.client import Client
from nicegui.single_page_router import SinglePageRouter
Expand Down Expand Up @@ -27,9 +26,7 @@ def __init__(self,
browser_history: bool = True,
parent: Optional['SinglePageRouterConfig'] = None,
on_instance_created: Optional[Callable[['SinglePageRouter'], None]] = None,
on_resolve: Optional[Callable[[str], Optional[SinglePageTarget]]] = None,
on_open: Optional[Callable[[SinglePageTarget], SinglePageTarget]] = None,
on_navigate: Optional[Callable[[str], Optional[str]]] = None,
on_navigate: Optional[Callable[[str], Optional[Union[SinglePageTarget, str]]]] = None,
**kwargs) -> None:
"""
:param path: the base path of the single page router.
Expand All @@ -40,18 +37,14 @@ def __init__(self,
:param browser_history: Optional flag to enable or disable the browser history management. Default is True.
:param on_instance_created: Optional callback which is called when a new instance is created. Each browser tab
or window is a new instance. This can be used to initialize the state of the application.
:param on_resolve: Optional callback which is called when a URL path is resolved to a target. Can be used
to resolve or redirect a URL path to a target.
:param on_open: Optional callback which is called when a target is opened. Can be used to modify the target
such as title or the actually called builder function.
:param on_navigate: Optional callback which is called when a navigation event is triggered. Can be used to
prevent or modify the navigation. Return the new URL if the navigation should be allowed, modify the URL
or return None to prevent the navigation.
:param parent: The parent outlet of this outlet.
:param kwargs: Additional arguments
"""
super().__init__(path, browser_history=browser_history, on_instance_created=on_instance_created,
on_resolve=on_resolve, on_open=on_open, on_navigate=on_navigate,
on_navigate=on_navigate,
parent=parent, **kwargs)
self.outlet_builder: Optional[Callable] = outlet_builder
if parent is None:
Expand Down Expand Up @@ -101,8 +94,7 @@ def outlet_view(**kwargs):

def view(self,
path: str,
title: Optional[str] = None,
on_open: Optional[Callable[[SinglePageTarget, Any], SinglePageTarget]] = None
title: Optional[str] = None
) -> 'OutletView':
"""Decorator for the view function.
Expand All @@ -112,10 +104,8 @@ def view(self,
:param path: The path of the view, relative to the base path of the outlet
:param title: Optional title of the view. If a title is set, it will be displayed in the browser tab
when the view is active, otherwise the default title of the application is displayed.
:param on_open: Optional callback which is called when the target is resolved to this view. It can be used
to modify the target before the view is displayed.
"""
return OutletView(self, path, title=title, on_open=on_open)
return OutletView(self, path, title=title)

def outlet(self, path: str, **kwargs) -> 'Outlet':
"""Defines a nested outlet
Expand Down Expand Up @@ -143,20 +133,16 @@ def current_url(self) -> str:
class OutletView:
"""Defines a single view / "content page" which is displayed in an outlet"""

def __init__(self, parent_outlet: SinglePageRouterConfig, path: str, title: Optional[str] = None,
on_open: Optional[Callable[[SinglePageTarget, Any], SinglePageTarget]] = None):
def __init__(self, parent_outlet: SinglePageRouterConfig, path: str, title: Optional[str] = None):
"""
:param parent_outlet: The parent outlet in which this view is displayed
:param path: The path of the view, relative to the base path of the outlet
:param title: Optional title of the view. If a title is set, it will be displayed in the browser tab
when the view is active, otherwise the default title of the application is displayed.
:param on_open: Optional callback which is called when the target is resolved to this view and going to be
opened. It can be used to modify the target before the view is displayed.
"""
self.path = path
self.title = title
self.parent_outlet = parent_outlet
self.on_open = on_open

@property
def url(self) -> str:
Expand All @@ -172,9 +158,6 @@ def handle_resolve(self, target: SinglePageTarget, **kwargs) -> SinglePageTarget
:param target: The resolved target
:return: The resolved target or a modified target
"""
if self.on_open is not None:
RouterFrame.run_safe(self.on_open, **kwargs | {'target': target},
type_check=True)
return target

def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
Expand Down
57 changes: 12 additions & 45 deletions nicegui/single_page_router.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fnmatch import fnmatch
from typing import Callable, Any, Optional, Dict, TYPE_CHECKING, Self
from typing import Callable, Any, Optional, Dict, TYPE_CHECKING, Self, Union

from nicegui import ui, core
from nicegui.context import context
Expand Down Expand Up @@ -81,9 +81,7 @@ def __init__(self,
use_browser_history=use_browser_history,
on_navigate=lambda url, history: self.navigate_to(url, history=history),
user_data={'router': self})
self._on_navigate: Optional[Callable[[str], Optional[str]]] = None
self._on_resolve: Optional[Callable[[str], Optional[SinglePageTarget]]] = None
self._on_open: Optional[Callable[[SinglePageTarget, Any], SinglePageTarget]] = None
self._on_navigate: Optional[Callable[[str], Optional[Union[SinglePageTarget, str]]]] = None

@property
def target_url(self) -> str:
Expand All @@ -99,50 +97,32 @@ def resolve_target(self, target: Any, user_data: Optional[Dict] = None) -> Singl
:param target: The target object such as a URL or Callable
:param user_data: Optional user data which is passed to the resolver functions
:return: The resolved SinglePageTarget object"""
if self._on_resolve is not None: # try custom handler first if defined
resolved_target = self._on_resolve(target)
if resolved_target is not None:
return resolved_target
if isinstance(target, SinglePageTarget):
return target
target = self.router_config.resolve_target(target)
if target.valid and target.router is None:
target.router = self
if target is None:
raise NotImplementedError
if target.router_path is not None:
original_path = target.original_path
if self._on_open is not None: # try the router instance's custom open handler first
target = RouterFrame.run_safe(self._on_open(**user_data | {'target': target}))
if target.original_path != original_path:
return self.resolve_target(target, user_data)
rp = target.router_path
if rp.on_open is not None: # try the router path's custom open handler
target = rp.on_open(target, **user_data)
if target.original_path != original_path:
return self.resolve_target(target, user_data)
config = self.router_config
while config is not None:
if config.on_open is not None:
target = config.on_open(target)
if target.original_path != original_path:
return self.resolve_target(target, user_data)
config = config.parent_config
return target

def handle_navigate(self, target: str) -> Optional[str]:
def handle_navigate(self, target: str) -> Optional[Union[SinglePageTarget, str]]:
"""Is called when there was a navigation event in the browser or by the navigate_to method.
By default, the original target is returned. The SinglePageRouter and the router config (the outlet) can
manipulate the target before it is resolved. If the target is None, the navigation is suppressed.
:param target: The target URL
:return: The target URL or None if the navigation is suppressed"""
:return: The target URL, a completely resolved target or None if the navigation is suppressed"""
if self._on_navigate is not None:
target = self._on_navigate(target)
if target is None:
return None
if isinstance(target, SinglePageTarget):
return target
new_target = self.router_config.handle_navigate(target)
if isinstance(target, SinglePageTarget):
return target
if new_target is None or new_target != target:
return new_target
return target
Expand All @@ -166,9 +146,10 @@ def navigate_to(self, target: [SinglePageTarget, str], server_side=True, sync=Fa
if target is None:
return
handler_kwargs = SinglePageRouter.get_user_data() | self.user_data | self.router_frame.user_data | \
{'previous_url_path': self.router_frame.target_url}
{'previous_url_path': self.router_frame.target_url}
handler_kwargs['url_path'] = target if isinstance(target, str) else target.original_path
target = self.resolve_target(target, user_data=handler_kwargs)
if not isinstance(target, SinglePageTarget):
target = self.resolve_target(target, user_data=handler_kwargs)
if target is None or not target.valid: # navigation suppressed
return
target_url = target.original_path
Expand Down Expand Up @@ -210,27 +191,13 @@ def update_user_data(self, new_data: dict) -> None:
:param new_data: The new user data to set"""
self.user_data.update(new_data)

def on_navigate(self, callback: Callable[[str], Optional[str]]) -> Self:
def on_navigate(self, callback: Callable[[str], Optional[Union[SinglePageTarget, str]]]) -> Self:
"""Set the on navigate callback which is called when a navigation event is triggered
:param callback: The callback function"""
self._on_navigate = callback
return self

def on_resolve(self, callback: Callable[[str], Optional[SinglePageTarget]]) -> Self:
"""Set the on resolve callback which is called when a URL path is resolved to a target
:param callback: The callback function"""
self._on_resolve = callback
return self

def on_open(self, callback: Callable[[SinglePageTarget, Any], SinglePageTarget]) -> Self:
"""Set the on open callback which is called when a target is opened
:param callback: The callback function"""
self._on_open = callback
return self

@staticmethod
def get_user_data() -> Dict:
"""Returns a combined dictionary of all user data of the parent router frames"""
Expand Down
29 changes: 8 additions & 21 deletions nicegui/single_page_router_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,14 @@ def __init__(self,
parent: Optional['SinglePageRouterConfig'] = None,
page_template: Optional[Callable[[], Generator]] = None,
on_instance_created: Optional[Callable[['SinglePageRouter'], None]] = None,
on_resolve: Optional[Callable[[str], Optional[SinglePageTarget]]] = None,
on_open: Optional[Callable[[SinglePageTarget], SinglePageTarget]] = None,
on_navigate: Optional[Callable[[str], Optional[str]]] = None,
on_navigate: Optional[Callable[[str], Optional[Union[SinglePageTarget, str]]]] = None,
**kwargs) -> None:
""":param path: the base path of the single page router.
:param browser_history: Optional flag to enable or disable the browser history management. Default is True.
:param parent: The parent router of this router if this router is a nested router.
:param page_template: Optional page template generator function which defines the layout of the page. It
needs to yield a value to separate the layout from the content area.
:param on_instance_created: Optional callback which is called when a new router instance is created. Each
:param on_resolve: Optional callback which is called when a URL path is resolved to a target. Can be used
to resolve or redirect a URL path to a target.
:param on_open: Optional callback which is called when a target is opened. Can be used to modify the target
such as title or the actually called builder function.
:param on_navigate: Optional callback which is called when a navigation event is triggered. Can be used to
prevent or modify the navigation. Return the new URL if the navigation should be allowed, modify the URL
or return None to prevent the navigation.
Expand All @@ -51,9 +45,7 @@ def __init__(self,
self.included_paths: Set[str] = set()
self.excluded_paths: Set[str] = set()
self.on_instance_created: Optional[Callable] = on_instance_created
self.on_resolve: Optional[Callable[[str], Optional[SinglePageTarget]]] = on_resolve
self.on_open: Optional[Callable[[SinglePageTarget], SinglePageTarget]] = on_open
self.on_navigate: Optional[Callable[[str], Optional[str]]] = on_navigate
self.on_navigate: Optional[Callable[[str], Optional[Union[SinglePageTarget, str]]]] = on_navigate
self.use_browser_history = browser_history
self.page_template = page_template
self._setup_configured = False
Expand Down Expand Up @@ -123,13 +115,6 @@ def resolve_target(self, target: Union[Callable, str]) -> SinglePageTarget:
if entry.builder == target:
return SinglePageTarget(router_path=entry)
else:
cur_config = self
while cur_config is not None: # try custom on_resolve functions first for manual resolution
if cur_config.on_resolve is not None:
resolved = cur_config.on_resolve(target)
if resolved is not None:
return resolved
cur_config = cur_config.parent_config
resolved = None
path = target.split('#')[0].split('?')[0]
for cur_router in self.child_routers:
Expand Down Expand Up @@ -162,15 +147,17 @@ def navigate_to(self, target: Union[Callable, str, SinglePageTarget], server_sid
router.navigate_to(org_target, server_side=server_side)
return True

def handle_navigate(self, url: str) -> Optional[str]:
def handle_navigate(self, url: str) -> Optional[Union[SinglePageTarget, str]]:
"""Handles a navigation event and returns the new URL if the navigation should be allowed
:param url: The URL to navigate to
:return: The new URL if the navigation should be allowed, None otherwise"""
if self.on_navigate is not None:
new_url = self.on_navigate(url)
if new_url != url:
return new_url
new_target = self.on_navigate(url)
if isinstance(new_target, SinglePageTarget):
return new_target
if new_target != url:
return new_target
if self.parent_config is not None:
return self.parent_config.handle_navigate(url)
return url
Expand Down

0 comments on commit 8f61006

Please sign in to comment.