This issue describes a new event system to replace flexx.react
. Discussions welcome.
The event system for Flexx should, like the rest of Flexx, be simple and intuitive to use. The current system borrowed some ideas from reactive programming to achieve this. I was initially enthousiastic about RP, but in retrospect I don't think its the right model.
The current system
The system in flexx.react
was a fun experiment, and it certainly has some nice features, but it also has some flaws:
- This "reactive programming" model does not work well in all UI situations, in particular applications geared towards visualization. The reason is that some things are events, and sometimes you need more information than just a single value.
- The fact that object "properties" are set/get with a function call is not very Pythonic. Even though I've gotten used to the idea, I still commonly make the mistake of using assignment. This is also a hurdle for newcomers.
- Scroll events don't let themselves be mapped to signals trivially; because scroll events are always relative.
- Unnecessary events to initialize signal values (i.e. the
mouse_down
state to False)
The good features can be summarized as:
- Being able to connect to an event using a decorator at the function that is going to handle the event.
- Being able to easily connect to multiple events.
- Dynamism (e.g. connect to
'children.*.size'
).
- An anologue and API compatible implementation in JS.
The new system
I am planning/proposing a new event system that is somewhat more conventional, in that it uses events, but has the nice features of the current system.
What this new system will have (that the current system has not):
- A better name, as react leans towards RP and is confusing with reactjs.
- Events are handled in the next event loop iteration (unless there is no event loop, in which case events are probably handled synchronously).
- Event handlers can also be called manually (i.e. an event handler remains a callable).
Generating events
Events can only apply to HasEvents
objects (you cannot create events on their own, as you could with signals). You can emit an event using x.emit(name, **values)
. This causes an event object to be created, which is basically a dictionary from values
with some extra info, like owner
.
Edit: I think it makes sense to allow emitting events doing x.event_name(ob)
, which will use ob to produce and emit an event object.
Event objects
Event objects are simply dicts with some fields. For property-change-events, the fields will be 'value', 'previous_value', 'time', 'previous_time', 'owner'. I think it would be useful to create a subclass to allow easier accessing using ev.value
, but otherwise they're just dicts.
Connecting to events
You can connect to an event using:
@connect('foo')
def on_foo(*events):
...
@connect('foo', 'bar')
def update(*events):
...
The decorator is basically the same as it is now, but the function signature now accepts a series of events objects. Even for on_foo()
, the function can be called with multiple 'foo' events, if multiple events where generated during one event loop iteration. Event handlers are free to process all events or only the last one (as update()
will likely do). The decorator turns the function into a callable object (maybe even the original function?), so that you can manually call update()
, which causes events
to be empty.
Properties
A decorator @prop
replaces the @input
and can be used to create properties. When a property is changed, an event with the same name is emitted, which contains information about the new value, old value, and timings.
Similarly, there will be a @readprop
(or different name?) decorator for readonly properties (much like the current @source
.
class Foo(HasEvents):
@prop
def name(s='John'):
return str(s).capitalize()
foo = Foo()
@connect('foo.name')
def on_name_change(*events):
print('name %r changed to %r' % (events[-1].value, events[0].previous_value)
foo.name = 'X'
foo.name = 'jane'
# in the next event loop, we will see one message saying "name 'John' changed to 'Jane'".
Defining events
Event can be defined using
class Foo(HasEvents):
# Short
my_event = Event('x', 'y', # x and y are names that the event object will have
docs='docs on this event')
# I also like this, its 2 lines more, but it allows more flexibility, it allows
# emitting events using `foo.my_event(ob)`, and I prefer writing docstrings this way.
@event
def my_event(ob):
""" docs on this event """
return Event(x=ob.x, y=ob.y) # create Event object based on input passed to `emit()`.
Streams and caching
One central concept of RP is that you have streams of signals, where new signals are created from a combination of signals etc. A model like this is still possible, by emitting new events from inside an event handler. We can consider using yield
to generate new events.
Another concept of RP is caching of signal values, which can help make apps that have a long-running operation to run efficiently. I think that it will take little extra effor to create a proxy event to implement this in the new system.
Event handling order and propagation
Because multiple events can be handled by a single function, handlers are not called in the order in which they were connected. The order should be deterministic, and I think it makes sense to use the event handler name for this. Event order can be influenced by chosing the handler name, and by a keyword argument to the decorator, e.g. @connect('foo', 'bar', key='__need this first')
. A handler can not prevent the event being passed to other handlers.
The system itself does not implement any form of event propagation, though systems using it can do so. Here's an (overly simplified) example:
class RootNode(Node):
@connect('somewhere.mouse_down')
def _on_mouse_event(self, ev):
ev.handled = False
for node in reversed(children):
node.mouse_event(ev)
if ev.handled:
break
class NodeThatReactsToLMB(Node):
def mouse_event(self, ev):
if ev.button == 1:
...
ev.handled = True
class NodeThatReactsToRMB(Node):
def mouse_event(self, ev):
if ev.button == 2:
...
ev.handled = True
Loose ends
Suggestions and discussion welcome!
- A name for this subpackage. Perhaps something derived of "zanshin"? Or something general enough to represent the ideas of property and event.
Does each event need a placeholder, or can events exist by virtue of just emitting them?
- Name for readonly property.