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.

https://docs.google.com/drawings/d/e/2PACX-1vSHp4iha6CTgjsQ52x77gn0hqQP4lZD-bcaVeCfRKhyMVtaLeuX5wpbgUGaIE0Sce_kBT9mqrfEgQxB/pub?w=503

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))
open in new tab

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()))
open in new tab

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.