Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a pure JavaScript event handler #2536

Merged
merged 6 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
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, Literal

from typing_extensions import Self

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__
Expand Down Expand Up @@ -85,6 +85,7 @@ def __init__(self, tag: Optional[str] = None, *, _client: Optional[Client] = Non
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')
Expand Down Expand Up @@ -187,6 +188,8 @@ def _collect_slot_dict(self) -> Dict[str, Any]:
}

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,
Expand All @@ -195,7 +198,7 @@ def _to_dict(self) -> Dict[str, Any]:
'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,
Expand Down Expand Up @@ -388,13 +391,38 @@ def tooltip(self, text: str) -> Self:
tooltip._text = text # pylint: disable=protected-access
return self

@overload
def on(self,
type: str, # pylint: disable=redefined-builtin
handler: Callable[..., Any],
args: Union[None, Sequence[str], Sequence[Optional[Sequence[str]]]] = None, *,
throttle: float = 0.0,
leading_events: bool = True,
trailing_events: bool = True,
js_handler: Literal[None] = None,
) -> Self:
...

@overload
def on(self,
type: str, # pylint: disable=redefined-builtin
handler: Literal[None] = None,
args: Literal[None] = None, *,
throttle: Literal[0] = 0,
leading_events: Literal[True] = True,
trailing_events: Literal[True] = True,
js_handler: str,
) -> Self:
...

falkoschindler marked this conversation as resolved.
Show resolved Hide resolved
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.

Expand All @@ -404,7 +432,11 @@ def on(self,
: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,
Expand All @@ -418,6 +450,13 @@ def on(self,
)
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:
Expand Down
44 changes: 33 additions & 11 deletions nicegui/event_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@
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)
Expand All @@ -24,19 +39,26 @@ def __post_init__(self) -> None:

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
32 changes: 20 additions & 12 deletions nicegui/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
6 changes: 6 additions & 0 deletions tests/test_js_event_listener.py
Original file line number Diff line number Diff line change
@@ -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)