22Events
33"""
44
5+ from __future__ import annotations
6+
57import asyncio
6- from typing import (
7- Any ,
8- Callable ,
9- Coroutine ,
10- Dict ,
11- Iterator ,
12- List ,
13- Mapping ,
14- Optional ,
15- Union ,
16- )
17- from uuid import uuid4
8+ from typing import Any , Callable , Iterator , List , Mapping , Optional , Sequence
189
1910from anyio import create_task_group
2011
21-
22- EventsMapping = Union [Dict [str , Union ["Callable[..., Any]" , "EventHandler" ]], "Events" ]
12+ from idom .core .proto import EventHandlerDict , EventHandlerFunc , EventHandlerType
2313
2414
2515def event (
26- function : Optional [Callable [..., Any ]] = None ,
2716 stop_propagation : bool = False ,
2817 prevent_default : bool = False ,
29- ) -> Union [ "EventHandler" , Callable [[Callable [..., Any ]], " EventHandler" ] ]:
30- """Create an event handler function with extra functionality .
18+ ) -> Callable [[Callable [..., Any ]], EventHandler ]:
19+ """A decorator for constructing an :class:`EventHandler` .
3120
3221 While you're always free to add callbacks by assigning them to an element's attributes
3322
@@ -39,23 +28,78 @@ def event(
3928 from taking place, or stoping the event from propagating up the DOM. This decorator
4029 allows you to add that functionality to your callbacks.
4130
31+ .. code-block:: python
32+
33+ @event(stop_propagation=True, prevent_default=True)
34+ def my_callback(*data):
35+ ...
36+
37+ element = idom.html.button({"onClick": my_callback})
38+
4239 Parameters:
4340 function:
44- A callback responsible for handling the event.
41+ A function or coroutine responsible for handling the event.
4542 stop_propagation:
4643 Block the event from propagating further up the DOM.
4744 prevent_default:
4845 Stops the default actional associate with the event from taking place.
4946 """
50- handler = EventHandler (stop_propagation , prevent_default )
51- if function is not None :
52- handler .add (function )
53- return handler
54- else :
55- return handler .add
5647
48+ def setup (function : Callable [..., Any ]) -> EventHandler :
49+ return EventHandler (
50+ to_event_handler_function (function ),
51+ stop_propagation ,
52+ prevent_default ,
53+ )
54+
55+ return setup
56+
57+
58+ class EventHandler :
59+ """Turn a function or coroutine into an event handler
60+
61+ Parameters:
62+ function:
63+ The function or coroutine which handles the event.
64+ stop_propagation:
65+ Block the event from propagating further up the DOM.
66+ prevent_default:
67+ Stops the default action associate with the event from taking place.
68+ target:
69+ A unique identifier for this event handler (auto-generated by default)
70+ """
5771
58- class Events (Mapping [str , "EventHandler" ]):
72+ __slots__ = (
73+ "__weakref__" ,
74+ "function" ,
75+ "prevent_default" ,
76+ "stop_propagation" ,
77+ "target" ,
78+ )
79+
80+ def __init__ (
81+ self ,
82+ function : EventHandlerFunc ,
83+ stop_propagation : bool = False ,
84+ prevent_default : bool = False ,
85+ target : Optional [str ] = None ,
86+ ) -> None :
87+ self .function = function
88+ self .prevent_default = prevent_default
89+ self .stop_propagation = stop_propagation
90+ self .target = target
91+
92+ def __repr__ (self ) -> str :
93+ public_names = [name for name in self .__slots__ if not name .startswith ("_" )]
94+ items = ", " .join ([f"{ n } ={ getattr (self , n )!r} " for n in public_names ])
95+ return f"{ type (self ).__name__ } ({ items } )"
96+
97+
98+ async def _no_op (data : List [Any ]) -> None :
99+ return None
100+
101+
102+ class Events (Mapping [str , EventHandler ]):
59103 """A container for event handlers.
60104
61105 Assign this object to the ``"eventHandlers"`` field of an element model.
@@ -64,10 +108,13 @@ class Events(Mapping[str, "EventHandler"]):
64108 __slots__ = "_handlers"
65109
66110 def __init__ (self ) -> None :
67- self ._handlers : Dict [ str , EventHandler ] = {}
111+ self ._handlers : EventHandlerDict = {}
68112
69113 def on (
70- self , event : str , stop_propagation : bool = False , prevent_default : bool = False
114+ self ,
115+ event : str ,
116+ stop_propagation : Optional [bool ] = None ,
117+ prevent_default : Optional [bool ] = None ,
71118 ) -> Callable [[Callable [..., Any ]], Callable [..., Any ]]:
72119 """A decorator for adding an event handler.
73120
@@ -105,13 +152,46 @@ def handler(event):
105152 event = "on" + event [:1 ].upper () + event [1 :]
106153
107154 if event not in self ._handlers :
108- handler = EventHandler (stop_propagation , prevent_default )
109- self ._handlers [event ] = handler
110- else :
111- handler = self ._handlers [event ]
155+ # do this so it's possible to stop event propagation or default behavior
156+ # without making the user have to pass a no op event handler themselves
157+ self ._handlers [event ] = EventHandler (
158+ _no_op ,
159+ stop_propagation ,
160+ prevent_default ,
161+ )
112162
113163 def setup (function : Callable [..., Any ]) -> Callable [..., Any ]:
114- handler .add (function )
164+ old_handler = self ._handlers [event ]
165+
166+ if old_handler .function is _no_op :
167+ return EventHandler (
168+ to_event_handler_function (function ),
169+ bool (stop_propagation ),
170+ bool (prevent_default ),
171+ )
172+
173+ new_stop_propagation = (
174+ old_handler .stop_propagation
175+ if stop_propagation is None
176+ else stop_propagation
177+ )
178+ new_prevent_default = (
179+ old_handler .prevent_default
180+ if prevent_default is None
181+ else prevent_default
182+ )
183+
184+ self ._handlers [event ] = merge_event_handlers (
185+ [
186+ old_handler ,
187+ EventHandler (
188+ to_event_handler_function (function ),
189+ new_stop_propagation ,
190+ new_prevent_default ,
191+ ),
192+ ]
193+ )
194+
115195 return function
116196
117197 return setup
@@ -125,91 +205,79 @@ def __len__(self) -> int:
125205 def __iter__ (self ) -> Iterator [str ]:
126206 return iter (self ._handlers )
127207
128- def __getitem__ (self , key : str ) -> " EventHandler" :
208+ def __getitem__ (self , key : str ) -> EventHandler :
129209 return self ._handlers [key ]
130210
131211 def __repr__ (self ) -> str : # pragma: no cover
132212 return repr (self ._handlers )
133213
134214
135- class EventHandler :
136- """An object which defines an event handler.
137-
138- The event handler object acts like a coroutine when called.
215+ def to_event_handler_function (function : Callable [..., Any ]) -> EventHandlerFunc :
216+ """Make a :data:`~idom.core.proto.EventHandlerFunc` from a function or coroutine
139217
140218 Parameters:
141- stop_propagation:
142- Block the event from propagating further up the DOM.
143- prevent_default:
144- Stops the default action associate with the event from taking place.
219+ function:
220+ A function or coroutine accepting a number of positional arguments.
145221 """
222+ if asyncio .iscoroutinefunction (function ):
223+ return lambda data : function (* data )
224+ else :
146225
147- __slots__ = (
148- "__weakref__" ,
149- "_coro_handlers" ,
150- "_func_handlers" ,
151- "prevent_default" ,
152- "stop_propagation" ,
153- "target" ,
154- )
226+ async def wrapper (data : List [Any ]) -> None :
227+ return function (* data )
155228
156- def __init__ (
157- self ,
158- stop_propagation : bool = False ,
159- prevent_default : bool = False ,
160- ) -> None :
161- self ._coro_handlers : List [Callable [..., Coroutine [Any , Any , Any ]]] = []
162- self ._func_handlers : List [Callable [..., Any ]] = []
163- self .prevent_default = prevent_default
164- self .stop_propagation = stop_propagation
165- self .target = uuid4 ().hex
229+ return wrapper
166230
167- def add (self , function : Callable [..., Any ]) -> "EventHandler" :
168- """Add a callback function or coroutine to the event handler.
169231
170- Parameters:
171- function:
172- The event handler function accepting parameters sent by the client.
173- Typically this is a single ``event`` parameter that is a dictionary.
174- """
175- if asyncio .iscoroutinefunction (function ):
176- self ._coro_handlers .append (function )
177- else :
178- self ._func_handlers .append (function )
179- return self
232+ def merge_event_handlers (event_handlers : Sequence [EventHandlerType ]) -> EventHandler :
233+ """Merge multiple event handlers into one
180234
181- def remove (self , function : Callable [..., Any ]) -> None :
182- """Remove the given function or coroutine from this event handler.
235+ Raises a ValueError if any handlers have conflicting
236+ :attr:`~idom.core.proto.EventHandlerType.stop_propagation` or
237+ :attr:`~idom.core.proto.EventHandlerType.prevent_default` attributes.
238+ """
239+ if not event_handlers :
240+ raise ValueError ("No event handlers to merge" )
241+ elif len (event_handlers ) == 1 :
242+ return event_handlers [0 ]
243+
244+ first_handler = event_handlers [0 ]
245+
246+ stop_propagation = first_handler .stop_propagation
247+ prevent_default = first_handler .prevent_default
248+ target = first_handler .target
249+
250+ for handler in event_handlers :
251+ if (
252+ handler .stop_propagation != stop_propagation
253+ or handler .prevent_default != prevent_default
254+ or handler .target != target
255+ ):
256+ raise ValueError (
257+ "Cannot merge handlers - "
258+ "'stop_propagation', 'prevent_default' or 'target' mistmatch."
259+ )
260+
261+ return EventHandler (
262+ merge_event_handler_funcs ([h .function for h in event_handlers ]),
263+ stop_propagation ,
264+ prevent_default ,
265+ target ,
266+ )
183267
184- Raises:
185- ValueError: if not found
186- """
187- if asyncio .iscoroutinefunction (function ):
188- self ._coro_handlers .remove (function )
189- else :
190- self ._func_handlers .remove (function )
191-
192- def clear (self ) -> None :
193- """Remove all functions and coroutines from this event handler"""
194- self ._coro_handlers .clear ()
195- self ._func_handlers .clear ()
196-
197- async def __call__ (self , data : List [Any ]) -> Any :
198- """Trigger all callbacks in the event handler."""
199- if self ._coro_handlers :
200- async with create_task_group () as group :
201- for handler in self ._coro_handlers :
202- group .start_soon (handler , * data )
203- for handler in self ._func_handlers :
204- handler (* data )
205-
206- def __contains__ (self , function : Any ) -> bool :
207- if asyncio .iscoroutinefunction (function ):
208- return function in self ._coro_handlers
209- else :
210- return function in self ._func_handlers
211268
212- def __repr__ (self ) -> str :
213- public_names = [name for name in self .__slots__ if not name .startswith ("_" )]
214- items = ", " .join ([f"{ n } ={ getattr (self , n )!r} " for n in public_names ])
215- return f"{ type (self ).__name__ } ({ items } )"
269+ def merge_event_handler_funcs (
270+ functions : Sequence [EventHandlerFunc ],
271+ ) -> EventHandlerFunc :
272+ """Make one event handler function from many"""
273+ if not functions :
274+ raise ValueError ("No handler functions to merge" )
275+ elif len (functions ) == 1 :
276+ return functions [0 ]
277+
278+ async def await_all_event_handlers (data : List [Any ]) -> None :
279+ async with create_task_group () as group :
280+ for func in functions :
281+ group .start_soon (func , data )
282+
283+ return await_all_event_handlers
0 commit comments