浏览代码

feat: javascript event listener

WSH032 1 年之前
父节点
当前提交
d5e7fdd4a7
共有 4 个文件被更改,包括 76 次插入25 次删除
  1. 17 2
      nicegui/element.py
  2. 33 11
      nicegui/event_listener.py
  3. 20 12
      nicegui/templates/index.html
  4. 6 0
      tests/test_js_event_listener.py

+ 17 - 2
nicegui/element.py

@@ -13,7 +13,7 @@ from . import context, core, events, helpers, json, storage
 from .awaitable_response import AwaitableResponse, NullResponse
 from .dependencies import Component, Library, register_library, register_resource, register_vue_component
 from .elements.mixins.visibility import Visibility
-from .event_listener import EventListener
+from .event_listener import EventListener, JSEventListener
 from .slot import Slot
 from .tailwind import Tailwind
 from .version import __version__
@@ -85,6 +85,7 @@ class Element(Visibility):
         self._props: Dict[str, Any] = {'key': self.id}  # HACK: workaround for #600 and #898
         self._props.update(self._default_props)
         self._event_listeners: Dict[str, EventListener] = {}
+        self._js_event_listeners: List[JSEventListener] = []
         self._text: Optional[str] = None
         self.slots: Dict[str, Slot] = {}
         self.default_slot = self.add_slot('default')
@@ -187,6 +188,8 @@ class Element(Visibility):
         }
 
     def _to_dict(self) -> Dict[str, Any]:
+        events = [listener.to_dict() for listener in self._event_listeners.values()]
+        events.extend([listener.to_dict() for listener in self._js_event_listeners])
         return {
             'id': self.id,
             'tag': self.tag,
@@ -195,7 +198,7 @@ class Element(Visibility):
             'props': self._props,
             'text': self._text,
             'slots': self._collect_slot_dict(),
-            'events': [listener.to_dict() for listener in self._event_listeners.values()],
+            'events': events,
             'component': {
                 'key': self.component.key,
                 'name': self.component.name,
@@ -395,6 +398,7 @@ class Element(Visibility):
            throttle: float = 0.0,
            leading_events: bool = True,
            trailing_events: bool = True,
+           js_handler: Optional[str] = None,
            ) -> Self:
         """Subscribe to an event.
 
@@ -404,7 +408,11 @@ 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 not ((js_handler is None) ^ (handler is None)):
+            raise ValueError('Either handler or js_handler must be specified, but not both')
+        
         if handler:
             listener = EventListener(
                 element_id=self.id,
@@ -418,6 +426,13 @@ class Element(Visibility):
             )
             self._event_listeners[listener.id] = listener
             self.update()
+        if js_handler:
+            listener = JSEventListener(
+                type=helpers.kebab_to_camel_case(type),
+                js_handler=js_handler,
+            )
+            self._js_event_listeners.append(listener)
+            self.update()
         return self
 
     def _handle_event(self, msg: Dict) -> None:

+ 33 - 11
nicegui/event_listener.py

@@ -7,6 +7,21 @@ from fastapi import Request
 from .dataclasses import KWONLY_SLOTS
 
 
+def _type_to_dict(type: str) -> Dict[str, Any]:
+    """Convert a type string to a dictionary representation."""
+    words = type.split('.')
+    type_ = words.pop(0)
+    specials = [w for w in words if w in {'capture', 'once', 'passive'}]
+    modifiers = [w for w in words if w in {'stop', 'prevent', 'self', 'ctrl', 'shift', 'alt', 'meta'}]
+    keys = [w for w in words if w not in specials + modifiers]
+    return {
+        'type': type_,
+        'specials': specials,
+        'modifiers': modifiers,
+        'keys': keys,
+    }
+
+
 @dataclass(**KWONLY_SLOTS)
 class EventListener:
     id: str = field(init=False)
@@ -24,19 +39,26 @@ class EventListener:
 
     def to_dict(self) -> Dict[str, Any]:
         """Return a dictionary representation of the event listener."""
-        words = self.type.split('.')
-        type_ = words.pop(0)
-        specials = [w for w in words if w in {'capture', 'once', 'passive'}]
-        modifiers = [w for w in words if w in {'stop', 'prevent', 'self', 'ctrl', 'shift', 'alt', 'meta'}]
-        keys = [w for w in words if w not in specials + modifiers]
-        return {
+        _dict = _type_to_dict(self.type)
+        _dict.update({
             'listener_id': self.id,
-            'type': type_,
-            'specials': specials,
-            'modifiers': modifiers,
-            'keys': keys,
             'args': self.args,
             'throttle': self.throttle,
             'leading_events': self.leading_events,
             'trailing_events': self.trailing_events,
-        }
+        })
+        return _dict
+
+
+@dataclass(**KWONLY_SLOTS)
+class JSEventListener:
+    type: str
+    js_handler: str
+
+    def to_dict(self) -> Dict[str, Any]:
+        """Return a dictionary representation of the event listener."""
+        _dict = _type_to_dict(self.type)
+        _dict.update({
+            'js_handler': self.js_handler,
+        })
+        return _dict

+ 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]) {

+ 6 - 0
tests/test_js_event_listener.py

@@ -0,0 +1,6 @@
+if __name__ == "__main__":
+    from nicegui import ui
+
+    ui.input("press `enter` key").on("keyup.enter", js_handler="(evt) => alert(evt)")
+
+    ui.run(reload=False)