Skip to content

Commit

Permalink
remove Events() object
Browse files Browse the repository at this point in the history
  • Loading branch information
rmorshea committed Aug 22, 2021
1 parent 3605057 commit 1651bdb
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 222 deletions.
2 changes: 1 addition & 1 deletion src/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 14 additions & 14 deletions src/idom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from . import config, html, log, web
from .core import hooks
from .core.component import Component, component
from .core.events import Events, event
from .core.events import EventHandler, event
from .core.layout import Layout
from .core.vdom import VdomDict, vdom
from .server.prefab import run
Expand All @@ -22,21 +22,21 @@


__all__ = [
"config",
"html",
"log",
"web",
"hooks",
"Component",
"component",
"Events",
"Component",
"config",
"event",
"Layout",
"VdomDict",
"vdom",
"run",
"Ref",
"html_to_vdom",
"EventHandler",
"hooks",
"hotswap",
"html_to_vdom",
"html",
"Layout",
"log",
"multiview",
"Ref",
"run",
"vdom",
"VdomDict",
"web",
]
165 changes: 38 additions & 127 deletions src/idom/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from __future__ import annotations

import asyncio
from typing import Any, Callable, Iterator, List, Mapping, Optional, Sequence
from typing import Any, Callable, List, Optional, Sequence

from anyio import create_task_group

from idom.core.proto import EventHandlerDict, EventHandlerFunc, EventHandlerType
from idom.core.proto import EventHandlerFunc, EventHandlerType


def event(
Expand Down Expand Up @@ -47,7 +47,7 @@ def my_callback(*data):

def setup(function: Callable[..., Any]) -> EventHandler:
return EventHandler(
to_event_handler_function(function),
to_event_handler_function(function, positional_args=True),
stop_propagation,
prevent_default,
)
Expand Down Expand Up @@ -84,152 +84,63 @@ def __init__(
prevent_default: bool = False,
target: Optional[str] = None,
) -> None:
self.function = function
self.function = to_event_handler_function(function, positional_args=False)
self.prevent_default = prevent_default
self.stop_propagation = stop_propagation
self.target = target

def __eq__(self, other: Any) -> bool:
for slot in self.__slots__:
if not slot.startswith("_"):
if not hasattr(other, slot):
return False
elif not getattr(other, slot) == getattr(self, slot):
return False
return True

def __repr__(self) -> str:
public_names = [name for name in self.__slots__ if not name.startswith("_")]
items = ", ".join([f"{n}={getattr(self, n)!r}" for n in public_names])
return f"{type(self).__name__}({items})"


async def _no_op(data: List[Any]) -> None:
return None


class Events(Mapping[str, EventHandler]):
"""A container for event handlers.
Assign this object to the ``"eventHandlers"`` field of an element model.
"""

__slots__ = "_handlers"

def __init__(self) -> None:
self._handlers: EventHandlerDict = {}

def on(
self,
event: str,
stop_propagation: Optional[bool] = None,
prevent_default: Optional[bool] = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""A decorator for adding an event handler.
Parameters:
event:
The camel-case name of the event, the word "on" is automatically
prepended. So passing "keyDown" would refer to the event "onKeyDown".
stop_propagation:
Block the event from propagating further up the DOM.
prevent_default:
Stops the default action associate with the event from taking place.
Returns:
A decorator which accepts an event handler function as its first argument.
The parameters of the event handler function may indicate event attributes
which should be sent back from the frontend. See :class:`EventHandler` for
more info.
Examples:
Simple "onClick" event handler:
.. code-block:: python
def clickable_element():
events = Events()
@events.on("click")
def handler(event):
# do something on a click event
...
return idom.vdom("button", "hello!", eventHandlers=events)
"""
if not event.startswith("on"):
event = "on" + event[:1].upper() + event[1:]

if event not in self._handlers:
# do this so it's possible to stop event propagation or default behavior
# without making the user have to pass a no op event handler themselves
self._handlers[event] = EventHandler(
_no_op,
stop_propagation,
prevent_default,
)

def setup(function: Callable[..., Any]) -> Callable[..., Any]:
old_handler = self._handlers[event]

if old_handler.function is _no_op:
return EventHandler(
to_event_handler_function(function),
bool(stop_propagation),
bool(prevent_default),
)

new_stop_propagation = (
old_handler.stop_propagation
if stop_propagation is None
else stop_propagation
)
new_prevent_default = (
old_handler.prevent_default
if prevent_default is None
else prevent_default
)

self._handlers[event] = merge_event_handlers(
[
old_handler,
EventHandler(
to_event_handler_function(function),
new_stop_propagation,
new_prevent_default,
),
]
)

return function

return setup

def __contains__(self, key: Any) -> bool:
return key in self._handlers

def __len__(self) -> int:
return len(self._handlers)

def __iter__(self) -> Iterator[str]:
return iter(self._handlers)

def __getitem__(self, key: str) -> EventHandler:
return self._handlers[key]

def __repr__(self) -> str: # pragma: no cover
return repr(self._handlers)


def to_event_handler_function(function: Callable[..., Any]) -> EventHandlerFunc:
def to_event_handler_function(
function: Callable[..., Any],
positional_args: bool = True,
) -> EventHandlerFunc:
"""Make a :data:`~idom.core.proto.EventHandlerFunc` from a function or coroutine
Parameters:
function:
A function or coroutine accepting a number of positional arguments.
positional_args:
Whether to pass the event parameters a positional args or as a list.
"""
if asyncio.iscoroutinefunction(function):
return lambda data: function(*data)
else:
if positional_args:
if asyncio.iscoroutinefunction(function):

async def wrapper(data: List[Any]) -> None:
await function(*data)

else:

async def wrapper(data: List[Any]) -> None:
function(*data)

return wrapper
elif not asyncio.iscoroutinefunction(function):

async def wrapper(data: List[Any]) -> None:
return function(*data)
function(data)

return wrapper
else:
return function


def merge_event_handlers(event_handlers: Sequence[EventHandlerType]) -> EventHandler:
def merge_event_handlers(
event_handlers: Sequence[EventHandlerType],
) -> EventHandlerType:
"""Merge multiple event handlers into one
Raises a ValueError if any handlers have conflicting
Expand Down
9 changes: 6 additions & 3 deletions src/idom/core/proto.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
List,
Expand Down Expand Up @@ -71,8 +70,12 @@ def __exit__(
EventHandlerDict = Dict[str, "EventHandlerType"]
"""A dict mapping between event names to their handlers"""

EventHandlerFunc = Callable[[List[Any]], Awaitable[None]]
"""A coroutine which can handle event data"""

class EventHandlerFunc(Protocol):
"""A coroutine which can handle event data"""

async def __call__(self, data: List[Any]) -> None:
...


@runtime_checkable
Expand Down
5 changes: 4 additions & 1 deletion src/idom/core/vdom.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,15 @@ def separate_attributes_and_event_handlers(
attributes: Mapping[str, Any], event_handlers: EventHandlerMapping
) -> Tuple[Dict[str, Any], EventHandlerDict]:
separated_attributes = {}
separated_event_handlers: Dict[str, List[EventHandler]] = {}
separated_event_handlers: Dict[str, List[EventHandlerType]] = {}

for k, v in event_handlers.items():
separated_event_handlers[k] = [v]

for k, v in attributes.items():

handler: EventHandlerType

if callable(v):
handler = EventHandler(to_event_handler_function(v))
elif (
Expand Down
13 changes: 9 additions & 4 deletions src/idom/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,17 @@ def Child(key):
def __init__(self) -> None:
self.target = uuid4().hex

def use(self, function: Callable[..., Any], *args, **kwargs) -> EventHandler:
def use(
self,
function: Callable[..., Any],
stop_propagation: bool = False,
prevent_default: bool = False,
) -> EventHandler:
return EventHandler(
to_event_handler_function(function),
*args,
target=self.target,
**kwargs,
stop_propagation,
prevent_default,
self.target,
)


Expand Down
5 changes: 1 addition & 4 deletions src/idom/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,14 @@ def Input(
attrs = attributes or {}
value, set_value = idom.hooks.use_state(value)

events = idom.Events()

@events.on("change")
def on_change(event: Dict[str, Any]) -> None:
value = event["value"]
set_value(value)
if not value and ignore_empty:
return
callback(value if cast is None else cast(value))

return html.input({"type": type, "value": value, **attrs}, event_handlers=events)
return html.input({**attrs, "type": type, "value": value, "onChange": on_change})


MountFunc = Callable[[ComponentConstructor], None]
Expand Down
Loading

0 comments on commit 1651bdb

Please sign in to comment.