1
0
Эх сурвалжийг харах

Merge pull request #2536 from WSH032/feature/js-event-listener

Introduce a pure JavaScript event handler
Rodja Trappe 1 жил өмнө
parent
commit
bd98fb42b7

+ 30 - 3
nicegui/element.py

@@ -5,7 +5,7 @@ import inspect
 import re
 from copy import copy, deepcopy
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence, Union, overload
 
 from typing_extensions import Self
 
@@ -388,14 +388,36 @@ class Element(Visibility):
             Tooltip(text)
         return self
 
+    @overload
+    def on(self,
+           type: str,  # pylint: disable=redefined-builtin
+           *,
+           js_handler: Optional[str] = None,
+           ) -> Self:
+        ...
+
+    @overload
     def on(self,
            type: str,  # pylint: disable=redefined-builtin
            handler: Optional[Callable[..., Any]] = None,
-           args: Union[None, Sequence[str], Sequence[Optional[Sequence[str]]]] = None, *,
+           args: Union[None, Sequence[str], Sequence[Optional[Sequence[str]]]] = None,
+           *,
            throttle: float = 0.0,
            leading_events: bool = True,
            trailing_events: bool = True,
            ) -> Self:
+        ...
+
+    def on(self,
+           type: str,  # pylint: disable=redefined-builtin
+           handler: Optional[Callable[..., Any]] = None,
+           args: Union[None, Sequence[str], Sequence[Optional[Sequence[str]]]] = None,
+           *,
+           throttle: float = 0.0,
+           leading_events: bool = True,
+           trailing_events: bool = True,
+           js_handler: Optional[str] = None,
+           ) -> Self:
         """Subscribe to an event.
 
         :param type: name of the event (e.g. "click", "mousedown", or "update:model-value")
@@ -404,13 +426,18 @@ class Element(Visibility):
         :param throttle: minimum time (in seconds) between event occurrences (default: 0.0)
         :param leading_events: whether to trigger the event handler immediately upon the first event occurrence (default: `True`)
         :param trailing_events: whether to trigger the event handler after the last event occurrence (default: `True`)
+        :param js_handler: JavaScript code that is executed upon occurrence of the event, e.g. `(evt) => alert(evt)` (default: `None`)
         """
-        if handler:
+        if handler and js_handler:
+            raise ValueError('Either handler or js_handler can be specified, but not both')
+
+        if handler or js_handler:
             listener = EventListener(
                 element_id=self.id,
                 type=helpers.kebab_to_camel_case(type),
                 args=[args] if args and isinstance(args[0], str) else args,  # type: ignore
                 handler=handler,
+                js_handler=js_handler,
                 throttle=throttle,
                 leading_events=leading_events,
                 trailing_events=trailing_events,

+ 2 - 2
nicegui/elements/code.py

@@ -28,8 +28,8 @@ class Code(Element):
         with self:
             self.markdown = markdown(f'```{language}\n{self.content}\n```').classes('overflow-auto')
             self.copy_button = button(icon='content_copy', on_click=self.show_checkmark) \
-                .props('round flat size=sm').classes('absolute right-2 top-2 opacity-20 hover:opacity-80')
-            self.copy_button._props['onclick'] = f'navigator.clipboard.writeText({json.dumps(self.content)})'
+                .props('round flat size=sm').classes('absolute right-2 top-2 opacity-20 hover:opacity-80') \
+                .on('click', js_handler=f'() => navigator.clipboard.writeText({json.dumps(self.content)})')
 
         self._last_scroll: float = 0.0
         self.markdown.on('scroll', self._handle_scroll)

+ 3 - 1
nicegui/event_listener.py

@@ -13,7 +13,8 @@ class EventListener:
     element_id: int
     type: str
     args: Sequence[Optional[Sequence[str]]]
-    handler: Callable
+    handler: Optional[Callable]
+    js_handler: Optional[str]
     throttle: float
     leading_events: bool
     trailing_events: bool
@@ -39,4 +40,5 @@ class EventListener:
             'throttle': self.throttle,
             'leading_events': self.leading_events,
             'trailing_events': self.trailing_events,
+            'js_handler': self.js_handler,
         }

+ 20 - 12
nicegui/templates/index.html

@@ -164,19 +164,27 @@
         element.events.forEach((event) => {
           let event_name = 'on' + event.type[0].toLocaleUpperCase() + event.type.substring(1);
           event.specials.forEach(s => event_name += s[0].toLocaleUpperCase() + s.substring(1));
-          let handler = (...args) => {
-            const data = {
-              id: element.id,
-              client_id: window.client_id,
-              listener_id: event.listener_id,
-              args: stringifyEventArgs(args, event.args),
+
+          let handler;
+          if (event.js_handler) {
+            handler = eval(event.js_handler);
+          }
+          else {
+            handler = (...args) => {
+              const data = {
+                id: element.id,
+                client_id: window.client_id,
+                listener_id: event.listener_id,
+                args: stringifyEventArgs(args, event.args),
+              };
+              const emitter = () => window.socket?.emit("event", data);
+              throttle(emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id);
+              if (element.props["loopback"] === False && event.type == "update:modelValue") {
+                element.props["model-value"] = args;
+              }
             };
-            const emitter = () => window.socket?.emit("event", data);
-            throttle(emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id);
-            if (element.props["loopback"] === False && event.type == "update:modelValue") {
-              element.props["model-value"] = args;
-            }
-          };
+          }
+
           handler = Vue.withModifiers(handler, event.modifiers);
           handler = event.keys.length ? Vue.withKeys(handler, event.keys) : handler;
           if (props[event_name]) {

+ 8 - 0
tests/test_events.py

@@ -176,3 +176,11 @@ def test_server_side_validation(screen: Screen, attribute: Literal['disabled', '
     screen.click('Hack')
     screen.wait(0.5)
     screen.should_not_contain('Success')
+
+
+def test_js_handler(screen: Screen) -> None:
+    ui.button('Button').on('click', js_handler='() => document.body.appendChild(document.createTextNode("Click!"))')
+
+    screen.open('/')
+    screen.click('Button')
+    screen.should_contain('Click!')

+ 12 - 0
website/documentation/content/generic_events_documentation.py

@@ -124,3 +124,15 @@ async def custom_events() -> None:
             }
         });
     ''')
+
+
+@doc.demo('Pure JavaScript events', '''
+    You can also use the `on` method to register a pure JavaScript event handler.
+    This can be useful if you want to call JavaScript code without sending any data to the server.
+    In this example we are using the `navigator.clipboard` API to copy a string to the clipboard.
+''')
+def pure_javascript() -> None:
+    ui.button('Copy to clipboard') \
+        .on('click', js_handler='''() => {
+            navigator.clipboard.writeText("Hello, NiceGUI!");
+        }''')

+ 2 - 2
website/documentation/demo.py

@@ -38,10 +38,10 @@ def demo(f: Callable, *, lazy: bool = True, tab: Optional[Union[str, Callable]]
         full_code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
         with python_window(classes='w-full max-w-[44rem]'):
             ui.markdown(f'````python\n{full_code}\n````')
-            icon = ui.icon('content_copy', size='xs') \
+            ui.icon('content_copy', size='xs') \
                 .classes('absolute right-2 top-10 opacity-10 hover:opacity-80 cursor-pointer') \
+                .on('click', js_handler=f'() => navigator.clipboard.writeText(`{full_code}`)') \
                 .on('click', lambda: ui.notify('Copied to clipboard', type='positive', color='primary'), [])
-            icon._props['onclick'] = f'navigator.clipboard.writeText(`{full_code}`)'  # pylint: disable=protected-access
         with browser_window(title=tab,
                             classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window') as window:
             if lazy: