فهرست منبع

Add handler type alias and use it in event handlers (#3811)

* Add Handler type alias

* Replace Callable with Handler in elements

* Fix imports and move Handler to events module

* fix confusion between select and change event handler

* use relative import statements

* minor cleanup

* fix mypy issue

* use Handler for generic events

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Marcus Lim 7 ماه پیش
والد
کامیت
b7b6956f26
49فایلهای تغییر یافته به همراه234 افزوده شده و 176 حذف شده
  1. 3 16
      nicegui/element.py
  2. 4 4
      nicegui/elements/button.py
  3. 4 4
      nicegui/elements/button_dropdown.py
  4. 3 2
      nicegui/elements/carousel.py
  5. 3 2
      nicegui/elements/checkbox.py
  6. 6 6
      nicegui/elements/chip.py
  7. 3 2
      nicegui/elements/choice_element.py
  8. 3 3
      nicegui/elements/codemirror.py
  9. 3 2
      nicegui/elements/color_input.py
  10. 4 4
      nicegui/elements/color_picker.py
  11. 3 2
      nicegui/elements/dark_mode.py
  12. 3 2
      nicegui/elements/date.py
  13. 4 4
      nicegui/elements/echart.py
  14. 3 2
      nicegui/elements/editor.py
  15. 3 2
      nicegui/elements/expansion.py
  16. 2 1
      nicegui/elements/input.py
  17. 4 4
      nicegui/elements/interactive_image.py
  18. 4 4
      nicegui/elements/item.py
  19. 8 8
      nicegui/elements/joystick.py
  20. 12 6
      nicegui/elements/json_editor.py
  21. 4 3
      nicegui/elements/keyboard.py
  22. 3 2
      nicegui/elements/knob.py
  23. 3 2
      nicegui/elements/menu.py
  24. 4 4
      nicegui/elements/mixins/selectable_element.py
  25. 4 4
      nicegui/elements/mixins/value_element.py
  26. 4 4
      nicegui/elements/notification.py
  27. 2 2
      nicegui/elements/number.py
  28. 5 4
      nicegui/elements/pagination.py
  29. 3 3
      nicegui/elements/radio.py
  30. 3 2
      nicegui/elements/range.py
  31. 7 6
      nicegui/elements/scene.py
  32. 11 4
      nicegui/elements/scene_view.py
  33. 5 5
      nicegui/elements/scroll_area.py
  34. 2 2
      nicegui/elements/select.py
  35. 3 2
      nicegui/elements/slider.py
  36. 3 2
      nicegui/elements/splitter.py
  37. 3 2
      nicegui/elements/stepper.py
  38. 3 2
      nicegui/elements/switch.py
  39. 13 7
      nicegui/elements/table.py
  40. 4 3
      nicegui/elements/tabs.py
  41. 3 2
      nicegui/elements/textarea.py
  42. 3 2
      nicegui/elements/time.py
  43. 3 3
      nicegui/elements/toggle.py
  44. 8 8
      nicegui/elements/tree.py
  45. 8 8
      nicegui/elements/upload.py
  46. 24 3
      nicegui/events.py
  47. 3 2
      nicegui/functions/on.py
  48. 8 0
      tests/test_radio_element.py
  49. 3 3
      website/documentation/content/storage_documentation.py

+ 3 - 16
nicegui/element.py

@@ -4,20 +4,7 @@ import inspect
 import re
 from copy import copy
 from pathlib import Path
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Callable,
-    ClassVar,
-    Dict,
-    Iterator,
-    List,
-    Optional,
-    Sequence,
-    Union,
-    cast,
-    overload,
-)
+from typing import TYPE_CHECKING, Any, ClassVar, Dict, Iterator, List, Optional, Sequence, Union, cast, overload
 
 from typing_extensions import Self
 
@@ -334,7 +321,7 @@ class Element(Visibility):
     @overload
     def on(self,
            type: str,  # pylint: disable=redefined-builtin
-           handler: Optional[Callable[..., Any]] = None,
+           handler: Optional[events.Handler[events.GenericEventArguments]] = None,
            args: Union[None, Sequence[str], Sequence[Optional[Sequence[str]]]] = None,
            *,
            throttle: float = 0.0,
@@ -345,7 +332,7 @@ class Element(Visibility):
 
     def on(self,
            type: str,  # pylint: disable=redefined-builtin
-           handler: Optional[Callable[..., Any]] = None,
+           handler: Optional[events.Handler[events.GenericEventArguments]] = None,
            args: Union[None, Sequence[str], Sequence[Optional[Sequence[str]]]] = None,
            *,
            throttle: float = 0.0,

+ 4 - 4
nicegui/elements/button.py

@@ -1,9 +1,9 @@
 import asyncio
-from typing import Any, Callable, Optional
+from typing import Optional
 
 from typing_extensions import Self
 
-from ..events import ClickEventArguments, handle_event
+from ..events import ClickEventArguments, Handler, handle_event
 from .mixins.color_elements import BackgroundColorElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.icon_element import IconElement
@@ -14,7 +14,7 @@ class Button(IconElement, TextElement, DisableableElement, BackgroundColorElemen
 
     def __init__(self,
                  text: str = '', *,
-                 on_click: Optional[Callable[..., Any]] = None,
+                 on_click: Optional[Handler[ClickEventArguments]] = None,
                  color: Optional[str] = 'primary',
                  icon: Optional[str] = None,
                  ) -> None:
@@ -37,7 +37,7 @@ class Button(IconElement, TextElement, DisableableElement, BackgroundColorElemen
         if on_click:
             self.on_click(on_click)
 
-    def on_click(self, callback: Callable[..., Any]) -> Self:
+    def on_click(self, callback: Handler[ClickEventArguments]) -> Self:
         """Add a callback to be invoked when the button is clicked."""
         self.on('click', lambda _: handle_event(callback, ClickEventArguments(sender=self, client=self.client)), [])
         return self

+ 4 - 4
nicegui/elements/button_dropdown.py

@@ -1,6 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
-from ..events import ClickEventArguments, handle_event
+from ..events import ClickEventArguments, Handler, ValueChangeEventArguments, handle_event
 from .mixins.color_elements import BackgroundColorElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.icon_element import IconElement
@@ -13,8 +13,8 @@ class DropdownButton(IconElement, TextElement, DisableableElement, BackgroundCol
     def __init__(self,
                  text: str = '', *,
                  value: bool = False,
-                 on_value_change: Optional[Callable[..., Any]] = None,
-                 on_click: Optional[Callable[..., Any]] = None,
+                 on_value_change: Optional[Handler[ValueChangeEventArguments]] = None,
+                 on_click: Optional[Handler[ClickEventArguments]] = None,
                  color: Optional[str] = 'primary',
                  icon: Optional[str] = None,
                  auto_close: Optional[bool] = False,

+ 3 - 2
nicegui/elements/carousel.py

@@ -1,8 +1,9 @@
 from __future__ import annotations
 
-from typing import Any, Callable, Optional, Union, cast
+from typing import Any, Optional, Union, cast
 
 from ..context import context
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
@@ -11,7 +12,7 @@ class Carousel(ValueElement):
 
     def __init__(self, *,
                  value: Union[str, CarouselSlide, None] = None,
-                 on_value_change: Optional[Callable[..., Any]] = None,
+                 on_value_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  animated: bool = False,
                  arrows: bool = False,
                  navigation: bool = False,

+ 3 - 2
nicegui/elements/checkbox.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 from .mixins.value_element import ValueElement
@@ -7,7 +8,7 @@ from .mixins.value_element import ValueElement
 
 class Checkbox(TextElement, ValueElement, DisableableElement):
 
-    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable[..., Any]] = None) -> None:
+    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Handler[ValueChangeEventArguments]] = None) -> None:
         """Checkbox
 
         This element is based on Quasar's `QCheckbox <https://quasar.dev/vue-components/checkbox>`_ component.

+ 6 - 6
nicegui/elements/chip.py

@@ -1,8 +1,8 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
 from typing_extensions import Self
 
-from ..events import ClickEventArguments, handle_event
+from ..events import ClickEventArguments, Handler, ValueChangeEventArguments, handle_event
 from .mixins.color_elements import BackgroundColorElement, TextColorElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.icon_element import IconElement
@@ -20,12 +20,12 @@ class Chip(IconElement, ValueElement, TextElement, BackgroundColorElement, TextC
                  icon: Optional[str] = None,
                  color: Optional[str] = 'primary',
                  text_color: Optional[str] = None,
-                 on_click: Optional[Callable[..., Any]] = None,
+                 on_click: Optional[Handler[ClickEventArguments]] = None,
                  selectable: bool = False,
                  selected: bool = False,
-                 on_selection_change: Optional[Callable[..., Any]] = None,
+                 on_selection_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  removable: bool = False,
-                 on_value_change: Optional[Callable[..., Any]] = None,
+                 on_value_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Chip
 
@@ -52,7 +52,7 @@ class Chip(IconElement, ValueElement, TextElement, BackgroundColorElement, TextC
         if on_click:
             self.on_click(on_click)
 
-    def on_click(self, callback: Callable[..., Any]) -> Self:
+    def on_click(self, callback: Handler[ClickEventArguments]) -> Self:
         """Add a callback to be invoked when the chip is clicked."""
         self._props['clickable'] = True
         self.update()

+ 3 - 2
nicegui/elements/choice_element.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional, Union
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.value_element import ValueElement
 
 
@@ -9,7 +10,7 @@ class ChoiceElement(ValueElement):
                  tag: Optional[str] = None,
                  options: Union[List, Dict],
                  value: Any,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         self.options = options
         self._values: List[str] = []

+ 3 - 3
nicegui/elements/codemirror.py

@@ -1,9 +1,9 @@
 from pathlib import Path
-from typing import Any, Callable, List, Literal, Optional, get_args
+from typing import Callable, List, Literal, Optional, get_args
 
 from nicegui.elements.mixins.disableable_element import DisableableElement
 from nicegui.elements.mixins.value_element import ValueElement
-from nicegui.events import GenericEventArguments
+from nicegui.events import GenericEventArguments, Handler, ValueChangeEventArguments
 
 SUPPORTED_LANGUAGES = Literal[
     'Angular Template',
@@ -253,7 +253,7 @@ class CodeMirror(ValueElement, DisableableElement, component='codemirror.js'):
         self,
         value: str = '',
         *,
-        on_change: Optional[Callable[..., Any]] = None,
+        on_change: Optional[Handler[ValueChangeEventArguments]] = None,
         language: Optional[SUPPORTED_LANGUAGES] = None,
         theme: SUPPORTED_THEMES = 'basicLight',
         indent: str = ' ' * 4,

+ 3 - 2
nicegui/elements/color_input.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Any, Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .button import Button as button
 from .color_picker import ColorPicker as color_picker
 from .mixins.disableable_element import DisableableElement
@@ -13,7 +14,7 @@ class ColorInput(ValueElement, DisableableElement):
                  label: Optional[str] = None, *,
                  placeholder: Optional[str] = None,
                  value: str = '',
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  preview: bool = False,
                  ) -> None:
         """Color Input

+ 4 - 4
nicegui/elements/color_picker.py

@@ -1,16 +1,16 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
 from typing_extensions import Self
 
 from ..element import Element
-from ..events import ColorPickEventArguments, GenericEventArguments, handle_event
+from ..events import ColorPickEventArguments, GenericEventArguments, Handler, handle_event
 from .menu import Menu
 
 
 class ColorPicker(Menu):
 
     def __init__(self, *,
-                 on_pick: Optional[Callable[..., Any]] = None,
+                 on_pick: Optional[Handler[ColorPickEventArguments]] = None,
                  value: bool = False,
                  ) -> None:
         """Color Picker
@@ -36,7 +36,7 @@ class ColorPicker(Menu):
         """
         self.q_color.props(f'model-value="{color}"')
 
-    def on_pick(self, callback: Callable[..., Any]) -> Self:
+    def on_pick(self, callback: Handler[ColorPickEventArguments]) -> Self:
         """Add a callback to be invoked when a color is picked."""
         self._pick_handlers.append(callback)
         return self

+ 3 - 2
nicegui/elements/dark_mode.py

@@ -1,12 +1,13 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.value_element import ValueElement
 
 
 class DarkMode(ValueElement, component='dark_mode.js'):
     VALUE_PROP = 'value'
 
-    def __init__(self, value: Optional[bool] = False, *, on_change: Optional[Callable[..., Any]] = None) -> None:
+    def __init__(self, value: Optional[bool] = False, *, on_change: Optional[Handler[ValueChangeEventArguments]] = None) -> None:
         """Dark mode
 
         You can use this element to enable, disable or toggle dark mode on the page.

+ 3 - 2
nicegui/elements/date.py

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

+ 4 - 4
nicegui/elements/echart.py

@@ -1,11 +1,11 @@
-from typing import Any, Callable, Dict, Optional
+from typing import Callable, Dict, Optional
 
 from typing_extensions import Self
 
 from .. import optional_features
 from ..awaitable_response import AwaitableResponse
 from ..element import Element
-from ..events import EChartPointClickEventArguments, GenericEventArguments, handle_event
+from ..events import EChartPointClickEventArguments, GenericEventArguments, Handler, handle_event
 
 try:
     from pyecharts.charts.base import default, json
@@ -19,7 +19,7 @@ except ImportError:
 
 class EChart(Element, component='echart.js', dependencies=['lib/echarts/echarts.min.js', 'lib/echarts-gl/echarts-gl.min.js']):
 
-    def __init__(self, options: Dict, on_point_click: Optional[Callable] = None, *, enable_3d: bool = False) -> None:
+    def __init__(self, options: Dict, on_point_click: Optional[Handler[EChartPointClickEventArguments]] = None, *, enable_3d: bool = False) -> None:
         """Apache EChart
 
         An element to create a chart using `ECharts <https://echarts.apache.org/>`_.
@@ -38,7 +38,7 @@ class EChart(Element, component='echart.js', dependencies=['lib/echarts/echarts.
         if on_point_click:
             self.on_point_click(on_point_click)
 
-    def on_point_click(self, callback: Callable[..., Any]) -> Self:
+    def on_point_click(self, callback: Handler[EChartPointClickEventArguments]) -> Self:
         """Add a callback to be invoked when a point is clicked."""
         def handle_point_click(e: GenericEventArguments) -> None:
             handle_event(callback, EChartPointClickEventArguments(

+ 3 - 2
nicegui/elements/editor.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Any, Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
@@ -12,7 +13,7 @@ class Editor(ValueElement, DisableableElement, component='editor.js'):
                  *,
                  placeholder: Optional[str] = None,
                  value: str = '',
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Editor
 

+ 3 - 2
nicegui/elements/expansion.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.icon_element import IconElement
 from .mixins.text_element import TextElement
@@ -14,7 +15,7 @@ class Expansion(IconElement, TextElement, ValueElement, DisableableElement):
                  icon: Optional[str] = None,
                  group: Optional[str] = None,
                  value: bool = False,
-                 on_value_change: Optional[Callable[..., Any]] = None
+                 on_value_change: Optional[Handler[ValueChangeEventArguments]] = None
                  ) -> None:
         """Expansion Element
 

+ 2 - 1
nicegui/elements/input.py

@@ -1,5 +1,6 @@
 from typing import Any, Callable, Dict, List, Optional, Union
 
+from ..events import Handler, ValueChangeEventArguments
 from .icon import Icon
 from .mixins.disableable_element import DisableableElement
 from .mixins.validation_element import ValidationElement
@@ -15,7 +16,7 @@ class Input(ValidationElement, DisableableElement, component='input.js'):
                  value: str = '',
                  password: bool = False,
                  password_toggle_button: bool = False,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  autocomplete: Optional[List[str]] = None,
                  validation: Optional[Union[Callable[..., Optional[str]], Dict[str, Callable[..., bool]]]] = None,
                  ) -> None:

+ 4 - 4
nicegui/elements/interactive_image.py

@@ -2,12 +2,12 @@ from __future__ import annotations
 
 import time
 from pathlib import Path
-from typing import Any, Callable, List, Optional, Tuple, Union, cast
+from typing import List, Optional, Tuple, Union, cast
 
 from typing_extensions import Self
 
 from .. import optional_features
-from ..events import GenericEventArguments, MouseEventArguments, handle_event
+from ..events import GenericEventArguments, Handler, MouseEventArguments, handle_event
 from .image import pil_to_base64
 from .mixins.content_element import ContentElement
 from .mixins.source_element import SourceElement
@@ -27,7 +27,7 @@ class InteractiveImage(SourceElement, ContentElement, component='interactive_ima
                  source: Union[str, Path, 'PIL_Image'] = '', *,  # noqa: UP037
                  content: str = '',
                  size: Optional[Tuple[float, float]] = None,
-                 on_mouse: Optional[Callable[..., Any]] = None,
+                 on_mouse: Optional[Handler[MouseEventArguments]] = None,
                  events: List[str] = ['click'],  # noqa: B006
                  cross: Union[bool, str] = False,
                  ) -> None:
@@ -67,7 +67,7 @@ class InteractiveImage(SourceElement, ContentElement, component='interactive_ima
     def set_source(self, source: Union[str, Path, 'PIL_Image']) -> None:  # noqa: UP037
         return super().set_source(source)
 
-    def on_mouse(self, on_mouse: Callable[..., Any]) -> Self:
+    def on_mouse(self, on_mouse: Handler[MouseEventArguments]) -> Self:
         """Add a callback to be invoked when a mouse event occurs."""
         def handle_mouse(e: GenericEventArguments) -> None:
             args = cast(dict, e.args)

+ 4 - 4
nicegui/elements/item.py

@@ -1,15 +1,15 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
 from typing_extensions import Self
 
-from ..events import ClickEventArguments, handle_event
+from ..events import ClickEventArguments, Handler, handle_event
 from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 
 
 class Item(DisableableElement):
 
-    def __init__(self, text: str = '', *, on_click: Optional[Callable[..., Any]] = None) -> None:
+    def __init__(self, text: str = '', *, on_click: Optional[Handler[ClickEventArguments]] = None) -> None:
         """List Item
 
         Creates a clickable list item based on Quasar's
@@ -30,7 +30,7 @@ class Item(DisableableElement):
             with self:
                 ItemSection(text=text)
 
-    def on_click(self, callback: Callable[..., Any]) -> Self:
+    def on_click(self, callback: Handler[ClickEventArguments]) -> Self:
         """Add a callback to be invoked when the List Item is clicked."""
         self._props['clickable'] = True  # idempotent
         self.on('click', lambda _: handle_event(callback, ClickEventArguments(sender=self, client=self.client)))

+ 8 - 8
nicegui/elements/joystick.py

@@ -1,17 +1,17 @@
-from typing import Any, Callable, Optional
+from typing import Any, Optional
 
 from typing_extensions import Self
 
 from ..element import Element
-from ..events import GenericEventArguments, JoystickEventArguments, handle_event
+from ..events import GenericEventArguments, Handler, JoystickEventArguments, handle_event
 
 
 class Joystick(Element, component='joystick.vue', dependencies=['lib/nipplejs/nipplejs.js']):
 
     def __init__(self, *,
-                 on_start: Optional[Callable[..., Any]] = None,
-                 on_move: Optional[Callable[..., Any]] = None,
-                 on_end: Optional[Callable[..., Any]] = None,
+                 on_start: Optional[Handler[JoystickEventArguments]] = None,
+                 on_move: Optional[Handler[JoystickEventArguments]] = None,
+                 on_end: Optional[Handler[JoystickEventArguments]] = None,
                  throttle: float = 0.05,
                  ** options: Any) -> None:
         """Joystick
@@ -61,17 +61,17 @@ class Joystick(Element, component='joystick.vue', dependencies=['lib/nipplejs/ni
         self.on('move', handle_move, ['data'], throttle=throttle)
         self.on('end', handle_end, [])
 
-    def on_start(self, callback: Callable[..., Any]) -> Self:
+    def on_start(self, callback: Handler[JoystickEventArguments]) -> Self:
         """Add a callback to be invoked when the user touches the joystick."""
         self._start_handlers.append(callback)
         return self
 
-    def on_move(self, callback: Callable[..., Any]) -> Self:
+    def on_move(self, callback: Handler[JoystickEventArguments]) -> Self:
         """Add a callback to be invoked when the user moves the joystick."""
         self._move_handlers.append(callback)
         return self
 
-    def on_end(self, callback: Callable[..., Any]) -> Self:
+    def on_end(self, callback: Handler[JoystickEventArguments]) -> Self:
         """Add a callback to be invoked when the user releases the joystick."""
         self._end_handlers.append(callback)
         return self

+ 12 - 6
nicegui/elements/json_editor.py

@@ -1,18 +1,24 @@
-from typing import Any, Callable, Dict, Optional
+from typing import Dict, Optional
 
 from typing_extensions import Self
 
 from ..awaitable_response import AwaitableResponse
 from ..element import Element
-from ..events import GenericEventArguments, JsonEditorChangeEventArguments, JsonEditorSelectEventArguments, handle_event
+from ..events import (
+    GenericEventArguments,
+    Handler,
+    JsonEditorChangeEventArguments,
+    JsonEditorSelectEventArguments,
+    handle_event,
+)
 
 
 class JsonEditor(Element, component='json_editor.js', dependencies=['lib/vanilla-jsoneditor/standalone.js']):
 
     def __init__(self,
                  properties: Dict, *,
-                 on_select: Optional[Callable] = None,
-                 on_change: Optional[Callable] = None,
+                 on_select: Optional[Handler[JsonEditorSelectEventArguments]] = None,
+                 on_change: Optional[Handler[JsonEditorChangeEventArguments]] = None,
                  ) -> None:
         """JSONEditor
 
@@ -33,14 +39,14 @@ class JsonEditor(Element, component='json_editor.js', dependencies=['lib/vanilla
         if on_change:
             self.on_change(on_change)
 
-    def on_change(self, callback: Callable[..., Any]) -> Self:
+    def on_change(self, callback: Handler[JsonEditorChangeEventArguments]) -> Self:
         """Add a callback to be invoked when the content changes."""
         def handle_on_change(e: GenericEventArguments) -> None:
             handle_event(callback, JsonEditorChangeEventArguments(sender=self, client=self.client, **e.args))
         self.on('change', handle_on_change, ['content', 'errors'])
         return self
 
-    def on_select(self, callback: Callable[..., Any]) -> Self:
+    def on_select(self, callback: Handler[JsonEditorSelectEventArguments]) -> Self:
         """Add a callback to be invoked when some of the content has been selected."""
         def handle_on_select(e: GenericEventArguments) -> None:
             handle_event(callback, JsonEditorSelectEventArguments(sender=self, client=self.client, **e.args))

+ 4 - 3
nicegui/elements/keyboard.py

@@ -1,4 +1,4 @@
-from typing import Any, Callable, List, Literal, Optional
+from typing import List, Literal, Optional
 
 from typing_extensions import Self
 
@@ -6,6 +6,7 @@ from ..binding import BindableProperty
 from ..element import Element
 from ..events import (
     GenericEventArguments,
+    Handler,
     KeyboardAction,
     KeyboardKey,
     KeyboardModifiers,
@@ -18,7 +19,7 @@ class Keyboard(Element, component='keyboard.js'):
     active = BindableProperty()
 
     def __init__(self,
-                 on_key: Optional[Callable[..., Any]] = None, *,
+                 on_key: Optional[Handler[KeyEventArguments]] = None, *,
                  active: bool = True,
                  repeating: bool = True,
                  ignore: List[Literal['input', 'select', 'button', 'textarea']] =
@@ -97,7 +98,7 @@ class Keyboard(Element, component='keyboard.js'):
         for handler in self._key_handlers:
             handle_event(handler, arguments)
 
-    def on_key(self, handler: Callable[..., Any]) -> Self:
+    def on_key(self, handler: Handler[KeyEventArguments]) -> Self:
         """Add a callback to be invoked when keyboard events occur."""
         self._key_handlers.append(handler)
         return self

+ 3 - 2
nicegui/elements/knob.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .label import Label
 from .mixins.color_elements import TextColorElement
 from .mixins.disableable_element import DisableableElement
@@ -19,7 +20,7 @@ class Knob(ValueElement, DisableableElement, TextColorElement):
                  track_color: Optional[str] = None,
                  size: Optional[str] = None,
                  show_value: bool = False,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Knob
 

+ 3 - 2
nicegui/elements/menu.py

@@ -1,6 +1,7 @@
-from typing import Any, Callable, Optional, Union
+from typing import Optional, Union
 
 from ..element import Element
+from ..events import ClickEventArguments, Handler
 from .context_menu import ContextMenu
 from .item import Item
 from .mixins.value_element import ValueElement
@@ -43,7 +44,7 @@ class MenuItem(Item):
 
     def __init__(self,
                  text: str = '',
-                 on_click: Optional[Callable[..., Any]] = None, *,
+                 on_click: Optional[Handler[ClickEventArguments]] = None, *,
                  auto_close: bool = True,
                  ) -> None:
         """Menu Item

+ 4 - 4
nicegui/elements/mixins/selectable_element.py

@@ -4,7 +4,7 @@ from typing_extensions import Self
 
 from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
-from ...events import ValueChangeEventArguments, handle_event
+from ...events import Handler, ValueChangeEventArguments, handle_event
 
 
 class SelectableElement(Element):
@@ -14,7 +14,7 @@ class SelectableElement(Element):
     def __init__(self, *,
                  selectable: bool,
                  selected: bool,
-                 on_selection_change: Optional[Callable[..., Any]],
+                 on_selection_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  **kwargs: Any) -> None:
         super().__init__(**kwargs)
         if not selectable:
@@ -27,11 +27,11 @@ class SelectableElement(Element):
         self.set_selected(selected)
         self.on('update:selected', lambda e: self.set_selected(e.args))
 
-        self._selection_change_handlers: List[Callable[..., Any]] = []
+        self._selection_change_handlers: List[Handler[ValueChangeEventArguments]] = []
         if on_selection_change:
             self.on_selection_change(on_selection_change)
 
-    def on_selection_change(self, callback: Callable[..., Any]) -> Self:
+    def on_selection_change(self, callback: Handler[ValueChangeEventArguments]) -> Self:
         """Add a callback to be invoked when the selection state changes."""
         self._selection_change_handlers.append(callback)
         return self

+ 4 - 4
nicegui/elements/mixins/value_element.py

@@ -4,7 +4,7 @@ from typing_extensions import Self
 
 from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
-from ...events import GenericEventArguments, ValueChangeEventArguments, handle_event
+from ...events import GenericEventArguments, Handler, ValueChangeEventArguments, handle_event
 
 
 class ValueElement(Element):
@@ -24,7 +24,7 @@ class ValueElement(Element):
 
     def __init__(self, *,
                  value: Any,
-                 on_value_change: Optional[Callable[..., Any]],
+                 on_value_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  throttle: float = 0,
                  **kwargs: Any,
                  ) -> None:
@@ -33,7 +33,7 @@ class ValueElement(Element):
         self.set_value(value)
         self._props[self.VALUE_PROP] = self._value_to_model_value(value)
         self._props['loopback'] = self.LOOPBACK
-        self._change_handlers: List[Callable[..., Any]] = [on_value_change] if on_value_change else []
+        self._change_handlers: List[Handler[ValueChangeEventArguments]] = [on_value_change] if on_value_change else []
 
         def handle_change(e: GenericEventArguments) -> None:
             self._send_update_on_value_change = self.LOOPBACK is True
@@ -41,7 +41,7 @@ class ValueElement(Element):
             self._send_update_on_value_change = True
         self.on(f'update:{self.VALUE_PROP}', handle_change, [None], throttle=throttle)
 
-    def on_value_change(self, callback: Callable[..., Any]) -> Self:
+    def on_value_change(self, callback: Handler[ValueChangeEventArguments]) -> Self:
         """Add a callback to be invoked when the value changes."""
         self._change_handlers.append(callback)
         return self

+ 4 - 4
nicegui/elements/notification.py

@@ -1,11 +1,11 @@
 import asyncio
-from typing import Any, Callable, Dict, Literal, Optional, Union
+from typing import Any, Dict, Literal, Optional, Union
 
 from typing_extensions import Self
 
 from ..context import context
 from ..element import Element
-from ..events import UiEventArguments, handle_event
+from ..events import Handler, UiEventArguments, handle_event
 
 NotificationPosition = Literal[
     'top-left',
@@ -40,7 +40,7 @@ class Notification(Element, component='notification.js'):
                  icon: Optional[str] = None,
                  spinner: bool = False,
                  timeout: Optional[float] = 5.0,
-                 on_dismiss: Optional[Callable] = None,
+                 on_dismiss: Optional[Handler[UiEventArguments]] = None,
                  options: Optional[Dict] = None,
                  **kwargs: Any,
                  ) -> None:
@@ -188,7 +188,7 @@ class Notification(Element, component='notification.js'):
         self._props['options']['closeBtn'] = value
         self.update()
 
-    def on_dismiss(self, callback: Callable[..., Any]) -> Self:
+    def on_dismiss(self, callback: Handler[UiEventArguments]) -> Self:
         """Add a callback to be invoked when the notification is dismissed."""
         self.on('dismiss', lambda _: handle_event(callback, UiEventArguments(sender=self, client=self.client)), [])
         return self

+ 2 - 2
nicegui/elements/number.py

@@ -1,6 +1,6 @@
 from typing import Any, Callable, Dict, Optional, Union
 
-from ..events import GenericEventArguments
+from ..events import GenericEventArguments, Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.validation_element import ValidationElement
 
@@ -19,7 +19,7 @@ class Number(ValidationElement, DisableableElement):
                  prefix: Optional[str] = None,
                  suffix: Optional[str] = None,
                  format: Optional[str] = None,  # pylint: disable=redefined-builtin
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  validation: Optional[Union[Callable[..., Optional[str]], Dict[str, Callable[..., bool]]]] = None,
                  ) -> None:
         """Number Input

+ 5 - 4
nicegui/elements/pagination.py

@@ -1,7 +1,8 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
-from nicegui.elements.mixins.disableable_element import DisableableElement
-from nicegui.elements.mixins.value_element import ValueElement
+from ..events import Handler, ValueChangeEventArguments
+from .mixins.disableable_element import DisableableElement
+from .mixins.value_element import ValueElement
 
 
 class Pagination(ValueElement, DisableableElement):
@@ -10,7 +11,7 @@ class Pagination(ValueElement, DisableableElement):
                  min: int, max: int, *,  # pylint: disable=redefined-builtin
                  direction_links: bool = False,
                  value: Optional[int] = ...,  # type: ignore
-                 on_change: Optional[Callable[..., Any]] = None) -> None:
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None) -> None:
         """Pagination
 
         A pagination element wrapping Quasar's `QPagination <https://quasar.dev/vue-components/pagination>`_ component.

+ 3 - 3
nicegui/elements/radio.py

@@ -1,6 +1,6 @@
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional, Union
 
-from ..events import GenericEventArguments
+from ..events import GenericEventArguments, Handler, ValueChangeEventArguments
 from .choice_element import ChoiceElement
 from .mixins.disableable_element import DisableableElement
 
@@ -10,7 +10,7 @@ class Radio(ChoiceElement, DisableableElement):
     def __init__(self,
                  options: Union[List, Dict], *,
                  value: Any = None,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Radio Selection
 

+ 3 - 2
nicegui/elements/range.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Dict, Optional
+from typing import Dict, Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
@@ -11,7 +12,7 @@ class Range(ValueElement, DisableableElement):
                  max: float,  # pylint: disable=redefined-builtin
                  step: float = 1.0,
                  value: Optional[Dict[str, int]] = None,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Range
 

+ 7 - 6
nicegui/elements/scene.py

@@ -9,6 +9,7 @@ from ..dataclasses import KWONLY_SLOTS
 from ..element import Element
 from ..events import (
     GenericEventArguments,
+    Handler,
     SceneClickEventArguments,
     SceneClickHit,
     SceneDragEventArguments,
@@ -74,10 +75,10 @@ class Scene(Element,
                  height: int = 300,
                  grid: Union[bool, Tuple[int, int]] = True,
                  camera: Optional[SceneCamera] = None,
-                 on_click: Optional[Callable[..., Any]] = None,
+                 on_click: Optional[Handler[SceneClickEventArguments]] = None,
                  click_events: List[str] = ['click', 'dblclick'],  # noqa: B006
-                 on_drag_start: Optional[Callable[..., Any]] = None,
-                 on_drag_end: Optional[Callable[..., Any]] = None,
+                 on_drag_start: Optional[Handler[SceneDragEventArguments]] = None,
+                 on_drag_end: Optional[Handler[SceneDragEventArguments]] = None,
                  drag_constraints: str = '',
                  background_color: str = '#eee',
                  ) -> None:
@@ -120,17 +121,17 @@ class Scene(Element,
         self._props['drag_constraints'] = drag_constraints
         self._classes.append('nicegui-scene')
 
-    def on_click(self, callback: Callable[..., Any]) -> Self:
+    def on_click(self, callback: Handler[SceneClickEventArguments]) -> Self:
         """Add a callback to be invoked when a 3D object is clicked."""
         self._click_handlers.append(callback)
         return self
 
-    def on_drag_start(self, callback: Callable[..., Any]) -> Self:
+    def on_drag_start(self, callback: Handler[SceneDragEventArguments]) -> Self:
         """Add a callback to be invoked when a 3D object is dragged."""
         self._drag_start_handlers.append(callback)
         return self
 
-    def on_drag_end(self, callback: Callable[..., Any]) -> Self:
+    def on_drag_end(self, callback: Handler[SceneDragEventArguments]) -> Self:
         """Add a callback to be invoked when a 3D object is dropped."""
         self._drag_end_handlers.append(callback)
         return self

+ 11 - 4
nicegui/elements/scene_view.py

@@ -1,10 +1,17 @@
 import asyncio
-from typing import Any, Callable, Optional
+from typing import Optional
 
 from typing_extensions import Self
 
 from ..element import Element
-from ..events import GenericEventArguments, SceneClickEventArguments, SceneClickHit, handle_event
+from ..events import (
+    ClickEventArguments,
+    GenericEventArguments,
+    Handler,
+    SceneClickEventArguments,
+    SceneClickHit,
+    handle_event,
+)
 from .scene import Scene, SceneCamera
 
 
@@ -20,7 +27,7 @@ class SceneView(Element,
                  width: int = 400,
                  height: int = 300,
                  camera: Optional[SceneCamera] = None,
-                 on_click: Optional[Callable[..., Any]] = None,
+                 on_click: Optional[Handler[ClickEventArguments]] = None,
                  ) -> None:
         """Scene View
 
@@ -48,7 +55,7 @@ class SceneView(Element,
         self.on('click3d', self._handle_click)
         self._classes.append('nicegui-scene-view')
 
-    def on_click(self, callback: Callable[..., Any]) -> Self:
+    def on_click(self, callback: Handler[ClickEventArguments]) -> Self:
         """Add a callback to be invoked when a 3D object is clicked."""
         self._click_handlers.append(callback)
         return self

+ 5 - 5
nicegui/elements/scroll_area.py

@@ -1,14 +1,14 @@
-from typing import Any, Callable, Literal, Optional
+from typing import Literal, Optional
 
 from typing_extensions import Self
 
 from ..element import Element
-from ..events import GenericEventArguments, ScrollEventArguments, handle_event
+from ..events import GenericEventArguments, Handler, ScrollEventArguments, handle_event
 
 
 class ScrollArea(Element):
 
-    def __init__(self, *, on_scroll: Optional[Callable[..., Any]] = None) -> None:
+    def __init__(self, *, on_scroll: Optional[Handler[ScrollEventArguments]] = None) -> None:
         """Scroll Area
 
         A way of customizing the scrollbars by encapsulating your content.
@@ -22,7 +22,7 @@ class ScrollArea(Element):
         if on_scroll:
             self.on_scroll(on_scroll)
 
-    def on_scroll(self, callback: Callable[..., Any]) -> Self:
+    def on_scroll(self, callback: Handler[ScrollEventArguments]) -> Self:
         """Add a callback to be invoked when the scroll position changes."""
         self.on('scroll', lambda e: self._handle_scroll(callback, e), args=[
             'verticalPosition',
@@ -36,7 +36,7 @@ class ScrollArea(Element):
         ])
         return self
 
-    def _handle_scroll(self, handler: Optional[Callable[..., Any]], e: GenericEventArguments) -> None:
+    def _handle_scroll(self, handler: Optional[Handler[ScrollEventArguments]], e: GenericEventArguments) -> None:
         handle_event(handler, ScrollEventArguments(
             sender=self,
             client=self.client,

+ 2 - 2
nicegui/elements/select.py

@@ -2,7 +2,7 @@ from collections.abc import Generator, Iterable
 from copy import deepcopy
 from typing import Any, Callable, Dict, Iterator, List, Literal, Optional, Union
 
-from ..events import GenericEventArguments
+from ..events import GenericEventArguments, Handler, ValueChangeEventArguments
 from .choice_element import ChoiceElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.validation_element import ValidationElement
@@ -14,7 +14,7 @@ class Select(ValidationElement, ChoiceElement, DisableableElement, component='se
                  options: Union[List, Dict], *,
                  label: Optional[str] = None,
                  value: Any = None,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  with_input: bool = False,
                  new_value_mode: Optional[Literal['add', 'add-unique', 'toggle']] = None,
                  multiple: bool = False,

+ 3 - 2
nicegui/elements/slider.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
@@ -11,7 +12,7 @@ class Slider(ValueElement, DisableableElement):
                  max: float,  # pylint: disable=redefined-builtin
                  step: float = 1.0,
                  value: Optional[float] = None,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Slider
 

+ 3 - 2
nicegui/elements/splitter.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional, Tuple
+from typing import Optional, Tuple
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
@@ -11,7 +12,7 @@ class Splitter(ValueElement, DisableableElement):
                  reverse: Optional[bool] = False,
                  limits: Optional[Tuple[float, float]] = (0, 100),
                  value: Optional[float] = 50,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Splitter
 

+ 3 - 2
nicegui/elements/stepper.py

@@ -1,9 +1,10 @@
 from __future__ import annotations
 
-from typing import Any, Callable, Optional, Union, cast
+from typing import Any, Optional, Union, cast
 
 from ..context import context
 from ..element import Element
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.icon_element import IconElement
 from .mixins.value_element import ValueElement
@@ -13,7 +14,7 @@ class Stepper(ValueElement):
 
     def __init__(self, *,
                  value: Union[str, Step, None] = None,
-                 on_value_change: Optional[Callable[..., Any]] = None,
+                 on_value_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  keep_alive: bool = True,
                  ) -> None:
         """Stepper

+ 3 - 2
nicegui/elements/switch.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 from .mixins.value_element import ValueElement
@@ -7,7 +8,7 @@ from .mixins.value_element import ValueElement
 
 class Switch(TextElement, ValueElement, DisableableElement):
 
-    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable[..., Any]] = None) -> None:
+    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Handler[ValueChangeEventArguments]] = None) -> None:
         """Switch
 
         This element is based on Quasar's `QToggle <https://quasar.dev/vue-components/toggle>`_ component.

+ 13 - 7
nicegui/elements/table.py

@@ -1,11 +1,17 @@
 import importlib.util
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union
 
 from typing_extensions import Self
 
 from .. import optional_features
 from ..element import Element
-from ..events import GenericEventArguments, TableSelectionEventArguments, ValueChangeEventArguments, handle_event
+from ..events import (
+    GenericEventArguments,
+    Handler,
+    TableSelectionEventArguments,
+    ValueChangeEventArguments,
+    handle_event,
+)
 from ..helpers import warn_once
 from .mixins.filter_element import FilterElement
 
@@ -26,8 +32,8 @@ class Table(FilterElement, component='table.js'):
                  title: Optional[str] = None,
                  selection: Optional[Literal['single', 'multiple']] = None,
                  pagination: Optional[Union[int, dict]] = None,
-                 on_select: Optional[Callable[..., Any]] = None,
-                 on_pagination_change: Optional[Callable[..., Any]] = None,
+                 on_select: Optional[Handler[TableSelectionEventArguments]] = None,
+                 on_pagination_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Table
 
@@ -86,12 +92,12 @@ class Table(FilterElement, component='table.js'):
                 handle_event(handler, arguments)
         self.on('update:pagination', handle_pagination_change)
 
-    def on_select(self, callback: Callable[..., Any]) -> Self:
+    def on_select(self, callback: Handler[TableSelectionEventArguments]) -> Self:
         """Add a callback to be invoked when the selection changes."""
         self._selection_handlers.append(callback)
         return self
 
-    def on_pagination_change(self, callback: Callable[..., Any]) -> Self:
+    def on_pagination_change(self, callback: Handler[ValueChangeEventArguments]) -> Self:
         """Add a callback to be invoked when the pagination changes."""
         self._pagination_change_handlers.append(callback)
         return self
@@ -108,7 +114,7 @@ class Table(FilterElement, component='table.js'):
                     title: Optional[str] = None,
                     selection: Optional[Literal['single', 'multiple']] = None,
                     pagination: Optional[Union[int, dict]] = None,
-                    on_select: Optional[Callable[..., Any]] = None) -> Self:
+                    on_select: Optional[Handler[TableSelectionEventArguments]] = None) -> Self:
         """Create a table from a Pandas DataFrame.
 
         Note:

+ 4 - 3
nicegui/elements/tabs.py

@@ -1,8 +1,9 @@
 from __future__ import annotations
 
-from typing import Any, Callable, Optional, Union
+from typing import Any, Optional, Union
 
 from ..context import context
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.icon_element import IconElement
 from .mixins.value_element import ValueElement
@@ -12,7 +13,7 @@ class Tabs(ValueElement):
 
     def __init__(self, *,
                  value: Union[Tab, TabPanel, None] = None,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Tabs
 
@@ -51,7 +52,7 @@ class TabPanels(ValueElement):
     def __init__(self,
                  tabs: Optional[Tabs] = None, *,
                  value: Union[Tab, TabPanel, str, None] = None,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  animated: bool = True,
                  keep_alive: bool = True,
                  ) -> None:

+ 3 - 2
nicegui/elements/textarea.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Dict, Optional, Union
+from typing import Callable, Dict, Optional, Union
 
+from ..events import Handler, ValueChangeEventArguments
 from .input import Input
 
 
@@ -9,7 +10,7 @@ class Textarea(Input, component='input.js'):
                  label: Optional[str] = None, *,
                  placeholder: Optional[str] = None,
                  value: str = '',
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  validation: Optional[Union[Callable[..., Optional[str]], Dict[str, Callable[..., bool]]]] = None,
                  ) -> None:
         """Textarea

+ 3 - 2
nicegui/elements/time.py

@@ -1,5 +1,6 @@
-from typing import Any, Callable, Optional
+from typing import Optional
 
+from ..events import Handler, ValueChangeEventArguments
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
@@ -9,7 +10,7 @@ class Time(ValueElement, DisableableElement):
     def __init__(self,
                  value: Optional[str] = None, *,
                  mask: str = 'HH:mm',
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  ) -> None:
         """Time Input
 

+ 3 - 3
nicegui/elements/toggle.py

@@ -1,6 +1,6 @@
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional, Union
 
-from ..events import GenericEventArguments
+from ..events import GenericEventArguments, Handler, ValueChangeEventArguments
 from .choice_element import ChoiceElement
 from .mixins.disableable_element import DisableableElement
 
@@ -10,7 +10,7 @@ class Toggle(ChoiceElement, DisableableElement):
     def __init__(self,
                  options: Union[List, Dict], *,
                  value: Any = None,
-                 on_change: Optional[Callable[..., Any]] = None,
+                 on_change: Optional[Handler[ValueChangeEventArguments]] = None,
                  clearable: bool = False,
                  ) -> None:
         """Toggle

+ 8 - 8
nicegui/elements/tree.py

@@ -1,8 +1,8 @@
-from typing import Any, Callable, Dict, Iterator, List, Literal, Optional, Set
+from typing import Any, Dict, Iterator, List, Literal, Optional, Set
 
 from typing_extensions import Self
 
-from ..events import GenericEventArguments, ValueChangeEventArguments, handle_event
+from ..events import GenericEventArguments, Handler, ValueChangeEventArguments, handle_event
 from .mixins.filter_element import FilterElement
 
 
@@ -13,9 +13,9 @@ class Tree(FilterElement):
                  node_key: str = 'id',
                  label_key: str = 'label',
                  children_key: str = 'children',
-                 on_select: Optional[Callable[..., Any]] = None,
-                 on_expand: Optional[Callable[..., Any]] = None,
-                 on_tick: Optional[Callable[..., Any]] = None,
+                 on_select: Optional[Handler[ValueChangeEventArguments]] = None,
+                 on_expand: Optional[Handler[ValueChangeEventArguments]] = None,
+                 on_tick: Optional[Handler[ValueChangeEventArguments]] = None,
                  tick_strategy: Optional[Literal['leaf', 'leaf-filtered', 'strict']] = None,
                  ) -> None:
         """Tree
@@ -77,7 +77,7 @@ class Tree(FilterElement):
                 handle_event(handler, ValueChangeEventArguments(sender=self, client=self.client, value=e.args))
         self.on('update:ticked', handle_ticked)
 
-    def on_select(self, callback: Callable[..., Any]) -> Self:
+    def on_select(self, callback: Handler[ValueChangeEventArguments]) -> Self:
         """Add a callback to be invoked when the selection changes."""
         self._select_handlers.append(callback)
         return self
@@ -96,12 +96,12 @@ class Tree(FilterElement):
         """Remove node selection."""
         return self.select(None)
 
-    def on_expand(self, callback: Callable[..., Any]) -> Self:
+    def on_expand(self, callback: Handler[ValueChangeEventArguments]) -> Self:
         """Add a callback to be invoked when the expansion changes."""
         self._expand_handlers.append(callback)
         return self
 
-    def on_tick(self, callback: Callable[..., Any]) -> Self:
+    def on_tick(self, callback: Handler[ValueChangeEventArguments]) -> Self:
         """Add a callback to be invoked when a node is ticked or unticked."""
         self._tick_handlers.append(callback)
         return self

+ 8 - 8
nicegui/elements/upload.py

@@ -1,10 +1,10 @@
-from typing import Any, Callable, Dict, List, Optional, cast
+from typing import Dict, List, Optional, cast
 
 from fastapi import Request
 from starlette.datastructures import UploadFile
 from typing_extensions import Self
 
-from ..events import MultiUploadEventArguments, UiEventArguments, UploadEventArguments, handle_event
+from ..events import Handler, MultiUploadEventArguments, UiEventArguments, UploadEventArguments, handle_event
 from ..nicegui import app
 from .mixins.disableable_element import DisableableElement
 
@@ -16,9 +16,9 @@ class Upload(DisableableElement, component='upload.js'):
                  max_file_size: Optional[int] = None,
                  max_total_size: Optional[int] = None,
                  max_files: Optional[int] = None,
-                 on_upload: Optional[Callable[..., Any]] = None,
-                 on_multi_upload: Optional[Callable[..., Any]] = None,
-                 on_rejected: Optional[Callable[..., Any]] = None,
+                 on_upload: Optional[Handler[UploadEventArguments]] = None,
+                 on_multi_upload: Optional[Handler[MultiUploadEventArguments]] = None,
+                 on_rejected: Optional[Handler[UiEventArguments]] = None,
                  label: str = '',
                  auto_upload: bool = False,
                  ) -> None:
@@ -91,17 +91,17 @@ class Upload(DisableableElement, component='upload.js'):
         for multi_upload_handler in self._multi_upload_handlers:
             handle_event(multi_upload_handler, multi_upload_args)
 
-    def on_upload(self, callback: Callable[..., Any]) -> Self:
+    def on_upload(self, callback: Handler[UploadEventArguments]) -> Self:
         """Add a callback to be invoked when a file is uploaded."""
         self._upload_handlers.append(callback)
         return self
 
-    def on_multi_upload(self, callback: Callable[..., Any]) -> Self:
+    def on_multi_upload(self, callback: Handler[MultiUploadEventArguments]) -> Self:
         """Add a callback to be invoked when multiple files have been uploaded."""
         self._multi_upload_handlers.append(callback)
         return self
 
-    def on_rejected(self, callback: Callable[..., Any]) -> Self:
+    def on_rejected(self, callback: Handler[UiEventArguments]) -> Self:
         """Add a callback to be invoked when a file is rejected."""
         self.on('rejected', lambda: handle_event(callback, UiEventArguments(sender=self, client=self.client)), args=[])
         return self

+ 24 - 3
nicegui/events.py

@@ -3,7 +3,21 @@ from __future__ import annotations
 from contextlib import nullcontext
 from dataclasses import dataclass
 from inspect import Parameter, signature
-from typing import TYPE_CHECKING, Any, Awaitable, BinaryIO, Callable, Dict, Iterator, List, Literal, Optional, Union
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Awaitable,
+    BinaryIO,
+    Callable,
+    Dict,
+    Iterator,
+    List,
+    Literal,
+    Optional,
+    TypeVar,
+    Union,
+    cast,
+)
 
 from . import background_tasks, core
 from .awaitable_response import AwaitableResponse
@@ -382,7 +396,11 @@ class JsonEditorChangeEventArguments(UiEventArguments):
     errors: Dict
 
 
-def handle_event(handler: Optional[Callable[..., Any]], arguments: EventArguments) -> None:
+EventT = TypeVar('EventT', bound=EventArguments)
+Handler = Union[Callable[[EventT], Any], Callable[[], Any]]
+
+
+def handle_event(handler: Optional[Handler[EventT]], arguments: EventT) -> None:
     """Call the given event handler.
 
     The handler is called within the context of the parent slot of the sender.
@@ -408,7 +426,10 @@ def handle_event(handler: Optional[Callable[..., Any]], arguments: EventArgument
             parent_slot = nullcontext()
 
         with parent_slot:
-            result = handler(arguments) if expects_arguments else handler()
+            if expects_arguments:
+                result = cast(Callable[[EventT], Any], handler)(arguments)
+            else:
+                result = cast(Callable[[], Any], handler)()
         if isinstance(result, Awaitable) and not isinstance(result, AwaitableResponse):
             # NOTE: await an awaitable result even if the handler is not a coroutine (like a lambda statement)
             async def wait_for_result():

+ 3 - 2
nicegui/functions/on.py

@@ -1,10 +1,11 @@
-from typing import Any, Callable, Optional, Sequence, Union
+from typing import Optional, Sequence, Union
 
 from ..context import context
+from ..events import GenericEventArguments, Handler
 
 
 def on(type: str,  # pylint: disable=redefined-builtin
-       handler: Optional[Callable[..., Any]] = None,
+       handler: Optional[Handler[GenericEventArguments]] = None,
        args: Union[None, Sequence[str], Sequence[Optional[Sequence[str]]]] = None, *,
        throttle: float = 0.0,
        leading_events: bool = True,

+ 8 - 0
tests/test_radio_element.py

@@ -1,6 +1,7 @@
 from nicegui import events, ui
 from nicegui.testing import Screen
 
+
 def test_radio_click(screen: Screen):
     r = ui.radio(['A', 'B', 'C'])
 
@@ -10,6 +11,7 @@ def test_radio_click(screen: Screen):
     screen.click('B')
     assert r.value == 'B'
 
+
 def test_radio_click_already_selected(screen: Screen):
     r = ui.radio(['A', 'B', 'C'], value='B')
 
@@ -17,6 +19,7 @@ def test_radio_click_already_selected(screen: Screen):
     screen.click('B')
     assert r.value == 'B'
 
+
 def test_radio_set_value(screen: Screen):
     r = ui.radio(['A', 'B', 'C'])
 
@@ -24,6 +27,7 @@ def test_radio_set_value(screen: Screen):
     r.set_value('B')
     assert r.value == 'B'
 
+
 def test_radio_set_options(screen: Screen):
     r = ui.radio(['A', 'B', 'C'])
 
@@ -31,6 +35,7 @@ def test_radio_set_options(screen: Screen):
     r.set_options(['D', 'E', 'F'])
     assert r.options == ['D', 'E', 'F']
 
+
 def test_radio_set_options_value_still_valid(screen: Screen):
     r = ui.radio(['A', 'B', 'C'], value='C')
 
@@ -38,6 +43,7 @@ def test_radio_set_options_value_still_valid(screen: Screen):
     r.set_options(['C', 'D', 'E'])
     assert r.value == 'C'
 
+
 def test_radio_set_options_value_none(screen: Screen):
     r = ui.radio(['A', 'B', 'C'], value='C')
 
@@ -45,6 +51,7 @@ def test_radio_set_options_value_none(screen: Screen):
     r.set_options(['D', 'E', 'F'])
     assert r.value is None
 
+
 def test_radio_set_options_value(screen: Screen):
     r = ui.radio(['A', 'B', 'C'])
 
@@ -52,6 +59,7 @@ def test_radio_set_options_value(screen: Screen):
     r.set_options(['D', 'E', 'F'], value='E')
     assert r.value == 'E'
 
+
 def test_radio_set_options_value_callback(screen: Screen):
     """Fix for https://github.com/zauberzeug/nicegui/issues/3733.
 

+ 3 - 3
website/documentation/content/storage_documentation.py

@@ -32,7 +32,7 @@ doc.title('Storage')
         Stored server-side, each dictionary is associated with a unique identifier held in a browser session cookie.
         Unique to each user, this storage is accessible across all their browser tabs.
         `app.storage.browser['id']` is used to identify the user.
-        This storage is only available within [page builder functions](/documentation/page) 
+        This storage is only available within [page builder functions](/documentation/page)
         and requires the `storage_secret` parameter in`ui.run()` to sign the browser session cookie.
     - `app.storage.general`:
         Also stored server-side, this dictionary provides a shared storage space accessible to all users.
@@ -40,11 +40,11 @@ doc.title('Storage')
         Unlike the previous types, this dictionary is stored directly as the browser session cookie, shared among all browser tabs for the same user.
         However, `app.storage.user` is generally preferred due to its advantages in reducing data payload, enhancing security, and offering larger storage capacity.
         By default, NiceGUI holds a unique identifier for the browser session in `app.storage.browser['id']`.
-        This storage is only available within [page builder functions](/documentation/page) 
+        This storage is only available within [page builder functions](/documentation/page)
         and requires the `storage_secret` parameter in`ui.run()` to sign the browser session cookie.
 
     The following table will help you to choose storage.
- 
+
     | Storage type                | `tab`  | `client` | `user` | `general` | `browser` |
     |-----------------------------|--------|----------|--------|-----------|-----------|
     | Location                    | Server | Server   | Server | Server    | Browser   |