Skip to content

Commit 9eb9664

Browse files
Merge branch 'zauberzeug:main' into main
2 parents c7e6299 + 712d056 commit 9eb9664

File tree

7 files changed

+114
-64
lines changed

7 files changed

+114
-64
lines changed

nicegui/elements/sub_pages.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ def __init__(self,
3333
Routes are defined as path patterns mapping to page builder functions.
3434
Path parameters like "/user/{id}" are extracted and passed to the builder function.
3535
36-
**This is an experimental feature, and the API is subject to change.**
37-
3836
*Added in version 2.22.0*
3937
4038
:param routes: dictionary mapping path patterns to page builder functions

nicegui/sub_pages_router.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ def __init__(self, request: Request | None) -> None:
4343
def on_path_changed(self, handler: Callable[[str], None]) -> None:
4444
"""Register a callback to be invoked when the path changes.
4545
46-
**This is an experimental feature, and the API is subject to change.**
47-
4846
:param handler: callback function that receives the new path as its argument
4947
"""
5048
self._path_changed_handlers.append(handler)

website/documentation/code_extraction.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,16 @@ def get_full_code(f: Callable) -> str:
2222
while code[0].strip() != '"""':
2323
del code[0]
2424
del code[0]
25-
indentation = len(code[0]) - len(code[0].lstrip())
25+
non_empty_lines = [line for line in code if line.strip()]
26+
indentation = len(non_empty_lines[0]) - len(non_empty_lines[0].lstrip())
2627
code = [line[indentation:] for line in code]
2728
code = ['from nicegui import ui'] + [_uncomment(line) for line in code]
2829
code = ['' if line == '#' else line for line in code]
29-
if not code[-1].startswith('ui.run('):
30+
31+
if any(line.strip().startswith('def root(') for line in code):
32+
code = [line for line in code if line.strip() != 'return root']
33+
code.append('ui.run(root)')
34+
elif not code[-1].startswith('ui.run('):
3035
code.append('')
3136
code.append('ui.run()')
3237
full_code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)

website/documentation/content/sub_pages_documentation.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import asyncio
2-
from typing import Any, Awaitable, Callable, Dict, Optional
2+
from typing import Any, Awaitable, Callable, Optional
33

44
from nicegui import PageArguments, background_tasks, ui
55

@@ -18,8 +18,8 @@ def init(self) -> None:
1818
self._render('/')
1919
self.move() # move to end
2020

21-
def link(self, text: str, route: str, **kwargs: Any) -> None:
22-
ui.label(text).classes('nicegui-link cursor-pointer').on('click', lambda: self._render(route, **kwargs))
21+
def link(self, text: str, route: str, **kwargs: Any) -> ui.label:
22+
return ui.label(text).classes('nicegui-link cursor-pointer').on('click', lambda: self._render(route, **kwargs))
2323

2424
def _render(self, route: str, **kwargs: Any) -> None:
2525
if self.task and not self.task.done():
@@ -46,8 +46,6 @@ def __init__(self, **kwargs: Any) -> None:
4646
The `ui.sub_pages` element itself functions as the container for the currently active sub page.
4747
You only need to provide the routes for each view builder function.
4848
NiceGUI takes care of replacing the content without triggering a full page reload when the URL changes.
49-
50-
**NOTE: This is an experimental feature, and the API is subject to change.**
5149
''')
5250
def main_demo() -> None:
5351
from uuid import uuid4

website/documentation/demo.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@ def demo(f: Callable, *, lazy: bool = True, tab: Optional[Union[str, Callable]]
2424

2525
async def handle_intersection():
2626
window.remove(spinner)
27-
if helpers.is_coroutine_function(f):
28-
await f()
29-
else:
30-
f()
27+
result = f()
28+
if callable(result):
29+
if helpers.is_coroutine_function(result):
30+
await result()
31+
else:
32+
result()
3133
intersection_observer(on_intersection=handle_intersection)
3234
else:
33-
assert not helpers.is_coroutine_function(f), 'async functions are not supported in non-lazy demos'
34-
f()
35+
result = f()
36+
if callable(result):
37+
assert not helpers.is_coroutine_function(result), \
38+
'async functions are not supported in non-lazy demos'
39+
result()
3540

3641
return f

website/documentation/intro.py

Lines changed: 90 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,105 @@
1+
import random
12
from typing import Callable
23

34
from nicegui import ui
45

56
from ..style import subheading
7+
from .content.sub_pages_documentation import FakeSubPages
68
from .demo import demo
79

810

911
def create_intro() -> None:
10-
@_main_page_demo('Styling', '''
11-
While having reasonable defaults, you can still modify the look of your app with CSS as well as Tailwind and Quasar classes.
12-
''')
13-
def formatting_demo():
14-
ui.icon('thumb_up')
15-
ui.markdown('This is **Markdown**.')
16-
ui.html('This is <strong>HTML</strong>.', sanitize=False)
17-
with ui.row():
18-
ui.label('CSS').style('color: #888; font-weight: bold')
19-
ui.label('Tailwind').classes('font-serif')
20-
ui.label('Quasar').classes('q-ml-xl')
21-
ui.link('NiceGUI on GitHub', 'https://github.com/zauberzeug/nicegui')
22-
23-
@_main_page_demo('Common UI Elements', '''
24-
NiceGUI comes with a collection of commonly used UI elements.
12+
@_main_page_demo('Single-Page Applications', '''
13+
Build applications with fast client-side routing using [`ui.sub_pages`](/documentation/sub_pages)
14+
and a `root` function as single entry point.
15+
For each visitor, the `root` function is executed and generates the interface.
16+
[`ui.link`](/documentation/link) and [`ui.navigate`](/documentation/navigate) can be used to navigate to other sub pages.
17+
18+
If you do not want a single-page application, you can also use [`@ui.page('/your/path')`](/documentation/page)
19+
to define standalone content available at a specific path.
2520
''')
26-
def common_elements_demo():
27-
from nicegui.events import ValueChangeEventArguments
28-
29-
def show(event: ValueChangeEventArguments):
30-
name = type(event.sender).__name__
31-
ui.notify(f'{name}: {event.value}')
32-
33-
ui.button('Button', on_click=lambda: ui.notify('Click'))
34-
with ui.row():
35-
ui.checkbox('Checkbox', on_change=show)
36-
ui.switch('Switch', on_change=show)
37-
ui.radio(['A', 'B', 'C'], value='A', on_change=show).props('inline')
38-
with ui.row():
39-
ui.input('Text input', on_change=show)
40-
ui.select(['One', 'Two'], value='One', on_change=show)
41-
ui.link('And many more...', '/documentation').classes('mt-8')
42-
43-
@_main_page_demo('Value Binding', '''
44-
Binding values between UI elements and data models is built into NiceGUI.
21+
def spa_demo():
22+
sub_pages = None # HIDE
23+
24+
def root():
25+
# ui.sub_pages({
26+
# '/': table_page,
27+
# '/map/{lat}/{lon}': map_page,
28+
# }).classes('w-full')
29+
nonlocal sub_pages # HIDE
30+
sub_pages = FakeSubPages({'/': table_page, '/map/{lat}/{lon}': map_page}).classes('w-full') # HIDE
31+
sub_pages.init() # HIDE
32+
33+
def table_page():
34+
ui.table(rows=[
35+
{'name': 'New York', 'lat': 40.7119, 'lon': -74.0027},
36+
{'name': 'London', 'lat': 51.5074, 'lon': -0.1278},
37+
{'name': 'Tokio', 'lat': 35.6863, 'lon': 139.7722},
38+
]).on('row-click', lambda e: sub_pages._render('/map/{lat}/{lon}', lat=e.args[1]['lat'], lon=e.args[1]['lon'])) # HIDE
39+
# ]).on('row-click', lambda e: ui.navigate.to(f'/map/{e.args[1]["lat"]}/{e.args[1]["lon"]}'))
40+
41+
def map_page(lat: float, lon: float):
42+
ui.leaflet(center=(lat, lon), zoom=10)
43+
# ui.link('Back to table', '/')
44+
sub_pages.link('Back to table', '/') # HIDE
45+
46+
return root
47+
48+
@_main_page_demo('Reactive Transformations', '''
49+
Create real-time interfaces with automatic updates.
50+
Type and watch text flow in both directions.
51+
When input changes, the [binding](/documentation/section_binding_properties) transforms the text
52+
with a custom Python function and updates the label.
4553
''')
4654
def binding_demo():
47-
class Demo:
48-
def __init__(self):
49-
self.number = 1
50-
51-
demo = Demo()
52-
v = ui.checkbox('visible', value=True)
53-
with ui.column().bind_visibility_from(v, 'value'):
54-
ui.slider(min=1, max=3).bind_value(demo, 'number')
55-
ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(demo, 'number')
56-
ui.number().bind_value(demo, 'number')
55+
def root():
56+
user_input = ui.input(value='Hello')
57+
ui.label().bind_text_from(user_input, 'value', reverse)
58+
59+
def reverse(text: str) -> str:
60+
return text[::-1]
61+
62+
return root
63+
64+
@_main_page_demo('Event System', '''
65+
Use an [Event](/documentation/event) to trigger actions and pass data.
66+
Here we have an IoT temperature sensor submitting its readings
67+
to a [FastAPI endpoint](/documentation/section_pages_routing#api_responses) with path "/sensor".
68+
When a new value arrives, it emits an event for the chart to be updated.
69+
''')
70+
def event_system_demo():
71+
import time
72+
73+
from nicegui import Event, app
74+
75+
sensor = Event[float]()
76+
77+
# @app.post('/sensor')
78+
def sensor_webhook(temperature: float):
79+
sensor.emit(temperature)
80+
81+
def root():
82+
chart = ui.echart({
83+
'xAxis': {'type': 'time', 'axisLabel': {'hideOverlap': True}},
84+
'yAxis': {'type': 'value', 'min': 'dataMin'},
85+
'series': [{'type': 'line', 'data': [], 'smooth': True}],
86+
})
87+
88+
def update_chart(temperature: float):
89+
data = chart.options['series'][0]['data']
90+
data.append([time.time(), temperature])
91+
if len(data) > 10:
92+
data.pop(0)
93+
94+
sensor.subscribe(update_chart)
95+
# END OF DEMO
96+
97+
data = chart.options['series'][0]['data']
98+
data.append([time.time(), 24.0])
99+
ui.timer(1.0, lambda: update_chart(round(data[-1][1] + (random.random() - 0.5), 1)), immediate=False)
100+
app # noqa: B018 to avoid unused import warning
101+
102+
return root
57103

58104

59105
def _main_page_demo(title: str, explanation: str) -> Callable:

website/main_page.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@ def create() -> None:
121121
'customizable [color themes](/documentation/section_styling_appearance#color_theming)',
122122
'custom CSS and classes',
123123
'modern look with material design',
124-
'[Tailwind CSS](https://tailwindcss.com/) auto-completion',
124+
'[Tailwind CSS](https://tailwindcss.com/)',
125125
])
126126
features('source', 'Coding', [
127-
'routing for multiple [pages](/documentation/page)',
127+
'single page apps with [ui.sub_pages](/documentation/sub_pages)',
128128
'auto-reload on code change',
129129
'persistent [user sessions](/documentation/storage)',
130-
'super nice [testing framework](/documentation/section_testing)',
130+
'super powerful [testing framework](/documentation/section_testing)',
131131
])
132132
features('anchor', 'Foundation', [
133133
'generic [Vue](https://vuejs.org/) to Python bridge',

0 commit comments

Comments
 (0)