The event system¶
The event system consists of components, properties, events, and reactions. They let different components of an application react to each-other and to user input.
In short:
- The
components
(e.g. widgets) form the units of which an application is build. - Each component has
properties
to reflect the state of the component. - Properties can only be mutated by
actions
. Calling (i.e. invoking) an action will not apply the action at once; actions are processed in batches. - When properties are modified (i.e. the state is changed),
corresponding
reactions
will be invoked. The reactions are processed when all pending actions are done. This means that during processing reactions, the state never changes, which is a great thing to rely on! - Reactions can also react to events generated by
emitters
, such as mouse events. - The
event loop
object is responsible for scheduling actions and reactions. In Python it integrates with Python’s own asyncio loop. In JavaScript it makes use of the JavaScript scheduling mechanics.
The asynchronous nature of actions combined with the fact that the state does not change during processing reactions, makes it easy to reason about cause and effect. The information flows in one direction. This concept was gratefully taken from modern frameworks such as React/Flux and Veux.
One might argue that the information flow is still circular, because there is an arrow going from reactions to actions. This is true, but note that actions invoked from reactions are not directly executed; they are pended and will be executed only after all reactions are done.
Relation to other parts of Flexx¶
This event system and its Component
class
form the basis for app.PyComponent
,
app.JsComponent
and the UI system
in flexx.ui
. It can be used in both Python and JavaScript and works exactly
the same in both languages.
Other than that, this is a generic event system that could drive any system that is based on asyncio.
Event object¶
An event is something that has occurred at a certain moment in time,
such as the mouse being pressed down or a property changing its value.
In Flexx, events are represented with dictionary objects that
provide information about the event (such as what button was pressed,
or the old and new value of a property). A custom Dict
class is used that inherits from dict
but allows attribute access,
e.g. ev.button
as an alternative to ev['button']
.
Each event object has at least two attributes: source
,
a reference to the component object emitting the event, and type
, a string
indicating the type of the event.
The Component class¶
The Component
class provides a base
class for objects that have properties, actions, reactions and emitters.
You can create your own components like so:
class MyObject(flx.Component):
... # attributes/properties/actions/reactions/emitters go here
def init(self):
super().init()
...
It is common to implement the init()
method of the component class. It gets
automatically called by the component, at a moment when all properties have
been initialized, but no events have been emitted yet. This is a good time
to further initialize the component, and/or to instantiate sub components.
One rarely needs to implement the __init__()
method.
When the init()
is called, the component is the currently “active”
component, which can be used to e.g. describe a hierarchy of objects, as is
done with widgets. It also implies that mutations are allowed and that actions
on the component itself have a direct effect (invoking actions of other
components is still asynchronous though).
Let’s look at a real working widget example and break it down. It contains a property, an action, and a few reactions:
from flexx import flx
class Example(flx.Widget):
counter = flx.IntProp(3, settable=True)
def init(self):
super().init()
with flx.HBox():
self.but1 = flx.Button(text='reset')
self.but2 = flx.Button(text='increase')
self.label = flx.Label(text='', flex=1) # take all remaining space
@flx.action
def increase(self):
self._mutate_counter(self.counter + 1)
@flx.reaction('but1.pointer_click')
def but1_clicked(self, *events):
self.set_counter(0)
@flx.reaction('but2.pointer_click')
def but2_clicked(self, *events):
self.increase(0)
@flx.reaction
def update_label(self, *events):
self.label.set_text('count is ' + str(self.counter))
We will now take a closer look at properties and actions. Reactions are so cool that they’ve got their own chapter :)
Properties represent state¶
In the widget example above, we can see an int property. There are a handful
of different property types
. For example:
class MyObject(flx.Component):
foo = flx.AnyProp(8, settable=True, doc='can have any value')
bar = flx.IntProp()
Properties accept one positional arguments to set the default value. If not
given, a sensible default value is used that depends on the type of property.
Docs can be added using the doc
argument. Note that properties are
readonly: they can can only be mutated by actions. The foo
property
(as well as the counter
property) is marked as settable, which will
automatically create a set_foo()
action.
Property values can be initialized when a component is created (also non-settable properties):
c = MyComponent(foo=42)
One can also set the initial value of a property to a function object. This creates an auto-reaction that sets the property, and makes it possible to hook things up in a very concise manner. In the example below, the label text will be automatically updated when the username property changes:
flx.Label(flex=1, text=lambda: 'count is ' + str(self.counter))
An event is emitted every time that a property changes. This event has attributes
old_value
and new_value
(except for in-place array mutations, as
explained below). At initialization, a component sends out an event for each property,
in which old_value
and new_value
will be the same.
Attributes¶
Component classes can also have Attributes
,
which are read-only (usually static) non-observable values (e.g. JsComponent.id
).
Local properties¶
Regular methods of a JsComponent
are only available in JavaScript. On the
other hand, all properties are available on the proxy object as well. This may
not always be useful. It is possible to create properties that are local
to JavaScript (or to Python in a PyComponent
) using
LocalProperty
. An alternative may be to use
Attribute
; these are also local to JavaScript/Python.
Actions can mutate properties¶
In the widget example above, we can see the definition of the increase()
action.
Actions
are needed because they are the
only place where properties can be mutated.
class Example(flx.Widget):
counter = flx.IntProp(3, settable=True)
...
@flx.action
def increase(self):
self._mutate_counter(self.counter + 1)
You may wonder why the example’s reaction does not simply do self.set_counter(self.counter + 1)
.
The reason is that actions are asynchronous; invoking an action does not perform
it directly. Therefore invoking set_counter()
twice will simply apply the
last value. Note though, that when an action is called from another action, it
is performed directly.
Actions can have any number of (positional) arguments, and always
returns the component itself, which allows chaining action invocations,
e.g. t.scale(3).translate(3, 4)
.
Mutations are done via the _mutate
method,
or by the auto-generated _mutate_xx()
methods.
Mutations can only be done from an action. Trying
to do so otherwise will result in an error. This may seem limiting at first,
but it greatly helps keeping it easy to reason about information flowing
through your application, even as it scales.
Mutations to array-like properties¶
The above shows the simple and most common use of mutations. For
list properties
, mutations can also be done in-place:
from flexx import flx
class Example(flx.Widget):
items = flx.ListProp(settable=True)
def init(self):
super().init()
with flx.HBox():
self.but1 = flx.Button(text='reset')
self.but2 = flx.Button(text='add')
flx.Label(flex=1, wrap=2, text=lambda: repr(self.items))
@flx.action
def add_item(self, item):
self._mutate_items([item], 'insert', len(self.items))
@flx.reaction('but1.pointer_click')
def but1_clicked(self, *events):
self.set_items([])
@flx.reaction('but2.pointer_click')
def but2_clicked(self, *events):
self.add_item(int(time()))
This allows more fine-grained control over state updates, which can also be handled by reactions in much more efficient ways. The types of mutations are ‘set’ (the default), ‘insert’, ‘replace’, and ‘remove’. In the latter, the provided value is the number of elements to remove. For the others it must be a list of elements to set/insert/replace at the specified index.
Emitters create events¶
Emitters
make it easy to generate events.
Similar to actions, they are created with a decorator.
# Somewhere in the Flexx codebase:
class Widget(JsComponent):
...
@flx.emitter
def key_down(self, e):
""" Event emitted when a key is pressed down while this
widget has focus.
...
"""
return self._create_key_event(e)
Emitters can have any number of arguments and should return a dictionary, which will get emitted as an event, with the event type matching the name of the emitter.
Note that strictly speaking emitters are not necessary as
Component.emit()
can be used to generate an event. However, they provide a mechanism to
generate an event based on certain input data, and also document the
events that a component may emit.