ソースを参照

Merge branch 'main' into feature/dependencies

# Conflicts:
#	nicegui/elements/interactive_image.py
#	nicegui/elements/keyboard.py
#	nicegui/elements/link.py
#	nicegui/elements/upload.py
#	nicegui/functions/refreshable.py
Falko Schindler 2 年 前
コミット
cdccbcc35a
68 ファイル変更745 行追加194 行削除
  1. 1 1
      .github/workflows/test.yml
  2. 109 0
      examples/simpy/async_realtime_environment.py
  3. 49 0
      examples/simpy/main.py
  4. 2 0
      examples/simpy/requirements.txt
  5. 65 0
      examples/todo_list/main.py
  6. 13 11
      main.py
  7. 10 0
      nicegui/__init__.py
  8. 6 6
      nicegui/background_tasks.py
  9. 5 5
      nicegui/binding.py
  10. 5 5
      nicegui/client.py
  11. 13 9
      nicegui/element.py
  12. 2 2
      nicegui/elements/button.py
  13. 9 2
      nicegui/elements/chart.js
  14. 2 2
      nicegui/elements/checkbox.py
  15. 5 1
      nicegui/elements/choice_element.py
  16. 7 3
      nicegui/elements/color_input.py
  17. 2 2
      nicegui/elements/color_picker.py
  18. 2 2
      nicegui/elements/date.py
  19. 2 2
      nicegui/elements/input.py
  20. 7 3
      nicegui/elements/interactive_image.py
  21. 3 3
      nicegui/elements/joystick.py
  22. 2 2
      nicegui/elements/keyboard.py
  23. 9 3
      nicegui/elements/line_plot.py
  24. 2 2
      nicegui/elements/link.py
  25. 6 2
      nicegui/elements/menu.py
  26. 8 5
      nicegui/elements/mixins/content_element.py
  27. 8 5
      nicegui/elements/mixins/disableable_element.py
  28. 8 5
      nicegui/elements/mixins/filter_element.py
  29. 8 5
      nicegui/elements/mixins/source_element.py
  30. 8 5
      nicegui/elements/mixins/text_element.py
  31. 13 5
      nicegui/elements/mixins/value_element.py
  32. 8 6
      nicegui/elements/mixins/visibility.py
  33. 3 2
      nicegui/elements/number.py
  34. 2 1
      nicegui/elements/pyplot.py
  35. 5 1
      nicegui/elements/radio.py
  36. 2 2
      nicegui/elements/scene.py
  37. 3 2
      nicegui/elements/select.py
  38. 3 2
      nicegui/elements/slider.py
  39. 3 2
      nicegui/elements/splitter.py
  40. 2 2
      nicegui/elements/switch.py
  41. 2 2
      nicegui/elements/table.py
  42. 3 2
      nicegui/elements/tabs.py
  43. 4 3
      nicegui/elements/textarea.py
  44. 4 4
      nicegui/elements/time.py
  45. 5 1
      nicegui/elements/toggle.py
  46. 4 3
      nicegui/elements/tree.py
  47. 3 3
      nicegui/elements/upload.py
  48. 2 2
      nicegui/events.py
  49. 1 1
      nicegui/functions/notify.py
  50. 2 2
      nicegui/functions/open.py
  51. 40 32
      nicegui/functions/refreshable.py
  52. 10 4
      nicegui/functions/timer.py
  53. 7 7
      nicegui/globals.py
  54. 1 1
      nicegui/helpers.py
  55. 3 3
      nicegui/page.py
  56. 2 2
      nicegui/run.py
  57. 4 1
      nicegui/slot.py
  58. 8 1
      nicegui/templates/index.html
  59. 86 0
      nicegui/ui.py
  60. 22 0
      tests/test_chart.py
  61. 14 0
      tests/test_date.py
  62. 37 0
      tests/test_refreshable.py
  63. 1 0
      website/documentation.py
  64. 2 2
      website/example_card.py
  65. 12 0
      website/more_documentation/button_documentation.py
  66. 15 0
      website/more_documentation/column_documentation.py
  67. 8 0
      website/more_documentation/date_documentation.py
  68. 21 0
      website/more_documentation/label_documentation.py

+ 1 - 1
.github/workflows/test.yml

@@ -25,7 +25,7 @@ jobs:
           poetry config virtualenvs.create false
           poetry config virtualenvs.create false
           poetry install
           poetry install
           # install packages to run the examples
           # install packages to run the examples
-          pip install opencv-python opencv-contrib-python-headless httpx replicate langchain openai
+          pip install opencv-python opencv-contrib-python-headless httpx replicate langchain openai simpy
           # try fix issue with importlib_resources
           # try fix issue with importlib_resources
           pip install importlib-resources
           pip install importlib-resources
       - name: test startup
       - name: test startup

+ 109 - 0
examples/simpy/async_realtime_environment.py

@@ -0,0 +1,109 @@
+import asyncio
+from time import monotonic
+from typing import Any, Optional, Union
+
+from numpy import Infinity
+from simpy.core import EmptySchedule, Environment, Infinity, SimTime, StopSimulation
+from simpy.events import URGENT, Event
+from simpy.rt import RealtimeEnvironment
+
+
+class AsyncRealtimeEnvironment(RealtimeEnvironment):
+    """A real-time simulation environment that uses asyncio.
+
+    The methods step and run are a 1-1 copy of the original methods from simpy.rt.RealtimeEnvironment,
+    except that they are async and await asyncio.sleep instead of time.sleep.
+    """
+
+    async def step(self) -> None:
+        """Process the next event after enough real-time has passed for the
+        event to happen.
+
+        The delay is scaled according to the real-time :attr:`factor`. With
+        :attr:`strict` mode enabled, a :exc:`RuntimeError` will be raised, if
+        the event is processed too slowly.
+
+        """
+        evt_time = self.peek()
+
+        if evt_time is Infinity:
+            raise EmptySchedule()
+
+        real_time = self.real_start + (evt_time - self.env_start) * self.factor
+
+        if self.strict and monotonic() - real_time > self.factor:
+            # Events scheduled for time *t* may take just up to *t+1*
+            # for their computation, before an error is raised.
+            delta = monotonic() - real_time
+            raise RuntimeError(
+                f'Simulation too slow for real time ({delta:.3f}s).'
+            )
+
+        # Sleep in a loop to fix inaccuracies of windows (see
+        # http://stackoverflow.com/a/15967564 for details) and to ignore
+        # interrupts.
+        while True:
+            delta = real_time - monotonic()
+            if delta <= 0:
+                break
+            await asyncio.sleep(delta)
+
+        Environment.step(self)
+
+    async def run(
+        self, until: Optional[Union[SimTime, Event]] = None
+    ) -> Optional[Any]:
+        """Executes :meth:`step()` until the given criterion *until* is met.
+
+        - If it is ``None`` (which is the default), this method will return
+          when there are no further events to be processed.
+
+        - If it is an :class:`~simpy.events.Event`, the method will continue
+          stepping until this event has been triggered and will return its
+          value.  Raises a :exc:`RuntimeError` if there are no further events
+          to be processed and the *until* event was not triggered.
+
+        - If it is a number, the method will continue stepping
+          until the environment's time reaches *until*.
+
+        """
+        if until is not None:
+            if not isinstance(until, Event):
+                # Assume that *until* is a number if it is not None and
+                # not an event.  Create a Timeout(until) in this case.
+                at: SimTime
+                if isinstance(until, int):
+                    at = until
+                else:
+                    at = float(until)
+
+                if at <= self.now:
+                    raise ValueError(
+                        f'until(={at}) must be > the current simulation time.'
+                    )
+
+                # Schedule the event before all regular timeouts.
+                until = Event(self)
+                until._ok = True
+                until._value = None
+                self.schedule(until, URGENT, at - self.now)
+
+            elif until.callbacks is None:
+                # Until event has already been processed.
+                return until.value
+
+            until.callbacks.append(StopSimulation.callback)
+
+        try:
+            while True:
+                await self.step()
+        except StopSimulation as exc:
+            return exc.args[0]  # == until.value
+        except EmptySchedule:
+            if until is not None:
+                assert not until.triggered
+                raise RuntimeError(
+                    f'No scheduled events left but "until" event was not '
+                    f'triggered: {until}'
+                )
+        return None

+ 49 - 0
examples/simpy/main.py

@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+import asyncio
+import datetime
+
+from async_realtime_environment import AsyncRealtimeEnvironment
+
+from nicegui import ui
+
+start_time = datetime.datetime.now()
+
+
+def clock(env):
+    while True:
+        simulation_time = start_time + datetime.timedelta(seconds=env.now)
+        clock_label.text = simulation_time.strftime('%H:%M:%S')
+        yield env.timeout(1)
+
+
+def traffic_light(env):
+    while True:
+        light.classes('bg-green-500', remove='bg-red-500')
+        yield env.timeout(30)
+        light.classes('bg-yellow-500', remove='bg-green-500')
+        yield env.timeout(5)
+        light.classes('bg-red-500', remove='bg-yellow-500')
+        yield env.timeout(20)
+
+
+async def run_simpy():
+    env = AsyncRealtimeEnvironment(factor=0.1)  # fast forward simulation with 1/10th of realtime
+    env.process(traffic_light(env))
+    env.process(clock(env))
+    try:
+        await env.run(until=300)  # run until 300 seconds of simulation time have passed
+    except asyncio.CancelledError:
+        return
+    ui.notify('Simulation completed')
+    content.classes('opacity-0')  # fade out the content
+
+# define the UI
+with ui.column().classes('absolute-center items-center transition-opacity duration-500') as content:
+    ui.label('SimPy Traffic Light Demo').classes('text-2xl mb-6')
+    light = ui.element('div').classes('w-10 h-10 rounded-full shadow-lg transition')
+    clock_label = ui.label()
+
+# start the simpy simulation as an async task in the background as soon as the UI is ready
+ui.timer(0, run_simpy, once=True)
+
+ui.run()

+ 2 - 0
examples/simpy/requirements.txt

@@ -0,0 +1,2 @@
+nicegui>=1.2
+simpy

+ 65 - 0
examples/todo_list/main.py

@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+from dataclasses import dataclass
+from typing import List
+
+from nicegui import ui
+
+
+@dataclass
+class TodoItem:
+    name: str
+    done: bool = False
+
+
+items: List[TodoItem] = [
+    TodoItem('Buy milk', done=True),
+    TodoItem('Clean the house'),
+    TodoItem('Call mom'),
+]
+
+
+def add(name: str) -> None:
+    items.append(TodoItem(name))
+    add_input.value = None
+    render_list.refresh()
+
+
+def remove(item: TodoItem) -> None:
+    items.remove(item)
+    render_list.refresh()
+
+
+def toggle(item: TodoItem) -> None:
+    item.done = not item.done
+    render_list.refresh()
+
+
+def rename(item: TodoItem, name: str) -> None:
+    item.name = name
+    render_list.refresh()
+
+
+@ui.refreshable
+def render_list():
+    if not items:
+        ui.label('List is empty.')
+        return
+    ui.linear_progress(sum(item.done for item in items) / len(items), show_value=False)
+    with ui.row().classes('justify-center w-full'):
+        ui.label(f'Completed: {sum(item.done for item in items)}')
+        ui.label(f'Remaining: {sum(not item.done for item in items)}')
+    for item in items:
+        with ui.row().classes('items-center'):
+            ui.checkbox(value=item.done, on_change=lambda _, item=item: toggle(item))
+            input = ui.input(value=item.name).classes('flex-grow')
+            input.on('keydown.enter', lambda _, item=item, input=input: rename(item, input.value))
+            ui.button(on_click=lambda _, item=item: remove(item)).props('flat fab-mini icon=delete color=grey')
+
+
+with ui.card().classes('w-80 items-stretch'):
+    ui.label('Todo list').classes('text-semibold text-2xl')
+    render_list()
+    add_input = ui.input('New item').classes('mx-12')
+    add_input.on('keydown.enter', lambda: add(add_input.value))
+
+ui.run()

+ 13 - 11
main.py

@@ -10,10 +10,10 @@ if True:
 
 
 import os
 import os
 from pathlib import Path
 from pathlib import Path
-from typing import Optional
+from typing import Awaitable, Callable, Optional
 
 
 from fastapi import Request
 from fastapi import Request
-from fastapi.responses import FileResponse, RedirectResponse
+from fastapi.responses import FileResponse, RedirectResponse, Response
 from starlette.middleware.sessions import SessionMiddleware
 from starlette.middleware.sessions import SessionMiddleware
 
 
 import prometheus
 import prometheus
@@ -36,17 +36,18 @@ app.add_static_files('/fonts', str(Path(__file__).parent / 'website' / 'fonts'))
 
 
 
 
 @app.get('/logo.png')
 @app.get('/logo.png')
-def logo():
+def logo() -> FileResponse:
     return FileResponse(svg.PATH / 'logo.png', media_type='image/png')
     return FileResponse(svg.PATH / 'logo.png', media_type='image/png')
 
 
 
 
 @app.get('/logo_square.png')
 @app.get('/logo_square.png')
-def logo():
+def logo_square() -> FileResponse:
     return FileResponse(svg.PATH / 'logo_square.png', media_type='image/png')
     return FileResponse(svg.PATH / 'logo_square.png', media_type='image/png')
 
 
 
 
 @app.middleware('http')
 @app.middleware('http')
-async def redirect_reference_to_documentation(request: Request, call_next):
+async def redirect_reference_to_documentation(request: Request,
+                                              call_next: Callable[[Request], Awaitable[Response]]) -> Response:
     if request.url.path == '/reference':
     if request.url.path == '/reference':
         return RedirectResponse('/documentation')
         return RedirectResponse('/documentation')
     return await call_next(request)
     return await call_next(request)
@@ -82,11 +83,11 @@ def add_header(menu: Optional[ui.left_drawer] = None) -> None:
         with ui.row().classes('max-lg:hidden'):
         with ui.row().classes('max-lg:hidden'):
             for title, target in menu_items.items():
             for title, target in menu_items.items():
                 ui.link(title, target).classes(replace='text-lg text-white')
                 ui.link(title, target).classes(replace='text-lg text-white')
-        with ui.link(target='https://discord.gg/TEpFeAaF4f').classes('max-[435px]:hidden'):
+        with ui.link(target='https://discord.gg/TEpFeAaF4f').classes('max-[435px]:hidden').tooltip('Discord'):
             svg.discord().classes('fill-white scale-125 m-1')
             svg.discord().classes('fill-white scale-125 m-1')
-        with ui.link(target='https://www.reddit.com/r/nicegui/').classes('max-[385px]:hidden'):
+        with ui.link(target='https://www.reddit.com/r/nicegui/').classes('max-[385px]:hidden').tooltip('Reddit'):
             svg.reddit().classes('fill-white scale-125 m-1')
             svg.reddit().classes('fill-white scale-125 m-1')
-        with ui.link(target='https://github.com/zauberzeug/nicegui/'):
+        with ui.link(target='https://github.com/zauberzeug/nicegui/').tooltip('GitHub'):
             svg.github().classes('fill-white scale-125 m-1')
             svg.github().classes('fill-white scale-125 m-1')
         add_star().classes('max-[480px]:hidden')
         add_star().classes('max-[480px]:hidden')
         with ui.row().classes('lg:hidden'):
         with ui.row().classes('lg:hidden'):
@@ -97,7 +98,7 @@ def add_header(menu: Optional[ui.left_drawer] = None) -> None:
 
 
 
 
 @ui.page('/')
 @ui.page('/')
-async def index_page(client: Client):
+async def index_page(client: Client) -> None:
     client.content.classes('p-0 gap-0')
     client.content.classes('p-0 gap-0')
     add_head_html()
     add_head_html()
     add_header()
     add_header()
@@ -269,6 +270,7 @@ async def index_page(client: Client):
             example_link('Local File Picker', 'demonstrates a dialog for selecting files locally on the server')
             example_link('Local File Picker', 'demonstrates a dialog for selecting files locally on the server')
             example_link('Search as you type', 'using public API of thecocktaildb.com to search for cocktails')
             example_link('Search as you type', 'using public API of thecocktaildb.com to search for cocktails')
             example_link('Menu and Tabs', 'uses Quasar to create foldable menu and tabs inside a header bar')
             example_link('Menu and Tabs', 'uses Quasar to create foldable menu and tabs inside a header bar')
+            example_link('Todo list', 'shows a simple todo list with checkboxes and text input')
             example_link('Trello Cards', 'shows Trello-like cards that can be dragged and dropped into columns')
             example_link('Trello Cards', 'shows Trello-like cards that can be dragged and dropped into columns')
             example_link('Slots', 'shows how to use scoped slots to customize Quasar elements')
             example_link('Slots', 'shows how to use scoped slots to customize Quasar elements')
             example_link('Table and slots', 'shows how to use component slots in a table')
             example_link('Table and slots', 'shows how to use component slots in a table')
@@ -317,7 +319,7 @@ async def index_page(client: Client):
 
 
 
 
 @ui.page('/documentation')
 @ui.page('/documentation')
-def documentation_page():
+def documentation_page() -> None:
     add_head_html()
     add_head_html()
     menu = side_menu()
     menu = side_menu()
     add_header(menu)
     add_header(menu)
@@ -332,7 +334,7 @@ def documentation_page():
 
 
 
 
 @ui.page('/documentation/{name}')
 @ui.page('/documentation/{name}')
-async def documentation_page_more(name: str, client: Client):
+async def documentation_page_more(name: str, client: Client) -> None:
     if not hasattr(ui, name):
     if not hasattr(ui, name):
         name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
         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')
     module = importlib.import_module(f'website.more_documentation.{name}_documentation')

+ 10 - 0
nicegui/__init__.py

@@ -9,3 +9,13 @@ from . import elements, globals, ui
 from .client import Client
 from .client import Client
 from .nicegui import app
 from .nicegui import app
 from .tailwind import Tailwind
 from .tailwind import Tailwind
+
+__all__ = [
+    'app',
+    'Client',
+    'elements',
+    'globals',
+    'Tailwind',
+    'ui',
+    '__version__',
+]

+ 6 - 6
nicegui/background_tasks.py

@@ -1,4 +1,4 @@
-'''inspired from https://quantlane.com/blog/ensure-asyncio-task-exceptions-get-logged/'''
+"""inspired from https://quantlane.com/blog/ensure-asyncio-task-exceptions-get-logged/"""
 import asyncio
 import asyncio
 import sys
 import sys
 from typing import Awaitable, Dict, Set, TypeVar
 from typing import Awaitable, Dict, Set, TypeVar
@@ -15,12 +15,12 @@ lazy_tasks_waiting: Dict[str, Awaitable[T]] = {}
 
 
 
 
 def create(coroutine: Awaitable[T], *, name: str = 'unnamed task') -> 'asyncio.Task[T]':
 def create(coroutine: Awaitable[T], *, name: str = 'unnamed task') -> 'asyncio.Task[T]':
-    '''Wraps a loop.create_task call and ensures there is an exception handler added to the task.
+    """Wraps a loop.create_task call and ensures there is an exception handler added to the task.
 
 
     If the task raises an exception, it is logged and handled by the global exception handlers.
     If the task raises an exception, it is logged and handled by the global exception handlers.
     Also a reference to the task is kept until it is done, so that the task is not garbage collected mid-execution.
     Also a reference to the task is kept until it is done, so that the task is not garbage collected mid-execution.
     See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task.
     See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task.
-    '''
+    """
     task = globals.loop.create_task(coroutine, name=name) if name_supported else globals.loop.create_task(coroutine)
     task = globals.loop.create_task(coroutine, name=name) if name_supported else globals.loop.create_task(coroutine)
     task.add_done_callback(_handle_task_result)
     task.add_done_callback(_handle_task_result)
     running_tasks.add(task)
     running_tasks.add(task)
@@ -28,11 +28,11 @@ def create(coroutine: Awaitable[T], *, name: str = 'unnamed task') -> 'asyncio.T
     return task
     return task
 
 
 
 
-def create_lazy(coroutine: Awaitable[T], *, name: str) -> 'asyncio.Task[T]':
-    '''Wraps a create call and ensures a second task with the same name is delayed until the first one is done.
+def create_lazy(coroutine: Awaitable[T], *, name: str) -> None:
+    """Wraps a create call and ensures a second task with the same name is delayed until the first one is done.
 
 
     If a third task with the same name is created while the first one is still running, the second one is discarded.
     If a third task with the same name is created while the first one is still running, the second one is discarded.
-    '''
+    """
     if name in lazy_tasks_running:
     if name in lazy_tasks_running:
         lazy_tasks_waiting[name] = coroutine
         lazy_tasks_waiting[name] = coroutine
         return
         return

+ 5 - 5
nicegui/binding.py

@@ -8,7 +8,7 @@ from . import globals
 
 
 bindings: DefaultDict[Tuple[int, str], List] = defaultdict(list)
 bindings: DefaultDict[Tuple[int, str], List] = defaultdict(list)
 bindable_properties: Dict[Tuple[int, str], Any] = {}
 bindable_properties: Dict[Tuple[int, str], Any] = {}
-active_links: List[Tuple[Any, str, Any, str, Callable]] = []
+active_links: List[Tuple[Any, str, Any, str, Callable[[Any], Any]]] = []
 
 
 
 
 def get_attribute(obj: Union[object, Dict], name: str) -> Any:
 def get_attribute(obj: Union[object, Dict], name: str) -> Any:
@@ -54,14 +54,14 @@ def propagate(source_obj: Any, source_name: str, visited: Optional[Set[Tuple[int
             propagate(target_obj, target_name, visited)
             propagate(target_obj, target_name, visited)
 
 
 
 
-def bind_to(self_obj: Any, self_name: str, other_obj: Any, other_name: str, forward: Callable) -> None:
+def bind_to(self_obj: Any, self_name: str, other_obj: Any, other_name: str, forward: Callable[[Any], Any]) -> None:
     bindings[(id(self_obj), self_name)].append((self_obj, other_obj, other_name, forward))
     bindings[(id(self_obj), self_name)].append((self_obj, other_obj, other_name, forward))
     if (id(self_obj), self_name) not in bindable_properties:
     if (id(self_obj), self_name) not in bindable_properties:
         active_links.append((self_obj, self_name, other_obj, other_name, forward))
         active_links.append((self_obj, self_name, other_obj, other_name, forward))
     propagate(self_obj, self_name)
     propagate(self_obj, self_name)
 
 
 
 
-def bind_from(self_obj: Any, self_name: str, other_obj: Any, other_name: str, backward: Callable) -> None:
+def bind_from(self_obj: Any, self_name: str, other_obj: Any, other_name: str, backward: Callable[[Any], Any]) -> None:
     bindings[(id(other_obj), other_name)].append((other_obj, self_obj, self_name, backward))
     bindings[(id(other_obj), other_name)].append((other_obj, self_obj, self_name, backward))
     if (id(other_obj), other_name) not in bindable_properties:
     if (id(other_obj), other_name) not in bindable_properties:
         active_links.append((other_obj, other_name, self_obj, self_name, backward))
         active_links.append((other_obj, other_name, self_obj, self_name, backward))
@@ -69,14 +69,14 @@ def bind_from(self_obj: Any, self_name: str, other_obj: Any, other_name: str, ba
 
 
 
 
 def bind(self_obj: Any, self_name: str, other_obj: Any, other_name: str, *,
 def bind(self_obj: Any, self_name: str, other_obj: Any, other_name: str, *,
-         forward: Callable = lambda x: x, backward: Callable = lambda x: x) -> None:
+         forward: Callable[[Any], Any] = lambda x: x, backward: Callable[[Any], Any] = lambda x: x) -> None:
     bind_from(self_obj, self_name, other_obj, other_name, backward=backward)
     bind_from(self_obj, self_name, other_obj, other_name, backward=backward)
     bind_to(self_obj, self_name, other_obj, other_name, forward=forward)
     bind_to(self_obj, self_name, other_obj, other_name, forward=forward)
 
 
 
 
 class BindableProperty:
 class BindableProperty:
 
 
-    def __init__(self, on_change: Optional[Callable] = None) -> None:
+    def __init__(self, on_change: Optional[Callable[..., Any]] = None) -> None:
         self.on_change = on_change
         self.on_change = on_change
 
 
     def __set_name__(self, _, name: str) -> None:
     def __set_name__(self, _, name: str) -> None:

+ 5 - 5
nicegui/client.py

@@ -47,8 +47,8 @@ class Client:
 
 
         self.page = page
         self.page = page
 
 
-        self.connect_handlers: List[Union[Callable, Awaitable]] = []
-        self.disconnect_handlers: List[Union[Callable, Awaitable]] = []
+        self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+        self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
 
 
     @property
     @property
     def ip(self) -> Optional[str]:
     def ip(self) -> Optional[str]:
@@ -131,7 +131,7 @@ class Client:
             await asyncio.sleep(check_interval)
             await asyncio.sleep(check_interval)
         return self.waiting_javascript_commands.pop(request_id)
         return self.waiting_javascript_commands.pop(request_id)
 
 
-    def open(self, target: Union[Callable, str]) -> None:
+    def open(self, target: Union[Callable[..., Any], str]) -> None:
         """Open a new page in the client."""
         """Open a new page in the client."""
         path = target if isinstance(target, str) else globals.page_routes[target]
         path = target if isinstance(target, str) else globals.page_routes[target]
         outbox.enqueue_message('open', path, self.id)
         outbox.enqueue_message('open', path, self.id)
@@ -140,10 +140,10 @@ class Client:
         """Download a file from the given URL."""
         """Download a file from the given URL."""
         outbox.enqueue_message('download', {'url': url, 'filename': filename}, self.id)
         outbox.enqueue_message('download', {'url': url, 'filename': filename}, self.id)
 
 
-    def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
+    def on_connect(self, handler: Union[Callable[..., Any], Awaitable]) -> None:
         """Register a callback to be called when the client connects."""
         """Register a callback to be called when the client connects."""
         self.connect_handlers.append(handler)
         self.connect_handlers.append(handler)
 
 
-    def on_disconnect(self, handler: Union[Callable, Awaitable]) -> None:
+    def on_disconnect(self, handler: Union[Callable[..., Any], Awaitable]) -> None:
         """Register a callback to be called when the client disconnects."""
         """Register a callback to be called when the client disconnects."""
         self.disconnect_handlers.append(handler)
         self.disconnect_handlers.append(handler)

+ 13 - 9
nicegui/element.py

@@ -3,7 +3,7 @@ from __future__ import annotations
 import re
 import re
 import warnings
 import warnings
 from copy import deepcopy
 from copy import deepcopy
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Union
 
 
 from typing_extensions import Self
 from typing_extensions import Self
 
 
@@ -18,7 +18,7 @@ from .tailwind import Tailwind
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from .client import Client
     from .client import Client
 
 
-PROPS_PATTERN = re.compile(r'([\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
+PROPS_PATTERN = re.compile(r'([:\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
 
 
 
 
 class Element(Visibility):
 class Element(Visibility):
@@ -77,9 +77,14 @@ class Element(Visibility):
     def __exit__(self, *_):
     def __exit__(self, *_):
         self.default_slot.__exit__(*_)
         self.default_slot.__exit__(*_)
 
 
+    def __iter__(self) -> Iterator[Element]:
+        for slot in self.slots.values():
+            for child in slot:
+                yield child
+
     def _collect_slot_dict(self) -> Dict[str, List[int]]:
     def _collect_slot_dict(self) -> Dict[str, List[int]]:
         return {
         return {
-            name: {'template': slot.template, 'ids': [child.id for child in slot.children]}
+            name: {'template': slot.template, 'ids': [child.id for child in slot]}
             for name, slot in self.slots.items()
             for name, slot in self.slots.items()
         }
         }
 
 
@@ -201,7 +206,7 @@ class Element(Visibility):
 
 
     def on(self,
     def on(self,
            type: str,
            type: str,
-           handler: Optional[Callable],
+           handler: Optional[Callable[..., Any]] = None,
            args: Optional[List[str]] = None, *,
            args: Optional[List[str]] = None, *,
            throttle: float = 0.0,
            throttle: float = 0.0,
            leading_events: bool = True,
            leading_events: bool = True,
@@ -255,9 +260,8 @@ class Element(Visibility):
 
 
     def _collect_descendant_ids(self) -> List[int]:
     def _collect_descendant_ids(self) -> List[int]:
         ids: List[int] = [self.id]
         ids: List[int] = [self.id]
-        for slot in self.slots.values():
-            for child in slot.children:
-                ids.extend(child._collect_descendant_ids())
+        for child in self:
+            ids.extend(child._collect_descendant_ids())
         return ids
         return ids
 
 
     def clear(self) -> None:
     def clear(self) -> None:
@@ -290,12 +294,12 @@ class Element(Visibility):
         :param element: either the element instance or its ID
         :param element: either the element instance or its ID
         """
         """
         if isinstance(element, int):
         if isinstance(element, int):
-            children = [child for slot in self.slots.values() for child in slot.children]
+            children = list(self)
             element = children[element]
             element = children[element]
         binding.remove([element], Element)
         binding.remove([element], Element)
         del self.client.elements[element.id]
         del self.client.elements[element.id]
         for slot in self.slots.values():
         for slot in self.slots.values():
-            slot.children[:] = [e for e in slot.children if e.id != element.id]
+            slot.children[:] = [e for e in slot if e.id != element.id]
         self.update()
         self.update()
 
 
     def delete(self) -> None:
     def delete(self) -> None:

+ 2 - 2
nicegui/elements/button.py

@@ -1,5 +1,5 @@
 import asyncio
 import asyncio
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from ..colors import set_background_color
 from ..colors import set_background_color
 from ..events import ClickEventArguments, handle_event
 from ..events import ClickEventArguments, handle_event
@@ -11,7 +11,7 @@ class Button(TextElement, DisableableElement):
 
 
     def __init__(self,
     def __init__(self,
                  text: str = '', *,
                  text: str = '', *,
-                 on_click: Optional[Callable] = None,
+                 on_click: Optional[Callable[..., Any]] = None,
                  color: Optional[str] = 'primary',
                  color: Optional[str] = 'primary',
                  ) -> None:
                  ) -> None:
         """Button
         """Button

+ 9 - 2
nicegui/elements/chart.js

@@ -4,6 +4,7 @@ export default {
     setTimeout(() => {
     setTimeout(() => {
       const imports = this.extras.map((extra) => import(window.path_prefix + extra));
       const imports = this.extras.map((extra) => import(window.path_prefix + extra));
       Promise.allSettled(imports).then(() => {
       Promise.allSettled(imports).then(() => {
+        this.seriesCount = this.options.series ? this.options.series.length : 0;
         this.chart = Highcharts[this.type](this.$el, this.options);
         this.chart = Highcharts[this.type](this.$el, this.options);
         this.chart.reflow();
         this.chart.reflow();
       });
       });
@@ -18,8 +19,14 @@ export default {
   methods: {
   methods: {
     update_chart() {
     update_chart() {
       if (this.chart) {
       if (this.chart) {
-        while (this.chart.series.length > this.options.series.length) this.chart.series[0].remove();
-        while (this.chart.series.length < this.options.series.length) this.chart.addSeries({}, false);
+        while (this.seriesCount > this.options.series.length) {
+          this.chart.series[0].remove();
+          this.seriesCount--;
+        }
+        while (this.seriesCount < this.options.series.length) {
+          this.chart.addSeries({}, false);
+          this.seriesCount++;
+        }
         this.chart.update(this.options);
         this.chart.update(this.options);
       }
       }
     },
     },

+ 2 - 2
nicegui/elements/checkbox.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 from .mixins.text_element import TextElement
@@ -7,7 +7,7 @@ from .mixins.value_element import ValueElement
 
 
 class Checkbox(TextElement, ValueElement, DisableableElement):
 class Checkbox(TextElement, ValueElement, DisableableElement):
 
 
-    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable] = None) -> None:
+    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable[..., Any]] = None) -> None:
         """Checkbox
         """Checkbox
 
 
         :param text: the label to display next to the checkbox
         :param text: the label to display next to the checkbox

+ 5 - 1
nicegui/elements/choice_element.py

@@ -6,7 +6,11 @@ from .mixins.value_element import ValueElement
 class ChoiceElement(ValueElement):
 class ChoiceElement(ValueElement):
 
 
     def __init__(self, *,
     def __init__(self, *,
-                 tag: str, options: Union[List, Dict], value: Any, on_change: Optional[Callable] = None) -> None:
+                 tag: str,
+                 options: Union[List, Dict],
+                 value: Any,
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         self.options = options
         self.options = options
         self._values: List[str] = []
         self._values: List[str] = []
         self._labels: List[str] = []
         self._labels: List[str] = []

+ 7 - 3
nicegui/elements/color_input.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from nicegui import ui
 from nicegui import ui
 
 
@@ -10,8 +10,12 @@ from .mixins.value_element import ValueElement
 class ColorInput(ValueElement, DisableableElement):
 class ColorInput(ValueElement, DisableableElement):
     LOOPBACK = False
     LOOPBACK = False
 
 
-    def __init__(self, label: Optional[str] = None, *,
-                 placeholder: Optional[str] = None, value: str = '', on_change: Optional[Callable] = None) -> None:
+    def __init__(self,
+                 label: Optional[str] = None, *,
+                 placeholder: Optional[str] = None,
+                 value: str = '',
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Color Input
         """Color Input
 
 
         :param label: displayed label for the color input
         :param label: displayed label for the color input

+ 2 - 2
nicegui/elements/color_picker.py

@@ -1,4 +1,4 @@
-from typing import Callable, Dict
+from typing import Any, Callable, Dict
 
 
 from nicegui.events import ColorPickEventArguments, handle_event
 from nicegui.events import ColorPickEventArguments, handle_event
 
 
@@ -8,7 +8,7 @@ from .menu import Menu
 
 
 class ColorPicker(Menu):
 class ColorPicker(Menu):
 
 
-    def __init__(self, *, on_pick: Callable, value: bool = False) -> None:
+    def __init__(self, *, on_pick: Callable[..., Any], value: bool = False) -> None:
         """Color Picker
         """Color Picker
 
 
         :param on_pick: callback to execute when a color is picked
         :param on_pick: callback to execute when a color is picked

+ 2 - 2
nicegui/elements/date.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
@@ -11,7 +11,7 @@ class Date(ValueElement, DisableableElement):
                  value: Optional[str] = None,
                  value: Optional[str] = None,
                  *,
                  *,
                  mask: str = 'YYYY-MM-DD',
                  mask: str = 'YYYY-MM-DD',
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None) -> None:
         """Date Input
         """Date Input
 
 
         This element is based on Quasar's `QDate <https://quasar.dev/vue-components/date>`_ component.
         This element is based on Quasar's `QDate <https://quasar.dev/vue-components/date>`_ component.

+ 2 - 2
nicegui/elements/input.py

@@ -14,9 +14,9 @@ class Input(ValueElement, DisableableElement):
                  value: str = '',
                  value: str = '',
                  password: bool = False,
                  password: bool = False,
                  password_toggle_button: bool = False,
                  password_toggle_button: bool = False,
-                 on_change: Optional[Callable] = None,
+                 on_change: Optional[Callable[..., Any]] = None,
                  autocomplete: Optional[List[str]] = None,
                  autocomplete: Optional[List[str]] = None,
-                 validation: Dict[str, Callable] = {}) -> None:
+                 validation: Dict[str, Callable[..., bool]] = {}) -> None:
         """Text Input
         """Text Input
 
 
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.

+ 7 - 3
nicegui/elements/interactive_image.py

@@ -1,7 +1,7 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 from pathlib import Path
 from pathlib import Path
-from typing import Callable, Dict, List, Optional
+from typing import Any, Callable, Dict, List, Optional
 
 
 from ..dependencies import register_vue_component
 from ..dependencies import register_vue_component
 from ..events import MouseEventArguments, handle_event
 from ..events import MouseEventArguments, handle_event
@@ -14,9 +14,13 @@ register_vue_component(name='interactive_image', path=Path(__file__).parent.join
 class InteractiveImage(SourceElement, ContentElement):
 class InteractiveImage(SourceElement, ContentElement):
     CONTENT_PROP = 'content'
     CONTENT_PROP = 'content'
 
 
-    def __init__(self, source: str = '', *,
+    def __init__(self,
+                 source: str = '', *,
                  content: str = '',
                  content: str = '',
-                 on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False) -> None:
+                 on_mouse: Optional[Callable[..., Any]] = None,
+                 events: List[str] = ['click'],
+                 cross: bool = False,
+                 ) -> None:
         """Interactive Image
         """Interactive Image
 
 
         Create an image with an SVG overlay that handles mouse events and yields image coordinates.
         Create an image with an SVG overlay that handles mouse events and yields image coordinates.

+ 3 - 3
nicegui/elements/joystick.py

@@ -12,9 +12,9 @@ register_library(name='nipplejs', path=Path(__file__).parent.joinpath('lib', 'ni
 class Joystick(Element):
 class Joystick(Element):
 
 
     def __init__(self, *,
     def __init__(self, *,
-                 on_start: Optional[Callable] = None,
-                 on_move: Optional[Callable] = None,
-                 on_end: Optional[Callable] = None,
+                 on_start: Optional[Callable[..., Any]] = None,
+                 on_move: Optional[Callable[..., Any]] = None,
+                 on_end: Optional[Callable[..., Any]] = None,
                  throttle: float = 0.05,
                  throttle: float = 0.05,
                  ** options: Any) -> None:
                  ** options: Any) -> None:
         """Joystick
         """Joystick

+ 2 - 2
nicegui/elements/keyboard.py

@@ -1,5 +1,5 @@
 from pathlib import Path
 from pathlib import Path
-from typing import Callable, Dict, List
+from typing import Any, Callable, Dict, List
 
 
 from typing_extensions import Literal
 from typing_extensions import Literal
 
 
@@ -15,7 +15,7 @@ class Keyboard(Element):
     active = BindableProperty()
     active = BindableProperty()
 
 
     def __init__(self,
     def __init__(self,
-                 on_key: Callable, *,
+                 on_key: Callable[..., Any], *,
                  active: bool = True,
                  active: bool = True,
                  repeating: bool = True,
                  repeating: bool = True,
                  ignore: List[Literal['input', 'select', 'button', 'textarea']] = ['input', 'select', 'button', 'textarea'],
                  ignore: List[Literal['input', 'select', 'button', 'textarea']] = ['input', 'select', 'button', 'textarea'],

+ 9 - 3
nicegui/elements/line_plot.py

@@ -1,11 +1,17 @@
-from typing import List
+from typing import Any, List
 
 
 from .pyplot import Pyplot
 from .pyplot import Pyplot
 
 
 
 
 class LinePlot(Pyplot):
 class LinePlot(Pyplot):
 
 
-    def __init__(self, *, n: int = 1, limit: int = 100, update_every: int = 1, close: bool = True, **kwargs) -> None:
+    def __init__(self, *,
+                 n: int = 1,
+                 limit: int = 100,
+                 update_every: int = 1,
+                 close: bool = True,
+                 **kwargs: Any,
+                 ) -> None:
         """Line Plot
         """Line Plot
 
 
         Create a line plot using pyplot.
         Create a line plot using pyplot.
@@ -26,7 +32,7 @@ class LinePlot(Pyplot):
         self.update_every = update_every
         self.update_every = update_every
         self.push_counter = 0
         self.push_counter = 0
 
 
-    def with_legend(self, titles: List[str], **kwargs):
+    def with_legend(self, titles: List[str], **kwargs: Any):
         self.fig.gca().legend(titles, **kwargs)
         self.fig.gca().legend(titles, **kwargs)
         self._convert_to_html()
         self._convert_to_html()
         return self
         return self

+ 2 - 2
nicegui/elements/link.py

@@ -1,5 +1,5 @@
 from pathlib import Path
 from pathlib import Path
-from typing import Callable, Union
+from typing import Any, Callable, Union
 
 
 from .. import globals
 from .. import globals
 from ..dependencies import register_vue_component
 from ..dependencies import register_vue_component
@@ -11,7 +11,7 @@ register_vue_component('link', Path(__file__).parent.joinpath('link.js'))
 
 
 class Link(TextElement):
 class Link(TextElement):
 
 
-    def __init__(self, text: str = '', target: Union[Callable, str] = '#', new_tab: bool = False) -> None:
+    def __init__(self, text: str = '', target: Union[Callable[..., Any], str] = '#', new_tab: bool = False) -> None:
         """Link
         """Link
 
 
         Create a hyperlink.
         Create a hyperlink.

+ 6 - 2
nicegui/elements/menu.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from .. import globals
 from .. import globals
 from ..events import ClickEventArguments, handle_event
 from ..events import ClickEventArguments, handle_event
@@ -28,7 +28,11 @@ class Menu(ValueElement):
 
 
 class MenuItem(TextElement):
 class MenuItem(TextElement):
 
 
-    def __init__(self, text: str = '', on_click: Optional[Callable] = None, *, auto_close: bool = True) -> None:
+    def __init__(self,
+                 text: str = '',
+                 on_click: Optional[Callable[..., Any]] = None, *,
+                 auto_close: bool = True,
+                 ) -> None:
         """Menu Item
         """Menu Item
 
 
         A menu item to be added to a menu.
         A menu item to be added to a menu.

+ 8 - 5
nicegui/elements/mixins/content_element.py

@@ -10,7 +10,7 @@ class ContentElement(Element):
     CONTENT_PROP = 'innerHTML'
     CONTENT_PROP = 'innerHTML'
     content = BindableProperty(on_change=lambda sender, content: sender.on_content_change(content))
     content = BindableProperty(on_change=lambda sender, content: sender.on_content_change(content))
 
 
-    def __init__(self, *, content: str, **kwargs) -> None:
+    def __init__(self, *, content: str, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         super().__init__(**kwargs)
         self.content = content
         self.content = content
         self.on_content_change(content)
         self.on_content_change(content)
@@ -18,7 +18,8 @@ class ContentElement(Element):
     def bind_content_to(self,
     def bind_content_to(self,
                         target_object: Any,
                         target_object: Any,
                         target_name: str = 'content',
                         target_name: str = 'content',
-                        forward: Callable = lambda x: x) -> Self:
+                        forward: Callable[..., Any] = lambda x: x,
+                        ) -> Self:
         """Bind the content of this element to the target object's target_name property.
         """Bind the content of this element to the target object's target_name property.
 
 
         The binding works one way only, from this element to the target.
         The binding works one way only, from this element to the target.
@@ -33,7 +34,8 @@ class ContentElement(Element):
     def bind_content_from(self,
     def bind_content_from(self,
                           target_object: Any,
                           target_object: Any,
                           target_name: str = 'content',
                           target_name: str = 'content',
-                          backward: Callable = lambda x: x) -> Self:
+                          backward: Callable[..., Any] = lambda x: x,
+                          ) -> Self:
         """Bind the content of this element from the target object's target_name property.
         """Bind the content of this element from the target object's target_name property.
 
 
         The binding works one way only, from the target to this element.
         The binding works one way only, from the target to this element.
@@ -48,8 +50,9 @@ class ContentElement(Element):
     def bind_content(self,
     def bind_content(self,
                      target_object: Any,
                      target_object: Any,
                      target_name: str = 'content', *,
                      target_name: str = 'content', *,
-                     forward: Callable = lambda x: x,
-                     backward: Callable = lambda x: x) -> Self:
+                     forward: Callable[..., Any] = lambda x: x,
+                     backward: Callable[..., Any] = lambda x: x,
+                     ) -> Self:
         """Bind the content of this element to the target object's target_name property.
         """Bind the content of this element to the target object's target_name property.
 
 
         The binding works both ways, from this element to the target and from the target to this element.
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 5
nicegui/elements/mixins/disableable_element.py

@@ -9,7 +9,7 @@ from ...element import Element
 class DisableableElement(Element):
 class DisableableElement(Element):
     enabled = BindableProperty(on_change=lambda sender, value: sender.on_enabled_change(value))
     enabled = BindableProperty(on_change=lambda sender, value: sender.on_enabled_change(value))
 
 
-    def __init__(self, **kwargs) -> None:
+    def __init__(self, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         super().__init__(**kwargs)
         self.enabled = True
         self.enabled = True
 
 
@@ -24,7 +24,8 @@ class DisableableElement(Element):
     def bind_enabled_to(self,
     def bind_enabled_to(self,
                         target_object: Any,
                         target_object: Any,
                         target_name: str = 'enabled',
                         target_name: str = 'enabled',
-                        forward: Callable = lambda x: x) -> Self:
+                        forward: Callable[..., Any] = lambda x: x,
+                        ) -> Self:
         """Bind the enabled state of this element to the target object's target_name property.
         """Bind the enabled state of this element to the target object's target_name property.
 
 
         The binding works one way only, from this element to the target.
         The binding works one way only, from this element to the target.
@@ -39,7 +40,8 @@ class DisableableElement(Element):
     def bind_enabled_from(self,
     def bind_enabled_from(self,
                           target_object: Any,
                           target_object: Any,
                           target_name: str = 'enabled',
                           target_name: str = 'enabled',
-                          backward: Callable = lambda x: x) -> Self:
+                          backward: Callable[..., Any] = lambda x: x,
+                          ) -> Self:
         """Bind the enabled state of this element from the target object's target_name property.
         """Bind the enabled state of this element from the target object's target_name property.
 
 
         The binding works one way only, from the target to this element.
         The binding works one way only, from the target to this element.
@@ -54,8 +56,9 @@ class DisableableElement(Element):
     def bind_enabled(self,
     def bind_enabled(self,
                      target_object: Any,
                      target_object: Any,
                      target_name: str = 'enabled', *,
                      target_name: str = 'enabled', *,
-                     forward: Callable = lambda x: x,
-                     backward: Callable = lambda x: x) -> Self:
+                     forward: Callable[..., Any] = lambda x: x,
+                     backward: Callable[..., Any] = lambda x: x,
+                     ) -> Self:
         """Bind the enabled state of this element to the target object's target_name property.
         """Bind the enabled state of this element to the target object's target_name property.
 
 
         The binding works both ways, from this element to the target and from the target to this element.
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 5
nicegui/elements/mixins/filter_element.py

@@ -10,7 +10,7 @@ class FilterElement(Element):
     FILTER_PROP = 'filter'
     FILTER_PROP = 'filter'
     filter = BindableProperty(on_change=lambda sender, filter: sender.on_filter_change(filter))
     filter = BindableProperty(on_change=lambda sender, filter: sender.on_filter_change(filter))
 
 
-    def __init__(self, *, filter: Optional[str] = None, **kwargs) -> None:
+    def __init__(self, *, filter: Optional[str] = None, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         super().__init__(**kwargs)
         self.filter = filter
         self.filter = filter
         self._props[self.FILTER_PROP] = filter
         self._props[self.FILTER_PROP] = filter
@@ -18,7 +18,8 @@ class FilterElement(Element):
     def bind_filter_to(self,
     def bind_filter_to(self,
                        target_object: Any,
                        target_object: Any,
                        target_name: str = 'filter',
                        target_name: str = 'filter',
-                       forward: Callable = lambda x: x) -> Self:
+                       forward: Callable[..., Any] = lambda x: x,
+                       ) -> Self:
         """Bind the filter of this element to the target object's target_name property.
         """Bind the filter of this element to the target object's target_name property.
 
 
         The binding works one way only, from this element to the target.
         The binding works one way only, from this element to the target.
@@ -33,7 +34,8 @@ class FilterElement(Element):
     def bind_filter_from(self,
     def bind_filter_from(self,
                          target_object: Any,
                          target_object: Any,
                          target_name: str = 'filter',
                          target_name: str = 'filter',
-                         backward: Callable = lambda x: x) -> Self:
+                         backward: Callable[..., Any] = lambda x: x,
+                         ) -> Self:
         """Bind the filter of this element from the target object's target_name property.
         """Bind the filter of this element from the target object's target_name property.
 
 
         The binding works one way only, from the target to this element.
         The binding works one way only, from the target to this element.
@@ -48,8 +50,9 @@ class FilterElement(Element):
     def bind_filter(self,
     def bind_filter(self,
                     target_object: Any,
                     target_object: Any,
                     target_name: str = 'filter', *,
                     target_name: str = 'filter', *,
-                    forward: Callable = lambda x: x,
-                    backward: Callable = lambda x: x) -> Self:
+                    forward: Callable[..., Any] = lambda x: x,
+                    backward: Callable[..., Any] = lambda x: x,
+                    ) -> Self:
         """Bind the filter of this element to the target object's target_name property.
         """Bind the filter of this element to the target object's target_name property.
 
 
         The binding works both ways, from this element to the target and from the target to this element.
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 5
nicegui/elements/mixins/source_element.py

@@ -9,7 +9,7 @@ from ...element import Element
 class SourceElement(Element):
 class SourceElement(Element):
     source = BindableProperty(on_change=lambda sender, source: sender.on_source_change(source))
     source = BindableProperty(on_change=lambda sender, source: sender.on_source_change(source))
 
 
-    def __init__(self, *, source: str, **kwargs) -> None:
+    def __init__(self, *, source: str, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         super().__init__(**kwargs)
         self.source = source
         self.source = source
         self._props['src'] = source
         self._props['src'] = source
@@ -17,7 +17,8 @@ class SourceElement(Element):
     def bind_source_to(self,
     def bind_source_to(self,
                        target_object: Any,
                        target_object: Any,
                        target_name: str = 'source',
                        target_name: str = 'source',
-                       forward: Callable = lambda x: x) -> Self:
+                       forward: Callable[..., Any] = lambda x: x,
+                       ) -> Self:
         """Bind the source of this element to the target object's target_name property.
         """Bind the source of this element to the target object's target_name property.
 
 
         The binding works one way only, from this element to the target.
         The binding works one way only, from this element to the target.
@@ -32,7 +33,8 @@ class SourceElement(Element):
     def bind_source_from(self,
     def bind_source_from(self,
                          target_object: Any,
                          target_object: Any,
                          target_name: str = 'source',
                          target_name: str = 'source',
-                         backward: Callable = lambda x: x) -> Self:
+                         backward: Callable[..., Any] = lambda x: x,
+                         ) -> Self:
         """Bind the source of this element from the target object's target_name property.
         """Bind the source of this element from the target object's target_name property.
 
 
         The binding works one way only, from the target to this element.
         The binding works one way only, from the target to this element.
@@ -47,8 +49,9 @@ class SourceElement(Element):
     def bind_source(self,
     def bind_source(self,
                     target_object: Any,
                     target_object: Any,
                     target_name: str = 'source', *,
                     target_name: str = 'source', *,
-                    forward: Callable = lambda x: x,
-                    backward: Callable = lambda x: x) -> Self:
+                    forward: Callable[..., Any] = lambda x: x,
+                    backward: Callable[..., Any] = lambda x: x,
+                    ) -> Self:
         """Bind the source of this element to the target object's target_name property.
         """Bind the source of this element to the target object's target_name property.
 
 
         The binding works both ways, from this element to the target and from the target to this element.
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 5
nicegui/elements/mixins/text_element.py

@@ -9,7 +9,7 @@ from ...element import Element
 class TextElement(Element):
 class TextElement(Element):
     text = BindableProperty(on_change=lambda sender, text: sender.on_text_change(text))
     text = BindableProperty(on_change=lambda sender, text: sender.on_text_change(text))
 
 
-    def __init__(self, *, text: str, **kwargs) -> None:
+    def __init__(self, *, text: str, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         super().__init__(**kwargs)
         self.text = text
         self.text = text
         self._text_to_model_text(text)
         self._text_to_model_text(text)
@@ -17,7 +17,8 @@ class TextElement(Element):
     def bind_text_to(self,
     def bind_text_to(self,
                      target_object: Any,
                      target_object: Any,
                      target_name: str = 'text',
                      target_name: str = 'text',
-                     forward: Callable = lambda x: x) -> Self:
+                     forward: Callable[..., Any] = lambda x: x,
+                     ) -> Self:
         """Bind the text of this element to the target object's target_name property.
         """Bind the text of this element to the target object's target_name property.
 
 
         The binding works one way only, from this element to the target.
         The binding works one way only, from this element to the target.
@@ -32,7 +33,8 @@ class TextElement(Element):
     def bind_text_from(self,
     def bind_text_from(self,
                        target_object: Any,
                        target_object: Any,
                        target_name: str = 'text',
                        target_name: str = 'text',
-                       backward: Callable = lambda x: x) -> Self:
+                       backward: Callable[..., Any] = lambda x: x,
+                       ) -> Self:
         """Bind the text of this element from the target object's target_name property.
         """Bind the text of this element from the target object's target_name property.
 
 
         The binding works one way only, from the target to this element.
         The binding works one way only, from the target to this element.
@@ -47,8 +49,9 @@ class TextElement(Element):
     def bind_text(self,
     def bind_text(self,
                   target_object: Any,
                   target_object: Any,
                   target_name: str = 'text', *,
                   target_name: str = 'text', *,
-                  forward: Callable = lambda x: x,
-                  backward: Callable = lambda x: x) -> Self:
+                  forward: Callable[..., Any] = lambda x: x,
+                  backward: Callable[..., Any] = lambda x: x,
+                  ) -> Self:
         """Bind the text of this element to the target object's target_name property.
         """Bind the text of this element to the target object's target_name property.
 
 
         The binding works both ways, from this element to the target and from the target to this element.
         The binding works both ways, from this element to the target and from the target to this element.

+ 13 - 5
nicegui/elements/mixins/value_element.py

@@ -13,7 +13,12 @@ class ValueElement(Element):
     LOOPBACK = True
     LOOPBACK = True
     value = BindableProperty(on_change=lambda sender, value: sender.on_value_change(value))
     value = BindableProperty(on_change=lambda sender, value: sender.on_value_change(value))
 
 
-    def __init__(self, *, value: Any, on_value_change: Optional[Callable], throttle: float = 0, **kwargs) -> None:
+    def __init__(self, *,
+                 value: Any,
+                 on_value_change: Optional[Callable[..., Any]],
+                 throttle: float = 0,
+                 **kwargs: Any,
+                 ) -> None:
         super().__init__(**kwargs)
         super().__init__(**kwargs)
         self.set_value(value)
         self.set_value(value)
         self._props[self.VALUE_PROP] = self._value_to_model_value(value)
         self._props[self.VALUE_PROP] = self._value_to_model_value(value)
@@ -30,7 +35,8 @@ class ValueElement(Element):
     def bind_value_to(self,
     def bind_value_to(self,
                       target_object: Any,
                       target_object: Any,
                       target_name: str = 'value',
                       target_name: str = 'value',
-                      forward: Callable = lambda x: x) -> Self:
+                      forward: Callable[..., Any] = lambda x: x,
+                      ) -> Self:
         """Bind the value of this element to the target object's target_name property.
         """Bind the value of this element to the target object's target_name property.
 
 
         The binding works one way only, from this element to the target.
         The binding works one way only, from this element to the target.
@@ -45,7 +51,8 @@ class ValueElement(Element):
     def bind_value_from(self,
     def bind_value_from(self,
                         target_object: Any,
                         target_object: Any,
                         target_name: str = 'value',
                         target_name: str = 'value',
-                        backward: Callable = lambda x: x) -> Self:
+                        backward: Callable[..., Any] = lambda x: x,
+                        ) -> Self:
         """Bind the value of this element from the target object's target_name property.
         """Bind the value of this element from the target object's target_name property.
 
 
         The binding works one way only, from the target to this element.
         The binding works one way only, from the target to this element.
@@ -60,8 +67,9 @@ class ValueElement(Element):
     def bind_value(self,
     def bind_value(self,
                    target_object: Any,
                    target_object: Any,
                    target_name: str = 'value', *,
                    target_name: str = 'value', *,
-                   forward: Callable = lambda x: x,
-                   backward: Callable = lambda x: x) -> Self:
+                   forward: Callable[..., Any] = lambda x: x,
+                   backward: Callable[..., Any] = lambda x: x,
+                   ) -> Self:
         """Bind the value of this element to the target object's target_name property.
         """Bind the value of this element to the target object's target_name property.
 
 
         The binding works both ways, from this element to the target and from the target to this element.
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 6
nicegui/elements/mixins/visibility.py

@@ -11,14 +11,15 @@ if TYPE_CHECKING:
 class Visibility:
 class Visibility:
     visible = BindableProperty(on_change=lambda sender, visible: sender.on_visibility_change(visible))
     visible = BindableProperty(on_change=lambda sender, visible: sender.on_visibility_change(visible))
 
 
-    def __init__(self, **kwargs) -> None:
+    def __init__(self, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         super().__init__(**kwargs)
         self.visible = True
         self.visible = True
 
 
     def bind_visibility_to(self,
     def bind_visibility_to(self,
                            target_object: Any,
                            target_object: Any,
                            target_name: str = 'visible',
                            target_name: str = 'visible',
-                           forward: Callable = lambda x: x) -> Self:
+                           forward: Callable[..., Any] = lambda x: x,
+                           ) -> Self:
         """Bind the visibility of this element to the target object's target_name property.
         """Bind the visibility of this element to the target object's target_name property.
 
 
         The binding works one way only, from this element to the target.
         The binding works one way only, from this element to the target.
@@ -33,7 +34,7 @@ class Visibility:
     def bind_visibility_from(self,
     def bind_visibility_from(self,
                              target_object: Any,
                              target_object: Any,
                              target_name: str = 'visible',
                              target_name: str = 'visible',
-                             backward: Callable = lambda x: x, *,
+                             backward: Callable[..., Any] = lambda x: x, *,
                              value: Any = None) -> Self:
                              value: Any = None) -> Self:
         """Bind the visibility of this element from the target object's target_name property.
         """Bind the visibility of this element from the target object's target_name property.
 
 
@@ -52,9 +53,10 @@ class Visibility:
     def bind_visibility(self,
     def bind_visibility(self,
                         target_object: Any,
                         target_object: Any,
                         target_name: str = 'visible', *,
                         target_name: str = 'visible', *,
-                        forward: Callable = lambda x: x,
-                        backward: Callable = lambda x: x,
-                        value: Any = None) -> Self:
+                        forward: Callable[..., Any] = lambda x: x,
+                        backward: Callable[..., Any] = lambda x: x,
+                        value: Any = None,
+                        ) -> Self:
         """Bind the visibility of this element to the target object's target_name property.
         """Bind the visibility of this element to the target object's target_name property.
 
 
         The binding works both ways, from this element to the target and from the target to this element.
         The binding works both ways, from this element to the target and from the target to this element.

+ 3 - 2
nicegui/elements/number.py

@@ -17,8 +17,9 @@ class Number(ValueElement, DisableableElement):
                  prefix: Optional[str] = None,
                  prefix: Optional[str] = None,
                  suffix: Optional[str] = None,
                  suffix: Optional[str] = None,
                  format: Optional[str] = None,
                  format: Optional[str] = None,
-                 on_change: Optional[Callable] = None,
-                 validation: Dict[str, Callable] = {}) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 validation: Dict[str, Callable[..., bool]] = {},
+                 ) -> None:
         """Number Input
         """Number Input
 
 
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.

+ 2 - 1
nicegui/elements/pyplot.py

@@ -1,5 +1,6 @@
 import asyncio
 import asyncio
 import io
 import io
+from typing import Any
 
 
 import matplotlib.pyplot as plt
 import matplotlib.pyplot as plt
 
 
@@ -9,7 +10,7 @@ from ..element import Element
 
 
 class Pyplot(Element):
 class Pyplot(Element):
 
 
-    def __init__(self, *, close: bool = True, **kwargs) -> None:
+    def __init__(self, *, close: bool = True, **kwargs: Any) -> None:
         """Pyplot Context
         """Pyplot Context
 
 
         Create a context to configure a `Matplotlib <https://matplotlib.org/>`_ plot.
         Create a context to configure a `Matplotlib <https://matplotlib.org/>`_ plot.

+ 5 - 1
nicegui/elements/radio.py

@@ -6,7 +6,11 @@ from .mixins.disableable_element import DisableableElement
 
 
 class Radio(ChoiceElement, DisableableElement):
 class Radio(ChoiceElement, DisableableElement):
 
 
-    def __init__(self, options: Union[List, Dict], *, value: Any = None, on_change: Optional[Callable] = None):
+    def __init__(self,
+                 options: Union[List, Dict], *,
+                 value: Any = None,
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Radio Selection
         """Radio Selection
 
 
         The options can be specified as a list of values, or as a dictionary mapping values to labels.
         The options can be specified as a list of values, or as a dictionary mapping values to labels.

+ 2 - 2
nicegui/elements/scene.py

@@ -8,7 +8,6 @@ from ..element import Element
 from ..events import SceneClickEventArguments, SceneClickHit, handle_event
 from ..events import SceneClickEventArguments, SceneClickHit, handle_event
 from ..helpers import KWONLY_SLOTS
 from ..helpers import KWONLY_SLOTS
 from .scene_object3d import Object3D
 from .scene_object3d import Object3D
-from .scene_objects import Scene as SceneObject
 
 
 register_vue_component(name='scene', path=Path(__file__).parent.joinpath('scene.js'))
 register_vue_component(name='scene', path=Path(__file__).parent.joinpath('scene.js'))
 register_library(name='three',
 register_library(name='three',
@@ -64,7 +63,8 @@ class Scene(Element):
                  width: int = 400,
                  width: int = 400,
                  height: int = 300,
                  height: int = 300,
                  grid: bool = True,
                  grid: bool = True,
-                 on_click: Optional[Callable] = None) -> None:
+                 on_click: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """3D Scene
         """3D Scene
 
 
         Display a 3d scene using `three.js <https://threejs.org/>`_.
         Display a 3d scene using `three.js <https://threejs.org/>`_.

+ 3 - 2
nicegui/elements/select.py

@@ -16,8 +16,9 @@ class Select(ChoiceElement, DisableableElement):
     def __init__(self, options: Union[List, Dict], *,
     def __init__(self, options: Union[List, Dict], *,
                  label: Optional[str] = None,
                  label: Optional[str] = None,
                  value: Any = None,
                  value: Any = None,
-                 on_change: Optional[Callable] = None,
-                 with_input: bool = False) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 with_input: bool = False,
+                 ) -> None:
         """Dropdown Selection
         """Dropdown Selection
 
 
         The options can be specified as a list of values, or as a dictionary mapping values to labels.
         The options can be specified as a list of values, or as a dictionary mapping values to labels.

+ 3 - 2
nicegui/elements/slider.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
@@ -11,7 +11,8 @@ class Slider(ValueElement, DisableableElement):
                  max: float,
                  max: float,
                  step: float = 1.0,
                  step: float = 1.0,
                  value: Optional[float] = None,
                  value: Optional[float] = None,
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Slider
         """Slider
 
 
         :param min: lower bound of the slider
         :param min: lower bound of the slider

+ 3 - 2
nicegui/elements/splitter.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional, Tuple
+from typing import Any, Callable, Optional, Tuple
 
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
@@ -11,7 +11,8 @@ class Splitter(ValueElement, DisableableElement):
                  reverse: Optional[bool] = False,
                  reverse: Optional[bool] = False,
                  limits: Optional[Tuple[float, float]] = (0, 100),
                  limits: Optional[Tuple[float, float]] = (0, 100),
                  value: Optional[float] = 50,
                  value: Optional[float] = 50,
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Splitter
         """Splitter
 
 
         The `ui.splitter` element divides the screen space into resizable sections, 
         The `ui.splitter` element divides the screen space into resizable sections, 

+ 2 - 2
nicegui/elements/switch.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 from .mixins.text_element import TextElement
@@ -7,7 +7,7 @@ from .mixins.value_element import ValueElement
 
 
 class Switch(TextElement, ValueElement, DisableableElement):
 class Switch(TextElement, ValueElement, DisableableElement):
 
 
-    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable] = None) -> None:
+    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable[..., Any]] = None) -> None:
         """Switch
         """Switch
 
 
         :param text: the label to display next to the switch
         :param text: the label to display next to the switch

+ 2 - 2
nicegui/elements/table.py

@@ -1,4 +1,4 @@
-from typing import Callable, Dict, List, Optional
+from typing import Any, Callable, Dict, List, Optional
 
 
 from typing_extensions import Literal
 from typing_extensions import Literal
 
 
@@ -16,7 +16,7 @@ class Table(FilterElement):
                  title: Optional[str] = None,
                  title: Optional[str] = None,
                  selection: Optional[Literal['single', 'multiple']] = None,
                  selection: Optional[Literal['single', 'multiple']] = None,
                  pagination: Optional[int] = None,
                  pagination: Optional[int] = None,
-                 on_select: Optional[Callable] = None,
+                 on_select: Optional[Callable[..., Any]] = None,
                  ) -> None:
                  ) -> None:
         """Table
         """Table
 
 

+ 3 - 2
nicegui/elements/tabs.py

@@ -9,7 +9,8 @@ class Tabs(ValueElement):
 
 
     def __init__(self, *,
     def __init__(self, *,
                  value: Any = None,
                  value: Any = None,
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Tabs
         """Tabs
 
 
         This element represents `Quasar's QTabs <https://quasar.dev/vue-components/tabs#qtabs-api>`_ component.
         This element represents `Quasar's QTabs <https://quasar.dev/vue-components/tabs#qtabs-api>`_ component.
@@ -47,7 +48,7 @@ class TabPanels(ValueElement):
     def __init__(self,
     def __init__(self,
                  tabs: Tabs, *,
                  tabs: Tabs, *,
                  value: Any = None,
                  value: Any = None,
-                 on_change: Optional[Callable] = None,
+                 on_change: Optional[Callable[..., Any]] = None,
                  animated: bool = True,
                  animated: bool = True,
                  ) -> None:
                  ) -> None:
         """Tab Panels
         """Tab Panels

+ 4 - 3
nicegui/elements/textarea.py

@@ -1,4 +1,4 @@
-from typing import Callable, Dict, Optional
+from typing import Any, Callable, Dict, Optional
 
 
 from .input import Input
 from .input import Input
 
 
@@ -9,8 +9,9 @@ class Textarea(Input):
                  label: Optional[str] = None, *,
                  label: Optional[str] = None, *,
                  placeholder: Optional[str] = None,
                  placeholder: Optional[str] = None,
                  value: str = '',
                  value: str = '',
-                 on_change: Optional[Callable] = None,
-                 validation: Dict[str, Callable] = {}) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 validation: Dict[str, Callable[..., bool]] = {},
+                 ) -> None:
         """Textarea
         """Textarea
 
 
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.

+ 4 - 4
nicegui/elements/time.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
@@ -7,10 +7,10 @@ from .mixins.value_element import ValueElement
 class Time(ValueElement, DisableableElement):
 class Time(ValueElement, DisableableElement):
 
 
     def __init__(self,
     def __init__(self,
-                 value: Optional[str] = None,
-                 *,
+                 value: Optional[str] = None, *,
                  mask: str = 'HH:mm',
                  mask: str = 'HH:mm',
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Time Input
         """Time Input
 
 
         This element is based on Quasar's `QTime <https://quasar.dev/vue-components/date>`_ component.
         This element is based on Quasar's `QTime <https://quasar.dev/vue-components/date>`_ component.

+ 5 - 1
nicegui/elements/toggle.py

@@ -6,7 +6,11 @@ from .mixins.disableable_element import DisableableElement
 
 
 class Toggle(ChoiceElement, DisableableElement):
 class Toggle(ChoiceElement, DisableableElement):
 
 
-    def __init__(self, options: Union[List, Dict], *, value: Any = None, on_change: Optional[Callable] = None) -> None:
+    def __init__(self,
+                 options: Union[List, Dict], *,
+                 value: Any = None,
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Toggle
         """Toggle
 
 
         The options can be specified as a list of values, or as a dictionary mapping values to labels.
         The options can be specified as a list of values, or as a dictionary mapping values to labels.

+ 4 - 3
nicegui/elements/tree.py

@@ -11,9 +11,10 @@ class Tree(Element):
                  node_key: str = 'id',
                  node_key: str = 'id',
                  label_key: str = 'label',
                  label_key: str = 'label',
                  children_key: str = 'children',
                  children_key: str = 'children',
-                 on_select: Optional[Callable] = None,
-                 on_expand: Optional[Callable] = None,
-                 on_tick: Optional[Callable] = None) -> None:
+                 on_select: Optional[Callable[..., Any]] = None,
+                 on_expand: Optional[Callable[..., Any]] = None,
+                 on_tick: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Tree
         """Tree
 
 
         Display hierarchical data using Quasar's `QTree <https://quasar.dev/vue-components/tree>`_ component.
         Display hierarchical data using Quasar's `QTree <https://quasar.dev/vue-components/tree>`_ component.

+ 3 - 3
nicegui/elements/upload.py

@@ -1,5 +1,5 @@
 from pathlib import Path
 from pathlib import Path
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from fastapi import Request, Response
 from fastapi import Request, Response
 
 
@@ -18,8 +18,8 @@ class Upload(DisableableElement):
                  max_file_size: Optional[int] = None,
                  max_file_size: Optional[int] = None,
                  max_total_size: Optional[int] = None,
                  max_total_size: Optional[int] = None,
                  max_files: Optional[int] = None,
                  max_files: Optional[int] = None,
-                 on_upload: Optional[Callable] = None,
-                 on_rejected: Optional[Callable] = None,
+                 on_upload: Optional[Callable[..., Any]] = None,
+                 on_rejected: Optional[Callable[..., Any]] = None,
                  label: str = '',
                  label: str = '',
                  auto_upload: bool = False,
                  auto_upload: bool = False,
                  ) -> None:
                  ) -> None:

+ 2 - 2
nicegui/events.py

@@ -268,7 +268,7 @@ class KeyEventArguments(EventArguments):
     modifiers: KeyboardModifiers
     modifiers: KeyboardModifiers
 
 
 
 
-def handle_event(handler: Optional[Callable],
+def handle_event(handler: Optional[Callable[..., Any]],
                  arguments: Union[EventArguments, Dict], *,
                  arguments: Union[EventArguments, Dict], *,
                  sender: Optional['Element'] = None) -> None:
                  sender: Optional['Element'] = None) -> None:
     try:
     try:
@@ -276,7 +276,7 @@ def handle_event(handler: Optional[Callable],
             return
             return
         no_arguments = not signature(handler).parameters
         no_arguments = not signature(handler).parameters
         sender = arguments.sender if isinstance(arguments, EventArguments) else sender
         sender = arguments.sender if isinstance(arguments, EventArguments) else sender
-        assert sender.parent_slot is not None
+        assert sender is not None and sender.parent_slot is not None
         with sender.parent_slot:
         with sender.parent_slot:
             result = handler() if no_arguments else handler(arguments)
             result = handler() if no_arguments else handler(arguments)
         if is_coroutine(handler):
         if is_coroutine(handler):

+ 1 - 1
nicegui/functions/notify.py

@@ -10,7 +10,7 @@ def notify(message: Any, *,
            closeBtn: Union[bool, str] = False,
            closeBtn: Union[bool, str] = False,
            type: Optional[Literal['positive', 'negative', 'warning', 'info', 'ongoing']] = None,
            type: Optional[Literal['positive', 'negative', 'warning', 'info', 'ongoing']] = None,
            color: Optional[str] = None,
            color: Optional[str] = None,
-           **kwargs,
+           **kwargs: Any,
            ) -> None:
            ) -> None:
     """Notification
     """Notification
 
 

+ 2 - 2
nicegui/functions/open.py

@@ -1,9 +1,9 @@
-from typing import Callable, Union
+from typing import Any, Callable, Union
 
 
 from .. import globals
 from .. import globals
 
 
 
 
-def open(target: Union[Callable, str]) -> None:
+def open(target: Union[Callable[..., Any], str]) -> None:
     """Open
     """Open
 
 
     Can be used to programmatically trigger redirects for a specific client.
     Can be used to programmatically trigger redirects for a specific client.

+ 40 - 32
nicegui/functions/refreshable.py

@@ -1,3 +1,4 @@
+from dataclasses import dataclass
 from pathlib import Path
 from pathlib import Path
 from typing import Any, Callable, Dict, List, Tuple
 from typing import Any, Callable, Dict, List, Tuple
 
 
@@ -6,14 +7,38 @@ from typing_extensions import Self
 from .. import background_tasks, globals
 from .. import background_tasks, globals
 from ..dependencies import register_vue_component
 from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
-from ..helpers import is_coroutine
+from ..helpers import KWONLY_SLOTS, is_coroutine
 
 
 register_vue_component(name='refreshable', path=Path(__file__).parent.joinpath('refreshable.js'))
 register_vue_component(name='refreshable', path=Path(__file__).parent.joinpath('refreshable.js'))
 
 
 
 
+@dataclass(**KWONLY_SLOTS)
+class RefreshableTarget:
+    container: Element
+    instance: Any
+    args: List[Any]
+    kwargs: Dict[str, Any]
+
+    def run(self, func: Callable[..., Any]) -> None:
+        if is_coroutine(func):
+            async def wait_for_result() -> None:
+                with self.container:
+                    if self.instance is None:
+                        await func(*self.args, **self.kwargs)
+                    else:
+                        await func(self.instance, *self.args, **self.kwargs)
+            return wait_for_result()
+        else:
+            with self.container:
+                if self.instance is None:
+                    func(*self.args, **self.kwargs)
+                else:
+                    func(self.instance, *self.args, **self.kwargs)
+
+
 class refreshable:
 class refreshable:
 
 
-    def __init__(self, func: Callable) -> None:
+    def __init__(self, func: Callable[..., Any]) -> None:
         """Refreshable UI functions
         """Refreshable UI functions
 
 
         The `@ui.refreshable` decorator allows you to create functions that have a `refresh` method.
         The `@ui.refreshable` decorator allows you to create functions that have a `refresh` method.
@@ -21,24 +46,27 @@ class refreshable:
         """
         """
         self.func = func
         self.func = func
         self.instance = None
         self.instance = None
-        self.containers: List[Tuple[Element, List[Any], Dict[str, Any]]] = []
+        self.targets: List[RefreshableTarget] = []
 
 
     def __get__(self, instance, _) -> Self:
     def __get__(self, instance, _) -> Self:
         self.instance = instance
         self.instance = instance
         return self
         return self
 
 
-    def __call__(self, *args, **kwargs) -> None:
+    def __call__(self, *args: Any, **kwargs: Any) -> None:
         self.prune()
         self.prune()
-        with Element('refreshable') as container:
-            container.use_component('refreshable')
-            self.containers.append((container, args, kwargs))
-            return self.func(*args, **kwargs) if self.instance is None else self.func(self.instance, *args, **kwargs)
+        container = Element('refreshable')
+        container.use_component('refreshable')
+        target = RefreshableTarget(container=container, instance=self.instance, args=args, kwargs=kwargs)
+        self.targets.append(target)
+        return target.run(self.func)
 
 
     def refresh(self) -> None:
     def refresh(self) -> None:
         self.prune()
         self.prune()
-        for container, args, kwargs in self.containers:
-            container.clear()
-            result = self._run_in_container(container, *args, **kwargs)
+        for target in self.targets:
+            if target.instance != self.instance:
+                continue
+            target.container.clear()
+            result = target.run(self.func)
             if is_coroutine(self.func):
             if is_coroutine(self.func):
                 if globals.loop and globals.loop.is_running():
                 if globals.loop and globals.loop.is_running():
                     background_tasks.create(result)
                     background_tasks.create(result)
@@ -46,24 +74,4 @@ class refreshable:
                     globals.app.on_startup(result)
                     globals.app.on_startup(result)
 
 
     def prune(self) -> None:
     def prune(self) -> None:
-        self.containers = [
-            (container, args, kwargs)
-            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)
+        self.targets = [target for target in self.targets if target.container.client.id in globals.clients]

+ 10 - 4
nicegui/functions/timer.py

@@ -1,6 +1,6 @@
 import asyncio
 import asyncio
 import time
 import time
-from typing import Callable
+from typing import Any, Callable
 
 
 from .. import background_tasks, globals
 from .. import background_tasks, globals
 from ..binding import BindableProperty
 from ..binding import BindableProperty
@@ -11,7 +11,12 @@ class Timer:
     active = BindableProperty()
     active = BindableProperty()
     interval = BindableProperty()
     interval = BindableProperty()
 
 
-    def __init__(self, interval: float, callback: Callable, *, active: bool = True, once: bool = False) -> None:
+    def __init__(self,
+                 interval: float,
+                 callback: Callable[..., Any], *,
+                 active: bool = True,
+                 once: bool = False,
+                 ) -> None:
         """Timer
         """Timer
 
 
         One major drive behind the creation of NiceGUI was the necessity to have a simple approach to update the interface in regular intervals,
         One major drive behind the creation of NiceGUI was the necessity to have a simple approach to update the interface in regular intervals,
@@ -78,10 +83,11 @@ class Timer:
             globals.handle_exception(e)
             globals.handle_exception(e)
 
 
     async def _connected(self, timeout: float = 60.0) -> bool:
     async def _connected(self, timeout: float = 60.0) -> bool:
-        '''Wait for the client connection before the timer callback can be allowed to manipulate the state.
+        """Wait for the client connection before the timer callback can be allowed to manipulate the state.
+
         See https://github.com/zauberzeug/nicegui/issues/206 for details.
         See https://github.com/zauberzeug/nicegui/issues/206 for details.
         Returns True if the client is connected, False if the client is not connected and the timer should be cancelled.
         Returns True if the client is connected, False if the client is not connected and the timer should be cancelled.
-        '''
+        """
         if self.slot.parent.client.shared:
         if self.slot.parent.client.shared:
             return True
             return True
         else:
         else:

+ 7 - 7
nicegui/globals.py

@@ -3,7 +3,7 @@ import inspect
 import logging
 import logging
 from contextlib import contextmanager
 from contextlib import contextmanager
 from enum import Enum
 from enum import Enum
-from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
 
 
 from socketio import AsyncServer
 from socketio import AsyncServer
 from uvicorn import Server
 from uvicorn import Server
@@ -48,13 +48,13 @@ slot_stacks: Dict[int, List['Slot']] = {}
 clients: Dict[str, 'Client'] = {}
 clients: Dict[str, 'Client'] = {}
 index_client: 'Client'
 index_client: 'Client'
 
 
-page_routes: Dict[Callable, str] = {}
+page_routes: Dict[Callable[..., Any], str] = {}
 
 
-startup_handlers: List[Union[Callable, Awaitable]] = []
-shutdown_handlers: List[Union[Callable, Awaitable]] = []
-connect_handlers: List[Union[Callable, Awaitable]] = []
-disconnect_handlers: List[Union[Callable, Awaitable]] = []
-exception_handlers: List[Callable] = [log.exception]
+startup_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+shutdown_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+exception_handlers: List[Callable[..., Any]] = [log.exception]
 
 
 
 
 def get_task_id() -> int:
 def get_task_id() -> int:

+ 1 - 1
nicegui/helpers.py

@@ -23,7 +23,7 @@ def is_coroutine(object: Any) -> bool:
     return asyncio.iscoroutinefunction(object)
     return asyncio.iscoroutinefunction(object)
 
 
 
 
-def safe_invoke(func: Union[Callable, Awaitable], client: Optional['Client'] = None) -> None:
+def safe_invoke(func: Union[Callable[..., Any], Awaitable], client: Optional['Client'] = None) -> None:
     try:
     try:
         if isinstance(func, Awaitable):
         if isinstance(func, Awaitable):
             async def func_with_client():
             async def func_with_client():

+ 3 - 3
nicegui/page.py

@@ -1,7 +1,7 @@
 import asyncio
 import asyncio
 import inspect
 import inspect
 import time
 import time
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 
 from fastapi import Request, Response
 from fastapi import Request, Response
 
 
@@ -21,7 +21,7 @@ class page:
                  dark: Optional[bool] = ...,
                  dark: Optional[bool] = ...,
                  language: Language = ...,
                  language: Language = ...,
                  response_timeout: float = 3.0,
                  response_timeout: float = 3.0,
-                 **kwargs,
+                 **kwargs: Any,
                  ) -> None:
                  ) -> None:
         """Page
         """Page
 
 
@@ -62,7 +62,7 @@ class page:
     def resolve_language(self) -> Optional[str]:
     def resolve_language(self) -> Optional[str]:
         return self.language if self.language is not ... else globals.language
         return self.language if self.language is not ... else globals.language
 
 
-    def __call__(self, func: Callable) -> Callable:
+    def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
         globals.app.remove_route(self.path)  # NOTE make sure only the latest route definition is used
         globals.app.remove_route(self.path)  # NOTE make sure only the latest route definition is used
         parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
         parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
 
 

+ 2 - 2
nicegui/run.py

@@ -2,7 +2,7 @@ import logging
 import multiprocessing
 import multiprocessing
 import os
 import os
 import sys
 import sys
-from typing import List, Optional, Tuple
+from typing import Any, List, Optional, Tuple
 
 
 import __main__
 import __main__
 import uvicorn
 import uvicorn
@@ -33,7 +33,7 @@ def run(*,
         uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
         uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
         exclude: str = '',
         exclude: str = '',
         tailwind: bool = True,
         tailwind: bool = True,
-        **kwargs,
+        **kwargs: Any,
         ) -> None:
         ) -> None:
     '''ui.run
     '''ui.run
 
 

+ 4 - 1
nicegui/slot.py

@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, List, Optional
+from typing import TYPE_CHECKING, Iterator, List, Optional
 
 
 from typing_extensions import Self
 from typing_extensions import Self
 
 
@@ -23,3 +23,6 @@ class Slot:
     def __exit__(self, *_) -> None:
     def __exit__(self, *_) -> None:
         globals.get_slot_stack().pop()
         globals.get_slot_stack().pop()
         globals.prune_slot_stack()
         globals.prune_slot_stack()
+
+    def __iter__(self) -> Iterator['Element']:
+        return iter(self.children)

+ 8 - 1
nicegui/templates/index.html

@@ -90,6 +90,12 @@
           style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, '') || undefined,
           style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, '') || undefined,
           ...element.props,
           ...element.props,
         };
         };
+        Object.entries(props).forEach(([key, value]) => {
+          if (key.startsWith(':')) {
+            props[key.substring(1)] = eval(value);
+            delete props[key];
+          }
+        });
         element.events.forEach((event) => {
         element.events.forEach((event) => {
           let event_name = 'on' + event.type[0].toLocaleUpperCase() + event.type.substring(1);
           let event_name = 'on' + event.type[0].toLocaleUpperCase() + event.type.substring(1);
           event.specials.forEach(s => event_name += s[0].toLocaleUpperCase() + s.substring(1));
           event.specials.forEach(s => event_name += s[0].toLocaleUpperCase() + s.substring(1));
@@ -174,8 +180,9 @@
           const query = { client_id: "{{ client_id }}" };
           const query = { client_id: "{{ client_id }}" };
           const url = window.location.protocol === 'https:' ? 'wss://' : 'ws://' + window.location.host;
           const url = window.location.protocol === 'https:' ? 'wss://' : 'ws://' + window.location.host;
           const extraHeaders = {{ socket_io_js_extra_headers | safe }};
           const extraHeaders = {{ socket_io_js_extra_headers | safe }};
+          const transports = ['websocket', 'polling'];
           window.path_prefix = "{{ prefix | safe }}";
           window.path_prefix = "{{ prefix | safe }}";
-          window.socket = io(url, { path: "{{ prefix | safe }}/_nicegui_ws/socket.io", query, extraHeaders });
+          window.socket = io(url, { path: "{{ prefix | safe }}/_nicegui_ws/socket.io", query, extraHeaders, transports });
           window.socket.on("connect", () => {
           window.socket.on("connect", () => {
             window.socket.emit("handshake", (ok) => {
             window.socket.emit("handshake", (ok) => {
               if (!ok) window.location.reload();
               if (!ok) window.location.reload();

+ 86 - 0
nicegui/ui.py

@@ -1,5 +1,90 @@
 import os
 import os
 
 
+__all__ = [
+    'deprecated',
+    'element',
+    'aggrid',
+    'audio',
+    'avatar',
+    'badge',
+    'button',
+    'card',
+    'card_actions',
+    'card_section',
+    'chart',
+    'chat_message',
+    'checkbox',
+    'color_input',
+    'color_picker',
+    'colors',
+    'column',
+    'dark_mode',
+    'date',
+    'dialog',
+    'expansion',
+    'grid',
+    'html',
+    'icon',
+    'image',
+    'input',
+    'interactive_image',
+    'joystick',
+    'keyboard',
+    'knob',
+    'label',
+    'link',
+    'link_target',
+    'log',
+    'markdown',
+    'menu',
+    'menu_item',
+    'mermaid',
+    'number',
+    'plotly',
+    'circular_progress',
+    'linear_progress',
+    'query',
+    'radio',
+    'row',
+    'scene',
+    'select',
+    'separator',
+    'slider',
+    'spinner',
+    'splitter',
+    'switch',
+    'table',
+    'tab',
+    'tab_panel',
+    'tab_panels',
+    'tabs',
+    'textarea',
+    'time',
+    'toggle',
+    'tooltip',
+    'tree',
+    'upload',
+    'video',
+    'download',
+    'add_body_html',
+    'add_head_html',
+    'run_javascript',
+    'notify',
+    'open',
+    'refreshable',
+    'timer',
+    'update',
+    'page',
+    'drawer',
+    'footer',
+    'header',
+    'left_drawer',
+    'page_sticky',
+    'right_drawer',
+    'run',
+    'run_with',
+]
+
 from .deprecation import deprecated
 from .deprecation import deprecated
 from .element import Element as element
 from .element import Element as element
 from .elements.aggrid import AgGrid as aggrid
 from .elements.aggrid import AgGrid as aggrid
@@ -86,3 +171,4 @@ if os.environ.get('MATPLOTLIB', 'true').lower() == 'true':
     from .elements.line_plot import LinePlot as line_plot
     from .elements.line_plot import LinePlot as line_plot
     from .elements.pyplot import Pyplot as pyplot
     from .elements.pyplot import Pyplot as pyplot
     plot = deprecated(pyplot, 'ui.plot', 'ui.pyplot', 317)
     plot = deprecated(pyplot, 'ui.plot', 'ui.pyplot', 317)
+    __all__.extend(['line_plot', 'pyplot', 'plot'])

+ 22 - 0
tests/test_chart.py

@@ -109,3 +109,25 @@ def test_replace_chart(screen: Screen):
     screen.click('Replace')
     screen.click('Replace')
     screen.should_contain('B')
     screen.should_contain('B')
     screen.should_not_contain('A')
     screen.should_not_contain('A')
+
+
+def test_stock_chart(screen: Screen):
+    """https://github.com/zauberzeug/nicegui/discussions/948"""
+    chart = ui.chart({'legend': {'enabled': True}, 'series': []}, type='stockChart', extras=['stock'])
+    ui.button('update', on_click=lambda: (
+        chart.options['series'].extend([{'name': 'alice'}, {'name': 'bob'}]),
+        chart.update(),
+    ))
+    ui.button('clear', on_click=lambda: (
+        chart.options['series'].clear(),
+        chart.update(),
+    ))
+
+    screen.open('/')
+    screen.click('update')
+    screen.should_contain('alice')
+    screen.should_contain('bob')
+    screen.click('clear')
+    screen.wait(0.5)
+    screen.should_not_contain('alice')
+    screen.should_not_contain('bob')

+ 14 - 0
tests/test_date.py

@@ -50,3 +50,17 @@ def test_date_with_range_and_multi_selection(screen: Screen):
     screen.click('25')
     screen.click('25')
     screen.click('28')
     screen.click('28')
     screen.should_contain('8 days')
     screen.should_contain('8 days')
+
+
+def test_date_with_filter(screen: Screen):
+    d = ui.date().props('''default-year-month=2023/01 :options="date => date <= '2023/01/15'"''')
+    ui.label().bind_text_from(d, 'value')
+
+    screen.open('/')
+    screen.click('14')
+    screen.should_contain('2023-01-14')
+    screen.click('15')
+    screen.should_contain('2023-01-15')
+    screen.click('16')
+    screen.wait(0.5)
+    screen.should_not_contain('2023-01-16')

+ 37 - 0
tests/test_refreshable.py

@@ -61,3 +61,40 @@ async def test_async_refreshable(screen: Screen) -> None:
     numbers.clear()
     numbers.clear()
     screen.click('Refresh')
     screen.click('Refresh')
     screen.should_contain('[]')
     screen.should_contain('[]')
+
+
+def test_multiple_targets(screen: Screen) -> None:
+    count = 0
+
+    class MyClass:
+
+        def __init__(self, name: str) -> None:
+            self.name = name
+            self.state = 1
+
+        @ui.refreshable
+        def create_ui(self) -> None:
+            nonlocal count
+            count += 1
+            ui.label(f'{self.name} = {self.state} ({count})')
+            ui.button(f'increment {self.name}', on_click=self.increment)
+
+        def increment(self) -> None:
+            self.state += 1
+            self.create_ui.refresh()
+
+    a = MyClass('A')
+    a.create_ui()
+
+    b = MyClass('B')
+    b.create_ui()
+
+    screen.open('/')
+    screen.should_contain('A = 1 (1)')
+    screen.should_contain('B = 1 (2)')
+    screen.click('increment A')
+    screen.should_contain('A = 2 (3)')
+    screen.should_contain('B = 1 (2)')
+    screen.click('increment B')
+    screen.should_contain('A = 2 (3)')
+    screen.should_contain('B = 2 (4)')

+ 1 - 0
website/documentation.py

@@ -207,6 +207,7 @@ def create_full() -> None:
         NiceGUI uses the [Quasar Framework](https://quasar.dev/) version 1.0 and hence has its full design power.
         NiceGUI uses the [Quasar Framework](https://quasar.dev/) version 1.0 and hence has its full design power.
         Each NiceGUI element provides a `props` method whose content is passed [to the Quasar component](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components):
         Each NiceGUI element provides a `props` method whose content is passed [to the Quasar component](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components):
         Have a look at [the Quasar documentation](https://quasar.dev/vue-components/button#design) for all styling props.
         Have a look at [the Quasar documentation](https://quasar.dev/vue-components/button#design) for all styling props.
+        Props with a leading `:` can contain JavaScript expressions that are evaluated on the client.
         You can also apply [Tailwind CSS](https://tailwindcss.com/) utility classes with the `classes` method.
         You can also apply [Tailwind CSS](https://tailwindcss.com/) utility classes with the `classes` method.
 
 
         If you really need to apply CSS, you can use the `styles` method. Here the delimiter is `;` instead of a blank space.
         If you really need to apply CSS, you can use the `styles` method. Here the delimiter is `;` instead of a blank space.

+ 2 - 2
website/example_card.py

@@ -3,7 +3,7 @@ from nicegui import ui
 from . import svg
 from . import svg
 
 
 
 
-def create():
+def create() -> None:
     with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
     with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):
@@ -27,7 +27,7 @@ def create():
                 ui.radio(['A', 'B', 'C'], value='A', on_change=lambda e: output.set_text(e.value)).props('inline')
                 ui.radio(['A', 'B', 'C'], value='A', on_change=lambda e: output.set_text(e.value)).props('inline')
 
 
 
 
-def create_narrow():
+def create_narrow() -> None:
     with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
     with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):

+ 12 - 0
website/more_documentation/button_documentation.py

@@ -8,6 +8,18 @@ def main_demo() -> None:
 
 
 
 
 def more() -> None:
 def more() -> None:
+    @text_demo('Icons', '''
+        You can also add an icon to a button.
+    ''')
+    async def icons() -> None:
+        with ui.row():
+            ui.button('demo').props('icon=history')
+            ui.button().props('icon=thumb_up')
+            with ui.button():
+                ui.label('sub-elements')
+                ui.image('https://picsum.photos/id/377/640/360') \
+                    .classes('rounded-full w-16 h-16 ml-4')
+
     @text_demo('Await button click', '''
     @text_demo('Await button click', '''
         Sometimes it is convenient to wait for a button click before continuing the execution.
         Sometimes it is convenient to wait for a button click before continuing the execution.
     ''')
     ''')

+ 15 - 0
website/more_documentation/column_documentation.py

@@ -1,8 +1,23 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from ..documentation_tools import text_demo
+
 
 
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.column():
     with ui.column():
         ui.label('label 1')
         ui.label('label 1')
         ui.label('label 2')
         ui.label('label 2')
         ui.label('label 3')
         ui.label('label 3')
+
+
+def more() -> None:
+    @text_demo('Masonry or Pinterest-Style Layout', '''
+        To create a masonry/Pinterest layout, the normal `ui.column` can not be used.
+        But it can be achieved with a few TailwindCSS classes.
+    ''')
+    def masonry() -> None:
+        with ui.element('div').classes('columns-3 w-full gap-2'):
+            for i, height in enumerate([50, 50, 50, 150, 100, 50]):
+                tailwind = f'mb-2 p-2 h-[{height}px] bg-blue-100 break-inside-avoid'
+                with ui.card().classes(tailwind):
+                    ui.label(f'Card #{i+1}')

+ 8 - 0
website/more_documentation/date_documentation.py

@@ -23,3 +23,11 @@ def more() -> None:
                 ui.icon('edit_calendar').on('click', lambda: menu.open()).classes('cursor-pointer')
                 ui.icon('edit_calendar').on('click', lambda: menu.open()).classes('cursor-pointer')
             with ui.menu() as menu:
             with ui.menu() as menu:
                 ui.date().bind_value(date)
                 ui.date().bind_value(date)
+
+    @text_demo('Date filter', '''
+        This demo shows how to filter the dates in a date picker.
+        In order to pass a function to the date picker, we use the `:options` property.
+        The leading `:` tells NiceGUI that the value is a JavaScript expression.
+    ''')
+    def date_filter():
+        ui.date().props('''default-year-month=2023/01 :options="date => date <= '2023/01/15'"''')

+ 21 - 0
website/more_documentation/label_documentation.py

@@ -1,5 +1,26 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from ..documentation_tools import text_demo
+
 
 
 def main_demo() -> None:
 def main_demo() -> None:
     ui.label('some label')
     ui.label('some label')
+
+
+def more() -> None:
+    @text_demo('Change Appearance Depending on the Content', '''
+        You can overwrite the `on_text_change` method to update other attributes of a label depending on its content. 
+        This technique also works for bindings as shown in the example below.
+    ''')
+    def status():
+        class status_label(ui.label):
+            def on_text_change(self, text: str) -> None:
+                super().on_text_change(text)
+                if text == 'ok':
+                    self.classes(replace='text-positive')
+                else:
+                    self.classes(replace='text-negative')
+
+        model = {'status': 'error'}
+        status_label().bind_text_from(model, 'status')
+        ui.switch(on_change=lambda e: model.update(status='ok' if e.value else 'error'))