Skip to content

Commit

Permalink
Clean up.
Browse files Browse the repository at this point in the history
Fixed SinglePageApp demos.
  • Loading branch information
Alyxion committed May 10, 2024
1 parent aa81849 commit 04be96a
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 51 deletions.
2 changes: 1 addition & 1 deletion examples/modularization/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ def index_page() -> None:
# Example 4: use APIRouter as described in https://nicegui.io/documentation/page#modularize_with_apirouter
app.include_router(api_router_example.router)

ui.run(title='Modularization Example')
ui.run(title='Modularization Example')
13 changes: 10 additions & 3 deletions examples/single_page_router/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Minimal example of a single page router with two pages
# Basic example of the SinglePageApp class which allows the fast conversion of already existing multi-page NiceGUI
# applications into a single page applications. Note that if you want more control over the routing, nested outlets or
# custom page setups,you should use the ui.outlet class instead which allows more flexibility.

from nicegui import ui
from nicegui.page import page
from nicegui.single_page_app import SinglePageApp
from nicegui.single_page_router import SinglePageRouter


@page('/', title='Welcome!')
Expand All @@ -18,5 +19,11 @@ def about():
ui.link('Index', '/')


router = SinglePageApp('/').reroute_pages()
def page_template():
with ui.header():
ui.label('My Company').classes('text-2xl')
yield # your content goes here


router = SinglePageApp('/', page_template=page_template).setup()
ui.run()
18 changes: 6 additions & 12 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ def is_auto_index_client(self) -> bool:
@property
def ip(self) -> Optional[str]:
"""Return the IP address of the client, or None if the client is not connected."""
return self.environ['asgi.scope']['client'][
0] if self.environ else None # pylint: disable=unsubscriptable-object
return self.environ['asgi.scope']['client'][0] if self.environ else None # pylint: disable=unsubscriptable-object

@property
def has_socket_connection(self) -> bool:
Expand Down Expand Up @@ -138,13 +137,12 @@ def build_response(self, request: Request, status_code: int = 200) -> Response:
'request': request,
'version': __version__,
'elements': elements.replace('&', '&')
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('`', '&#96;')
.replace('$', '&#36;'),
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('`', '&#96;')
.replace('$', '&#36;'),
'head_html': self.head_html,
'body_html': '<style>' + '\n'.join(vue_styles) + '</style>\n' + self.body_html + '\n' + '\n'.join(
vue_html),
'body_html': '<style>' + '\n'.join(vue_styles) + '</style>\n' + self.body_html + '\n' + '\n'.join(vue_html),
'vue_scripts': '\n'.join(vue_scripts),
'imports': json.dumps(imports),
'js_imports': '\n'.join(js_imports),
Expand Down Expand Up @@ -261,7 +259,6 @@ def handle_handshake(self) -> None:

def handle_disconnect(self) -> None:
"""Wait for the browser to reconnect; invoke disconnect handlers if it doesn't."""

async def handle_disconnect() -> None:
if self.page.reconnect_timeout is not None:
delay = self.page.reconnect_timeout
Expand All @@ -274,7 +271,6 @@ async def handle_disconnect() -> None:
self.safe_invoke(t)
if not self.shared:
self.delete()

self._disconnect_task = background_tasks.create(handle_disconnect())

def handle_event(self, msg: Dict) -> None:
Expand All @@ -298,7 +294,6 @@ def safe_invoke(self, func: Union[Callable[..., Any], Awaitable]) -> None:
async def func_with_client():
with self:
await func

background_tasks.create(func_with_client())
else:
with self:
Expand All @@ -307,7 +302,6 @@ async def func_with_client():
async def result_with_client():
with self:
await result

background_tasks.create(result_with_client())
except Exception as e:
core.app.handle_exception(e)
Expand Down
23 changes: 12 additions & 11 deletions nicegui/elements/router_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ def resolve_target(self, target: Any) -> SinglePageTarget:
return self._on_resolve(target)
raise NotImplementedError

def navigate_to(self, target: [SinglePageTarget, str], _server_side=True, _sync=False) -> None:
def navigate_to(self, target: [SinglePageTarget, str], _server_side=True, sync=False) -> None:
"""Open a new page in the browser by exchanging the content of the router frame
:param target: The target page or url.
: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."""
the browser history should be updated. Default is False.
:param sync: Optional flag to define if the content should be updated synchronously. Default is False.
"""
# check if sub router is active and might handle the target
for path_mask, frame in self.child_frames.items():
if path_mask == target or target.startswith(path_mask + '/'):
Expand All @@ -110,7 +112,7 @@ def navigate_to(self, target: [SinglePageTarget, str], _server_side=True, _sync=
if target_url.fragment is not None:
ui.run_javascript(f'window.location.href = "#{target_url.fragment}";') # go to fragment
return
title = "Page not found"
title = 'Page not found'
builder = self._page_not_found
else:
builder = entry.builder
Expand All @@ -119,19 +121,20 @@ def navigate_to(self, target: [SinglePageTarget, str], _server_side=True, _sync=
ui.run_javascript(
f'window.history.pushState({{page: "{target_url.original_path}"}}, "", "{target_url.original_path}");')
self._props['target_url'] = target_url.original_path
builder_kwargs = {**target_url.path_args, **target_url.query_args, "url_path": target_url.original_path}
builder_kwargs = {**target_url.path_args, **target_url.query_args, 'url_path': target_url.original_path}
target_fragment = target_url.fragment
recursive_user_data = RouterFrame.get_user_data() | self.user_data
builder_kwargs.update(recursive_user_data)
self.update_content(builder, builder_kwargs, title, target_fragment, _sync=_sync)
self.update_content(builder, builder_kwargs, title, target_fragment, sync=sync)

def update_content(self, builder, builder_kwargs, title, target_fragment, _sync=False):
def update_content(self, builder, builder_kwargs, title, target_fragment, sync=False):
"""Update the content of the router frame
:param builder: The builder function which builds the content of the page
:param builder_kwargs: The keyword arguments to pass to the builder function
:param title: The title of the page
:param target_fragment: The fragment to navigate to after the content has been loaded"""
:param target_fragment: The fragment to navigate to after the content has been loaded
:param sync: Optional flag to define if the content should be updated synchronously. Default is False."""
if self.change_title:
ui.page_title(title if title is not None else core.app.config.title)

Expand All @@ -141,14 +144,12 @@ def exec_builder():

async def build() -> None:
with self:
result = exec_builder()
if helpers.is_coroutine_function(builder):
await result
exec_builder()
if target_fragment is not None:
await ui.run_javascript(f'window.location.href = "#{target_fragment}";')

self.clear()
if _sync:
if sync:
with self:
exec_builder()
if target_fragment is not None:
Expand Down
29 changes: 22 additions & 7 deletions nicegui/outlet.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import inspect
from typing import Callable, Any, Self, Optional, Generator

from nicegui.client import Client
Expand All @@ -7,14 +6,23 @@


class Outlet(SinglePageRouter):
"""An outlet function defines the page layout of a single page application into which dynamic content can be
inserted. It is a high-level abstraction which manages the routing and content area of the SPA."""
"""An outlet allows the creation of single page applications which do not reload the page when navigating between
different views. The outlet is a container for multiple views and can contain nested outlets.
To define a new outlet, use the @ui.outlet decorator on a function which defines the layout of the outlet.
The layout function must be a generator function and contain a yield statement to separate the layout from the
content area. The yield can also be used to pass properties to the content are by return a dictionary with the
properties. Each property can be received as function argument in all nested views and outlets.
Once the outlet is defined, multiple views can be added to the outlet using the @outlet.view decorator on
a function.
"""

def __init__(self,
path: str,
outlet_builder: Optional[Callable] = None,
browser_history: bool = True,
parent: Optional["SinglePageRouter"] = None,
parent: Optional['SinglePageRouter'] = None,
on_instance_created: Optional[Callable] = None,
**kwargs) -> None:
"""
Expand Down Expand Up @@ -78,7 +86,11 @@ def outlet_view(**kwargs):
return self

def view(self, path: str, title: Optional[str] = None) -> 'OutletView':
"""Decorator for the view function
"""Decorator for the view function.
With the view function you define the actual content of the page. The view function is called when the user
navigates to the specified path relative to the outlet's base path.
: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.
Expand Down Expand Up @@ -123,8 +135,11 @@ def __init__(self, parent_outlet: SinglePageRouter, path: str, title: Optional[s

@property
def url(self) -> str:
"""The absolute URL of the view"""
return (self.parent_outlet.base_path.rstrip("/") + "/" + self.path.lstrip("/")).rstrip('/')
"""The absolute URL of the view
:return: The absolute URL of the view
"""
return (self.parent_outlet.base_path.rstrip('/') + '/' + self.path.lstrip('/')).rstrip('/')

def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
"""Decorator for the view function"""
Expand Down
3 changes: 1 addition & 2 deletions nicegui/page_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from .context import context
from .element import Element
from .elements.mixins.value_element import ValueElement
from .elements.router_frame import RouterFrame
from .functions.html import add_body_html
from .logging import log

Expand Down Expand Up @@ -273,7 +272,7 @@ def __init__(self, position: PageStickyPositions = 'bottom-right', x_offset: flo

def _check_current_slot(element: Element) -> None:
parent = context.slot.parent
if parent != parent.client.content and not isinstance(parent, RouterFrame):
if parent != parent.client.content:
log.warning(f'Found top level layout element "{element.__class__.__name__}" inside element "{parent.__class__.__name__}". '
'Top level layout elements should not be nested but must be direct children of the page content. '
'This will be raising an exception in NiceGUI 1.5') # DEPRECATED
21 changes: 14 additions & 7 deletions nicegui/single_page_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ def resolve_target(self, target: Union[Callable, str]) -> SinglePageTarget:
# isolate the real path elements and update target accordingly
target = "/".join(path.split("/")[:len(cur_router.base_path.split("/"))])
break
parser = SinglePageTarget(target, router=self)
result = parser.parse_single_page_route(self.routes, target)
result = SinglePageTarget(target, router=self).parse_url_path(routes=self.routes)
if resolved is not None:
result.original_path = resolved.original_path
return result
Expand All @@ -178,10 +177,14 @@ def build_page_template(self) -> Generator:
content of the page.
:return: The page template generator function"""
if self.build_page_template is not None:

def default_template():
yield

if self.page_template is not None:
return self.page_template()
else:
raise ValueError('No page template generator function provided.')
return default_template()

def build_page(self, initial_url: Optional[str] = None, **kwargs):
kwargs['url_path'] = initial_url
Expand All @@ -202,9 +205,13 @@ def build_page(self, initial_url: Optional[str] = None, **kwargs):
pass
content_area.update_user_data(new_user_data)

def insert_content_area(self, initial_url: Optional[str] = None,
def insert_content_area(self,
initial_url: Optional[str] = None,
user_data: Optional[Dict] = None) -> RouterFrame:
"""Setups the content area"""
"""Inserts the content area in form of a RouterFrame into the page
:param initial_url: The initial URL to initialize the router's content with
:param user_data: Optional user data to pass to the content area"""
parent_router_frame = RouterFrame.get_current_frame()
content = RouterFrame(router=self,
included_paths=sorted(list(self.included_paths)),
Expand All @@ -218,7 +225,7 @@ def insert_content_area(self, initial_url: Optional[str] = None,
context.client.single_page_router_frame = content
initial_url = content.target_url
if initial_url is not None:
content.navigate_to(initial_url, _server_side=False, _sync=True)
content.navigate_to(initial_url, _server_side=False, sync=True)
return content

def _register_child_router(self, router: "SinglePageRouter") -> None:
Expand Down
18 changes: 11 additions & 7 deletions nicegui/single_page_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@


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"""
"""A helper class which is used to resolve and URL path and it's query and fragment parameters to find the matching
SinglePageRouterEntry and extract path and query parameters."""

def __init__(self, path: Optional[str] = None, entry: Optional['SinglePageRouterEntry'] = None,
fragment: Optional[str] = None, query_string: Optional[str] = None,
def __init__(self,
path: Optional[str] = None,
entry: Optional['SinglePageRouterEntry'] = None,
fragment: Optional[str] = None,
query_string: Optional[str] = None,
router: Optional['SinglePageRouter'] = None):
"""
:param path: The path of the URL
Expand All @@ -31,12 +34,13 @@ def __init__(self, path: Optional[str] = None, entry: Optional['SinglePageRouter
self.valid = entry is not None
self.router = router

def parse_single_page_route(self, routes: Dict[str, 'SinglePageRouterEntry'], path: str) -> Self:
def parse_url_path(self, routes: Dict[str, 'SinglePageRouterEntry']) -> Self:
"""
Parses the route using the provided routes dictionary and path.
:param routes: All routes of the single page router
:param path: The path of the URL
"""
parsed_url = urllib.parse.urlparse(urllib.parse.unquote(path))
parsed_url = urllib.parse.urlparse(urllib.parse.unquote(self.path))
self.routes = routes # all valid routes
self.path = parsed_url.path # url path w/o query
self.fragment = parsed_url.fragment if len(parsed_url.fragment) > 0 else None
Expand Down
2 changes: 1 addition & 1 deletion nicegui/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,4 @@
from .page_layout import PageSticky as page_sticky
from .page_layout import RightDrawer as right_drawer
from .ui_run import run
from .ui_run_with import run_with
from .ui_run_with import run_with

0 comments on commit 04be96a

Please sign in to comment.