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.
components(e.g. widgets) form the units of which an application is build.
- Each component has
propertiesto 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),
reactionswill 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 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
form the basis for
app.JsComponent and the UI system
the same in both languages.
Other than that, this is a generic event system that could drive any system that is based on asyncio.
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
class is used that inherits from
dict but allows attribute access,
ev.button as an alternative to
Each event object has at least two attributes:
a reference to the component object emitting the event, and
type, a string
indicating the type of the event.
The Component class¶
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() is called, the component is the currently “active”
component, which can be used to e.g. descrive 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
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
(as well as the
counter property) is marked as settable, which will
automatically create a
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
new_value (except for in-place array mutations, as
explained below). At initialization, a component sends out an event for each property,
new_value will be the same.
Component classes can also have
which are read-only (usually static) non-observable values (e.g.
Regular methods of a
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
LocalProperty. An alternative may be to use
Actions can mutate properties¶
In the widget example above, we can see the definition of the
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 chainging action invokations,
Mutations are done via the
or by the auto-generated
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 stricly speaking emitters are not necessary as
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.