فهرست منبع

Merge commit '89980649a8039a040df09b2982a32ce5598c2ba2' into pr/godsarmy/822

Rodja Trappe 2 سال پیش
والد
کامیت
7a8a48186c

+ 3 - 3
CITATION.cff

@@ -8,7 +8,7 @@ authors:
   given-names: Rodja
   orcid: https://orcid.org/0009-0009-4735-6227
 title: 'NiceGUI: Web-based interfaces with Python. The nice way.'
-version: v1.2.10
-date-released: '2023-04-27'
+version: v1.2.11
+date-released: '2023-05-02'
 url: https://github.com/zauberzeug/nicegui
-doi: 10.5281/zenodo.7871621
+doi: 10.5281/zenodo.7887694

+ 6 - 6
README.md

@@ -11,16 +11,16 @@ You can create buttons, dialogs, Markdown, 3D scenes, plots and much more.
 It is great for micro web apps, dashboards, robotics projects, smart home solutions and similar use cases.
 You can also use it in development, for example when tweaking/configuring a machine learning algorithm or tuning motor controllers.
 
-NiceGUI is available as [PyPI package](https://pypi.org/project/nicegui/), [Docker image](https://hub.docker.com/r/zauberzeug/nicegui) and on [conda-forge](https://anaconda.org/conda-forge/nicegui) + [GitHub](https://github.com/zauberzeug/nicegui).
+NiceGUI is available as [PyPI package](https://pypi.org/project/nicegui/), [Docker image](https://hub.docker.com/r/zauberzeug/nicegui) and on [conda-forge](https://anaconda.org/conda-forge/nicegui) as well as [GitHub](https://github.com/zauberzeug/nicegui).
 
-[![PyPI version](https://badge.fury.io/py/nicegui.svg)](https://pypi.org/project/nicegui/)
-[![PyPI downloads](https://img.shields.io/pypi/dm/nicegui)](https://pypi.org/project/nicegui/)
+[![PyPI](https://img.shields.io/pypi/v/nicegui?color=dark-green)](https://pypi.org/project/nicegui/)
+[![PyPI downloads](https://img.shields.io/pypi/dm/nicegui?color=dark-green)](https://pypi.org/project/nicegui/)
 [![Conda version](https://img.shields.io/conda/v/conda-forge/nicegui?color=green&label=conda-forge)](https://anaconda.org/conda-forge/nicegui)
 [![Conda downloads](https://img.shields.io/conda/dn/conda-forge/nicegui?color=green&label=downloads)](https://anaconda.org/conda-forge/nicegui)
 [![Docker pulls](https://img.shields.io/docker/pulls/zauberzeug/nicegui)](https://hub.docker.com/r/zauberzeug/nicegui)<br />
 [![GitHub license](https://img.shields.io/github/license/zauberzeug/nicegui?color=orange)](https://github.com/zauberzeug/nicegui/blob/main/LICENSE)
 [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/zauberzeug/nicegui)](https://github.com/zauberzeug/nicegui/graphs/commit-activity)
-[![GitHub issues](https://img.shields.io/github/issues/zauberzeug/nicegui?color=lightgreen)](https://github.com/zauberzeug/nicegui/issues)
+[![GitHub issues](https://img.shields.io/github/issues/zauberzeug/nicegui?color=blue)](https://github.com/zauberzeug/nicegui/issues)
 [![GitHub forks](https://img.shields.io/github/forks/zauberzeug/nicegui)](https://github.com/zauberzeug/nicegui/network)
 [![GitHub stars](https://img.shields.io/github/stars/zauberzeug/nicegui)](https://github.com/zauberzeug/nicegui/stargazers)
 
@@ -40,7 +40,7 @@ NiceGUI is available as [PyPI package](https://pypi.org/project/nicegui/), [Dock
   - interact with tables
   - navigate foldable tree structures
 - built-in timer to refresh data in intervals (even every 10 ms)
-- straight-forward data binding and refeshable functions to write even less code
+- straight-forward data binding and refreshable functions to write even less code
 - notifications, dialogs and menus to provide state of the art user interaction
 - shared and individual web pages
 - ability to add custom routes and data responses
@@ -49,7 +49,7 @@ NiceGUI is available as [PyPI package](https://pypi.org/project/nicegui/), [Dock
 - live-cycle events and session data
 - runs in Jupyter Notebooks and allows Python's interactive mode
 - auto-complete support for Tailwind CSS
-- svg, base64 and emoji favicon support
+- SVG, Base64 and emoji favicon support
 
 ## Installation
 

+ 5 - 4
examples/chat_app/main.py

@@ -4,15 +4,15 @@ from typing import List, Tuple
 
 from nicegui import Client, ui
 
-messages: List[Tuple[str, str]] = []
+messages: List[Tuple[str, str, str]] = []
 
 
 @ui.refreshable
 async def chat_messages(name_input: ui.input) -> None:
-    for name, text in messages:
+    for name, text, stamp in messages:
         ui.chat_message(text=text,
                         name=name,
-                        stamp=datetime.utcnow().strftime('%X'),
+                        stamp=stamp,
                         avatar=f'https://robohash.org/{name or "anonymous"}?bgset=bg2',
                         sent=name == name_input.value)
     await ui.run_javascript('window.scrollTo(0, document.body.scrollHeight)', respond=False)
@@ -21,7 +21,8 @@ async def chat_messages(name_input: ui.input) -> None:
 @ui.page('/')
 async def main(client: Client):
     def send() -> None:
-        messages.append((name.value, text.value))
+        stamp = datetime.utcnow().strftime('%X')
+        messages.append((name.value, text.value, stamp))
         text.value = ''
         chat_messages.refresh()
 

+ 32 - 0
examples/pandas_dataframe/main.py

@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+import pandas as pd
+from pandas.api.types import is_bool_dtype, is_numeric_dtype
+
+from nicegui import ui
+
+df = pd.DataFrame(data={
+    'col1': [x for x in range(4)],
+    'col2': ['This', 'column', 'contains', 'strings.'],
+    'col3': [x / 4 for x in range(4)],
+    'col4': [True, False, True, False],
+})
+
+
+def update(*, df: pd.DataFrame, r: int, c: int, value):
+    df.iat[r, c] = value
+    ui.notify(f'Set ({r}, {c}) to {value}')
+
+
+with ui.grid(rows=len(df.index)+1).classes('grid-flow-col'):
+    for c, col in enumerate(df.columns):
+        ui.label(col).classes('font-bold')
+        for r, row in enumerate(df.loc[:, col]):
+            if is_bool_dtype(df[col].dtype):
+                cls = ui.checkbox
+            elif is_numeric_dtype(df[col].dtype):
+                cls = ui.number
+            else:
+                cls = ui.input
+            cls(value=row, on_change=lambda event, r=r, c=c: update(df=df, r=r, c=c, value=event.value))
+
+ui.run()

+ 8 - 2
main.py

@@ -271,6 +271,8 @@ async def index_page(client: Client):
             example_link('Single Page App', 'navigate without reloading the page')
             example_link('Chat App', 'a simple chat app')
             example_link('SQLite Database', 'CRUD operations on a SQLite database')
+            example_link('Pandas DataFrame',
+                         'shows how to display an editable [pandas](https://pandas.pydata.org) DataFrame')
 
     with ui.row().classes('bg-primary w-full min-h-screen mt-16'):
         link_target('why')
@@ -330,9 +332,13 @@ def documentation_page_more(name: str):
     if not hasattr(ui, name):
         name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
     module = importlib.import_module(f'website.more_documentation.{name}_documentation')
-    api = getattr(ui, name)
     more = getattr(module, 'more', None)
-    back_link_target = str(api.__doc__ or api.__init__.__doc__).splitlines()[0].strip()
+    if hasattr(ui, name):
+        api = getattr(ui, name)
+        back_link_target = str(api.__doc__ or api.__init__.__doc__).splitlines()[0].strip()
+    else:
+        api = name
+        back_link_target = name
 
     add_head_html()
     add_header()

+ 2 - 2
nicegui/client.py

@@ -53,7 +53,7 @@ class Client:
     @property
     def ip(self) -> Optional[str]:
         """Return the IP address of the client, or None if the client is not connected."""
-        return self.environ.get('REMOTE_ADDR') if self.environ else None
+        return self.environ['asgi.scope']['client'][0] if self.environ else None
 
     @property
     def has_socket_connection(self) -> bool:
@@ -68,7 +68,7 @@ class Client:
         self.content.__exit__()
 
     def build_response(self, request: Request, status_code: int = 200) -> Response:
-        prefix = request.headers.get('X-Forwarded-Prefix', '')
+        prefix = request.headers.get('X-Forwarded-Prefix', request.scope.get('root_path', ''))
         vue_html, vue_styles, vue_scripts = generate_vue_content()
         elements = json.dumps({id: element._to_dict() for id, element in self.elements.items()})
         return templates.TemplateResponse('index.html', {

+ 14 - 0
nicegui/element.py

@@ -266,6 +266,20 @@ class Element(Visibility):
             slot.children.clear()
         self.update()
 
+    def move(self, target_container: Optional[Element] = None, target_index: int = -1):
+        """Move the element to another container.
+
+        :param target_container: container to move the element to (default: the parent container)
+        :param target_index: index within the target slot (default: append to the end)
+        """
+        self.parent_slot.children.remove(self)
+        self.parent_slot.parent.update()
+        target_container = target_container or self.parent_slot.parent
+        target_index = target_index if target_index >= 0 else len(target_container.default_slot.children)
+        target_container.default_slot.children.insert(target_index, self)
+        self.parent_slot = target_container.default_slot
+        target_container.update()
+
     def remove(self, element: Union[Element, int]) -> None:
         """Remove a child element.
 

+ 8 - 0
nicegui/elements/button.py

@@ -1,3 +1,4 @@
+import asyncio
 from typing import Callable, Optional
 
 from ..colors import set_background_color
@@ -34,3 +35,10 @@ class Button(TextElement, DisableableElement):
 
     def _text_to_model_text(self, text: str) -> None:
         self._props['label'] = text
+
+    async def clicked(self) -> None:
+        """Wait until the button is clicked."""
+        event = asyncio.Event()
+        self.on('click', event.set)
+        await self.client.connected()
+        await event.wait()

+ 2 - 2
nicegui/elements/chat_message.js

@@ -1,7 +1,7 @@
 export default {
   template: `
     <q-chat-message
-      :text="[text]"
+      :text="text"
       :name="name"
       :label="label"
       :stamp="stamp"
@@ -10,7 +10,7 @@ export default {
     />
   `,
   props: {
-    text: String,
+    text: Array,
     name: String,
     label: String,
     stamp: String,

+ 14 - 3
nicegui/elements/chat_message.py

@@ -1,4 +1,5 @@
-from typing import Optional
+import html
+from typing import List, Optional, Union
 
 from ..dependencies import register_component
 from ..element import Element
@@ -9,26 +10,36 @@ register_component('chat_message', __file__, 'chat_message.js')
 class ChatMessage(Element):
 
     def __init__(self,
-                 text: str, *,
+                 text: Union[str, List[str]], *,
                  name: Optional[str] = None,
                  label: Optional[str] = None,
                  stamp: Optional[str] = None,
                  avatar: Optional[str] = None,
                  sent: bool = False,
+                 text_html: bool = False,
                  ) -> None:
         """Chat Message
 
         Based on Quasar's `Chat Message <https://quasar.dev/vue-components/chat/>`_ component.
 
-        :param text: the message body
+        :param text: the message body (can be a list of strings for multiple message parts)
         :param name: the name of the message author
         :param label: renders a label header/section only
         :param stamp: timestamp of the message
         :param avatar: URL to an avatar
         :param sent: render as a sent message (so from current user) (default: False)
+        :param text_html: render text as HTML (default: False)
         """
         super().__init__('chat_message')
+
+        if isinstance(text, str):
+            text = [text]
+        if not text_html:
+            text = [html.escape(part) for part in text]
+            text = [part.replace('\n', '<br />') for part in text]
         self._props['text'] = text
+        self._props['text-html'] = True
+
         if name is not None:
             self._props['name'] = name
         if label is not None:

+ 2 - 1
nicegui/elements/input.py

@@ -57,8 +57,9 @@ class Input(ValueElement, DisableableElement):
         if autocomplete:
             def find_autocompletion() -> Optional[str]:
                 if self.value:
+                    needle = str(self.value).casefold()
                     for item in autocomplete:
-                        if item.startswith(self.value):
+                        if item.casefold().startswith(needle):
                             return item
 
             def autocomplete_input() -> None:

+ 2 - 2
nicegui/elements/log.py

@@ -1,3 +1,4 @@
+import urllib.parse
 from collections import deque
 from typing import Any, Optional
 
@@ -24,8 +25,7 @@ class Log(Element):
         self.lines: deque[str] = deque(maxlen=max_lines)
 
     def push(self, line: Any) -> None:
-        line = str(line)
-        self.lines.extend(line.splitlines())
+        self.lines.extend(map(urllib.parse.quote, str(line).splitlines()))
         self._props['lines'] = '\n'.join(self.lines)
         self.run_method('push', line)
 

+ 20 - 22
nicegui/functions/refreshable.py

@@ -30,36 +30,18 @@ class refreshable:
         self.prune()
         with Element('refreshable') as container:
             self.containers.append((container, args, kwargs))
-        if is_coroutine(self.func):
-            async def wait_for_result():
-                with container:
-                    await self.func(*args, **kwargs) if self.instance is None else self.func(self.instance, *args, **kwargs)
-            return wait_for_result()
-        else:
-            with container:
-                self.func(*args, **kwargs) if self.instance is None else self.func(self.instance, *args, **kwargs)
+        return self._run_in_container(container, *args, **kwargs)
 
     def refresh(self) -> None:
         self.prune()
         for container, args, kwargs in self.containers:
             container.clear()
+            result = self._run_in_container(container, *args, **kwargs)
             if is_coroutine(self.func):
-                async def wait_for_result(container: Element, args, kwargs):
-                    with container:
-                        if self.instance is None:
-                            await self.func(*args, **kwargs)
-                        else:
-                            await self.func(self.instance, *args, **kwargs)
                 if globals.loop and globals.loop.is_running():
-                    background_tasks.create(wait_for_result(container=container, args=args, kwargs=kwargs))
+                    background_tasks.create(result)
                 else:
-                    globals.app.on_startup(wait_for_result(container=container, args=args, kwargs=kwargs))
-            else:
-                with container:
-                    if self.instance is None:
-                        self.func(*args, **kwargs)
-                    else:
-                        self.func(self.instance, *args, **kwargs)
+                    globals.app.on_startup(result)
 
     def prune(self) -> None:
         self.containers = [
@@ -67,3 +49,19 @@ class refreshable:
             for container, args, kwargs in self.containers
             if container.client.id in globals.clients
         ]
+
+    def _run_in_container(self, container: Element, *args, **kwargs) -> None:
+        if is_coroutine(self.func):
+            async def wait_for_result() -> None:
+                with container:
+                    if self.instance is None:
+                        await self.func(*args, **kwargs)
+                    else:
+                        await self.func(self.instance, *args, **kwargs)
+            return wait_for_result()
+        else:
+            with container:
+                if self.instance is None:
+                    self.func(*args, **kwargs)
+                else:
+                    self.func(self.instance, *args, **kwargs)

+ 9 - 0
nicegui/static/nicegui.css

@@ -65,6 +65,7 @@
   height: 16rem;
 }
 .nicegui-log {
+  padding: 0.25rem;
   border-width: 1px;
   white-space: pre;
   font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
@@ -76,6 +77,14 @@
   padding: 1rem 1rem 0.5rem 1rem;
   margin: 1rem 0;
 }
+.nicegui-markdown th {
+  padding: 0.5rem;
+  border: 1px solid #8884;
+}
+.nicegui-markdown td {
+  padding: 0.5rem;
+  border: 1px solid #8884;
+}
 
 #popup {
   position: fixed;

+ 2 - 2
tests/test_auto_context.py

@@ -92,7 +92,7 @@ def test_autoupdate_on_async_event_handler(screen: Screen):
 def test_autoupdate_on_async_timer_callback(screen: Screen):
     async def update():
         ui.label('1')
-        await asyncio.sleep(2.0)
+        await asyncio.sleep(3.0)
         ui.label('2')
     ui.label('0')
     ui.timer(2.0, update, once=True)
@@ -100,7 +100,7 @@ def test_autoupdate_on_async_timer_callback(screen: Screen):
     app.on_startup(lambda: ui.label('connection established'))  # HACK: allow waiting for client connection
 
     screen.open('/')
-    with screen.implicitly_wait(20.0):
+    with screen.implicitly_wait(10.0):
         screen.wait_for('connection established')
     screen.should_contain('0')
     screen.should_not_contain('1')

+ 27 - 0
tests/test_chat.py

@@ -0,0 +1,27 @@
+from selenium.webdriver.common.by import By
+
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_no_html(screen: Screen):
+    ui.chat_message('<strong>HTML</strong>')
+
+    screen.open('/')
+    screen.should_contain('<strong>HTML</strong>')
+
+
+def test_html(screen: Screen):
+    ui.chat_message('<strong>HTML</strong>', text_html=True)
+
+    screen.open('/')
+    screen.should_contain('HTML')
+    screen.should_not_contain('<strong>HTML</strong>')
+
+
+def test_newline(screen: Screen):
+    ui.chat_message('Hello\nNiceGUI!')
+
+    screen.open('/')
+    assert screen.find('Hello').find_element(By.TAG_NAME, 'br')

+ 25 - 0
tests/test_element.py

@@ -136,3 +136,28 @@ def test_remove_and_clear(screen: Screen):
     screen.should_not_contain('Label A')
     screen.should_not_contain('Label B')
     screen.should_not_contain('Label C')
+
+
+def test_move(screen: Screen):
+    with ui.card() as a:
+        ui.label('A')
+        x = ui.label('X')
+
+    with ui.card() as b:
+        ui.label('B')
+
+    ui.button('Move X to A', on_click=lambda: x.move(a))
+    ui.button('Move X to B', on_click=lambda: x.move(b))
+    ui.button('Move X to top', on_click=lambda: x.move(target_index=0))
+
+    screen.open('/')
+    assert screen.find('A').location['y'] < screen.find('X').location['y'] < screen.find('B').location['y']
+    screen.click('Move X to B')
+    screen.wait(0.5)
+    assert screen.find('A').location['y'] < screen.find('B').location['y'] < screen.find('X').location['y']
+    screen.click('Move X to A')
+    screen.wait(0.5)
+    assert screen.find('A').location['y'] < screen.find('X').location['y'] < screen.find('B').location['y']
+    screen.click('Move X to top')
+    screen.wait(0.5)
+    assert screen.find('X').location['y'] < screen.find('A').location['y'] < screen.find('B').location['y']

+ 4 - 0
tests/test_input.py

@@ -96,11 +96,15 @@ def test_autocompletion(screen: Screen):
     screen.should_contain('oo')
 
     element.send_keys(Keys.TAB)
+    screen.wait(0.2)
     assert element.get_attribute('value') == 'foo'
 
     element.send_keys(Keys.BACKSPACE)
+    screen.wait(0.2)
     element.send_keys(Keys.BACKSPACE)
+    screen.wait(0.2)
     element.send_keys('x')
+    screen.wait(0.2)
     element.send_keys(Keys.TAB)
     screen.wait(0.5)
     assert element.get_attribute('value') == 'fx'

+ 7 - 0
tests/test_log.py

@@ -43,3 +43,10 @@ def test_replace_log(screen: Screen):
     screen.click('Replace')
     screen.should_contain('B')
     screen.should_not_contain('A')
+
+
+def test_special_characters(screen: Screen):
+    ui.log().push('100%')
+
+    screen.open('/')
+    screen.should_contain('100%')

+ 3 - 0
tests/test_refreshable.py

@@ -1,3 +1,5 @@
+import asyncio
+
 from nicegui import ui
 
 from .screen import Screen
@@ -34,6 +36,7 @@ async def test_async_refreshable(screen: Screen) -> None:
 
     @ui.refreshable
     async def number_ui() -> None:
+        await asyncio.sleep(0.1)
         ui.label('[' + ', '.join(str(n) for n in sorted(numbers)) + ']')
 
     @ui.page('/')

+ 4 - 0
website/demo.py

@@ -25,6 +25,10 @@ def demo(f: Callable) -> Callable:
         while not code[0].strip().startswith('def') and not code[0].strip().startswith('async def'):
             del code[0]
         del code[0]
+        if code[0].strip().startswith('"""'):
+            while code[0].strip() != '"""':
+                del code[0]
+            del code[0]
         indentation = len(code[0]) - len(code[0].lstrip())
         code = [line[indentation:] for line in code]
         code = ['from nicegui import ui'] + [remove_prefix(line, '# ') for line in code]

+ 1 - 56
website/documentation.py

@@ -316,26 +316,7 @@ def create_full() -> None:
 
     load_demo(ui.timer)
     load_demo(ui.keyboard)
-
-    @text_demo('Bindings', '''
-        NiceGUI is able to directly bind UI elements to models.
-        Binding is possible for UI element properties like text, value or visibility and for model properties that are (nested) class attributes.
-
-        Each element provides methods like `bind_value` and `bind_visibility` to create a two-way binding with the corresponding property.
-        To define a one-way binding use the `_from` and `_to` variants of these methods.
-        Just pass a property of the model as parameter to these methods to create the binding.
-    ''')
-    def bindings_demo():
-        class Demo:
-            def __init__(self):
-                self.number = 1
-
-        demo = Demo()
-        v = ui.checkbox('visible', value=True)
-        with ui.column().bind_visibility_from(v, 'value'):
-            ui.slider(min=1, max=3).bind_value(demo, 'number')
-            ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(demo, 'number')
-            ui.number().bind_value(demo, 'number')
+    load_demo('bindings')
 
     @text_demo('UI Updates', '''
         NiceGUI tries to automatically synchronize the state of UI elements with the client, e.g. when a label text, an input value or style/classes/props of an element have changed.
@@ -403,42 +384,6 @@ def create_full() -> None:
         ui.label(f'shared auto-index page with ID {CONSTANT_UUID}')
         ui.link('private page', private_page)
 
-    @text_demo('Pages with Path Parameters', '''
-        Page routes can contain parameters like [FastAPI](https://fastapi.tiangolo.com/tutorial/path-params/>).
-        If type-annotated, they are automatically converted to bool, int, float and complex values.
-        If the page function expects a `request` argument, the request object is automatically provided.
-        The `client` argument provides access to the websocket connection, layout, etc.
-    ''')
-    def page_with_path_parameters_demo():
-        @ui.page('/repeat/{word}/{count}')
-        def page(word: str, count: int):
-            ui.label(word * count)
-
-        ui.link('Say hi to Santa!', 'repeat/Ho! /3')
-
-    @text_demo('Wait for Client Connection', '''
-        To wait for a client connection, you can add a `client` argument to the decorated page function
-        and await `client.connected()`.
-        All code below that statement is executed after the websocket connection between server and client has been established.
-
-        For example, this allows you to run JavaScript commands; which is only possible with a client connection (see [#112](https://github.com/zauberzeug/nicegui/issues/112)).
-        Also it is possible to do async stuff while the user already sees some content.
-    ''')
-    def wait_for_connected_demo():
-        import asyncio
-
-        from nicegui import Client
-
-        @ui.page('/wait_for_connection')
-        async def wait_for_connection(client: Client):
-            ui.label('This text is displayed immediately.')
-            await client.connected()
-            await asyncio.sleep(2)
-            ui.label('This text is displayed 2 seconds after the page has been fully loaded.')
-            ui.label(f'The IP address {client.ip} was obtained from the websocket.')
-
-        ui.link('wait for connection', wait_for_connection)
-
     @text_demo('Page Layout', '''
         With `ui.header`, `ui.footer`, `ui.left_drawer` and `ui.right_drawer` you can add additional layout elements to a page.
         The `fixed` argument controls whether the element should scroll or stay fixed on the screen.

+ 11 - 6
website/documentation_tools.py

@@ -38,8 +38,7 @@ def subheading(text: str, *, make_menu_entry: bool = True, more_link: Optional[s
     ui.html(f'<div id="{name}"></div>').style('position: relative; top: -90px')
     with ui.row().classes('gap-2 items-center relative'):
         if more_link:
-            with ui.link(text, f'documentation/{more_link}').classes('text-2xl'):
-                ui.icon('open_in_new', size='0.75em').classes('mb-1 ml-2')
+            ui.link(text, f'documentation/{more_link}').classes('text-2xl')
         else:
             ui.label(text).classes('text-2xl')
         with ui.link(target=f'#{name}').classes('absolute').style('transform: translateX(-150%)'):
@@ -87,7 +86,10 @@ class intro_demo(text_demo):
 
 class element_demo:
 
-    def __init__(self, element_class: Union[Callable, type]) -> None:
+    def __init__(self, element_class: Union[Callable, type, str]) -> None:
+        if isinstance(element_class, str):
+            module = importlib.import_module(f'website.more_documentation.{element_class}_documentation')
+            element_class = getattr(module, 'main_demo')
         self.element_class = element_class
 
     def __call__(self, f: Callable, *, more_link: Optional[str] = None) -> Callable:
@@ -96,11 +98,14 @@ class element_demo:
         with ui.column().classes('w-full mb-8 gap-2'):
             subheading(title, more_link=more_link)
             render_docstring(documentation, with_params=more_link is None)
-            return demo(f)
+            result = demo(f)
+            if more_link:
+                ui.markdown(f'See [more...](documentation/{more_link})').classes('bold-links arrow-links')
+        return result
 
 
-def load_demo(api: Union[type, Callable]) -> None:
-    name = pascal_to_snake(api.__name__)
+def load_demo(api: Union[type, Callable, str]) -> None:
+    name = pascal_to_snake(api if isinstance(api, str) else api.__name__)
     try:
         module = importlib.import_module(f'website.more_documentation.{name}_documentation')
     except ModuleNotFoundError:

+ 54 - 0
website/more_documentation/bindings_documentation.py

@@ -0,0 +1,54 @@
+from nicegui import ui
+
+from ..documentation_tools import text_demo
+
+
+def main_demo() -> None:
+    """Bindings
+
+    NiceGUI is able to directly bind UI elements to models.
+    Binding is possible for UI element properties like text, value or visibility and for model properties that are (nested) class attributes.
+    Each element provides methods like `bind_value` and `bind_visibility` to create a two-way binding with the corresponding property.
+    To define a one-way binding use the `_from` and `_to` variants of these methods.
+    Just pass a property of the model as parameter to these methods to create the binding.
+    """
+    class Demo:
+        def __init__(self):
+            self.number = 1
+
+    demo = Demo()
+    v = ui.checkbox('visible', value=True)
+    with ui.column().bind_visibility_from(v, 'value'):
+        ui.slider(min=1, max=3).bind_value(demo, 'number')
+        ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(demo, 'number')
+        ui.number().bind_value(demo, 'number')
+
+
+date = '2023-01-01'
+
+
+def more() -> None:
+    @text_demo('Bind to dictionary', '''
+        Here we are binding the text of labels to a dictionary.
+    ''')
+    def bind_dictionary():
+        data = {'name': 'Bob', 'age': 17}
+
+        ui.label().bind_text_from(data, 'name', backward=lambda n: f'Name: {n}')
+        ui.label().bind_text_from(data, 'age', backward=lambda a: f'Age: {a}')
+
+        ui.button('Turn 18', on_click=lambda: data.update(age=18))
+
+    @text_demo('Bind to variable', '''
+        Here we are binding the value from the datepicker to a bare variable.
+        Therefore we use the dictionary `globals()` which contains all global variables.
+        This demo is based on the [official datepicker example](https://nicegui.io/documentation/date#input_element_with_date_picker).
+    ''')
+    def bind_variable():
+        # date = '2023-01-01'
+
+        with ui.input('Date').bind_value(globals(), 'date') as date_input:
+            with ui.menu() as menu:
+                ui.date(on_change=lambda: ui.notify(f'Date: {date}')).bind_value(date_input)
+            with date_input.add_slot('append'):
+                ui.icon('edit_calendar').on('click', menu.open).classes('cursor-pointer')

+ 18 - 0
website/more_documentation/button_documentation.py

@@ -1,5 +1,23 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     ui.button('Click me!', on_click=lambda: ui.notify(f'You clicked me!'))
+
+
+def more() -> None:
+    @text_demo('Await button click', '''
+        Sometimes it is convenient to wait for a button click before continuing the execution.
+    ''')
+    async def await_button_click() -> None:
+        # @ui.page('/')
+        # async def index():
+            b = ui.button('Step')
+            await b.clicked()
+            ui.label('One')
+            await b.clicked()
+            ui.label('Two')
+            await b.clicked()
+            ui.label('Three')

+ 23 - 0
website/more_documentation/chat_message_documentation.py

@@ -1,8 +1,31 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     ui.chat_message('Hello NiceGUI!',
                     name='Robot',
                     stamp='now',
                     avatar='https://robohash.org/ui')
+
+
+def more() -> None:
+    @text_demo('HTML text', '''
+        Using the `text_html` parameter, you can send HTML text to the chat.
+    ''')
+    def html_text():
+        ui.chat_message('Without <strong>HTML</strong>')
+        ui.chat_message('With <strong>HTML</strong>', text_html=True)
+
+    @text_demo('Newline', '''
+        You can use newlines in the chat message.
+    ''')
+    def newline():
+        ui.chat_message('This is a\nlong line!')
+
+    @text_demo('Multi-part messages', '''
+        You can send multiple message parts by passing a list of strings.
+    ''')
+    def multiple_messages():
+        ui.chat_message(['Hi! 😀', 'How are you?'])

+ 19 - 0
website/more_documentation/element_documentation.py

@@ -1,6 +1,25 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     with ui.element('div').classes('p-2 bg-blue-100'):
         ui.label('inside a colored div')
+
+
+def more() -> None:
+    @text_demo('Move elements', '''
+        This demo shows how to move elements between or within containers.
+    ''')
+    def move_elements() -> None:
+        with ui.card() as a:
+            ui.label('A')
+            x = ui.label('X')
+
+        with ui.card() as b:
+            ui.label('B')
+
+        ui.button('Move X to A', on_click=lambda: x.move(a))
+        ui.button('Move X to B', on_click=lambda: x.move(b))
+        ui.button('Move X to top', on_click=lambda: x.move(target_index=0))

+ 12 - 0
website/more_documentation/markdown_documentation.py

@@ -39,3 +39,15 @@ def more() -> None:
             ui.run(dark=True)
             ```
         ''')
+
+    @text_demo('Markdown tables', '''
+        By activating the "tables" extra, you can use Markdown tables.
+        See the [markdown2 documentation](https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras) for a list of available extras.
+    ''')
+    def markdown_with_code_blocks():
+        ui.markdown('''
+            | First name | Last name |
+            | ---------- | --------- |
+            | Max        | Planck    |
+            | Marie      | Curie     |
+        ''', extras=['tables'])

+ 40 - 0
website/more_documentation/page_documentation.py

@@ -1,5 +1,7 @@
 from nicegui import ui
 
+from ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     @ui.page('/other_page')
@@ -14,3 +16,41 @@ def main_demo() -> None:
 
     ui.link('Visit other page', other_page)
     ui.link('Visit dark page', dark_page)
+
+
+def more() -> None:
+    @text_demo('Pages with Path Parameters', '''
+        Page routes can contain parameters like [FastAPI](https://fastapi.tiangolo.com/tutorial/path-params/>).
+        If type-annotated, they are automatically converted to bool, int, float and complex values.
+        If the page function expects a `request` argument, the request object is automatically provided.
+        The `client` argument provides access to the websocket connection, layout, etc.
+    ''')
+    def page_with_path_parameters_demo():
+        @ui.page('/repeat/{word}/{count}')
+        def page(word: str, count: int):
+            ui.label(word * count)
+
+        ui.link('Say hi to Santa!', 'repeat/Ho! /3')
+
+    @text_demo('Wait for Client Connection', '''
+        To wait for a client connection, you can add a `client` argument to the decorated page function
+        and await `client.connected()`.
+        All code below that statement is executed after the websocket connection between server and client has been established.
+
+        For example, this allows you to run JavaScript commands; which is only possible with a client connection (see [#112](https://github.com/zauberzeug/nicegui/issues/112)).
+        Also it is possible to do async stuff while the user already sees some content.
+    ''')
+    def wait_for_connected_demo():
+        import asyncio
+
+        from nicegui import Client
+
+        @ui.page('/wait_for_connection')
+        async def wait_for_connection(client: Client):
+            ui.label('This text is displayed immediately.')
+            await client.connected()
+            await asyncio.sleep(2)
+            ui.label('This text is displayed 2 seconds after the page has been fully loaded.')
+            ui.label(f'The IP address {client.ip} was obtained from the websocket.')
+
+        ui.link('wait for connection', wait_for_connection)

+ 14 - 1
website/more_documentation/query_documentation.py

@@ -1,4 +1,6 @@
-from nicegui import ui
+from nicegui import globals, ui
+
+from ..documentation_tools import text_demo
 
 
 def main_demo() -> None:
@@ -10,3 +12,14 @@ def main_demo() -> None:
     # END OF DEMO
     ui.button('Blue', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ddeeff'))
     ui.button('Orange', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ffeedd'))
+
+
+def more() -> None:
+    @text_demo('Set background gradient', '''
+        It's easy to set a background gradient, image or similar. 
+        See [w3schools.com](https://www.w3schools.com/cssref/pr_background-image.php) for more information about setting background with CSS.
+    ''')
+    def background_image():
+        # ui.query('body').classes('bg-gradient-to-t from-blue-400 to-blue-100')
+        # END OF DEMO
+        globals.get_slot_stack()[-1].parent.classes('bg-gradient-to-t from-blue-400 to-blue-100')

+ 17 - 0
website/more_documentation/upload_documentation.py

@@ -16,3 +16,20 @@ def more() -> None:
         ui.upload(on_upload=lambda e: ui.notify(f'Uploaded {e.name}'),
                   on_rejected=lambda: ui.notify('Rejected!'),
                   max_file_size=1_000_000).classes('max-w-full')
+
+    @text_demo('Show file content', '''
+        In this demo, the uploaded markdown file is shown in a dialog.
+    ''')
+    def show_file_content() -> None:
+        from nicegui import events
+
+        with ui.dialog().props('full-width') as dialog:
+            with ui.card():
+                content = ui.markdown()
+
+        def handle_upload(e: events.UploadEventArguments):
+            text = e.content.read().decode('utf-8')
+            content.set_content(text)
+            dialog.open()
+
+        ui.upload(on_upload=handle_upload).props('accept=.md').classes('max-w-full')