Tutorial for flexx.event - properties and events


In [1]:
%gui asyncio
from flexx import event

Events

In flexx.event, events are represented with dictionary objects that provide information about the event (such as what button was pressed, or the new value of a property). A custom Dict class is used that inherits from dict and allows attribute access, e.g. ev.button as an alternative to ev['button'].

Reactions

Events originate from Component objects. When an event is emitted, it can be reacted upon.


In [2]:
class MyObject(event.Component):
    
    @event.reaction('!foo')
    def on_foo(self, *events):
        print('received the foo event %i times' % len(events))

ob = MyObject()

In [3]:
for i in range(3):
    ob.emit('foo', {})


received the foo event 3 times

Note how the reaction is connected using a "connection string", which (in this case) indicates we connect to the type "foo" event of the object. The connection string allows some powerful mechanics, as we will see later in this tutorial. Here, we prefixed the connection string with "!", to supress a warning that Flexx would otherwise give, because it does not know about the "foo" event.

Also note how the reaction accepts multiple events at once. This means that in situations where we only care about something being changed, we can skip "duplicate" events. In situation where each individual event needs processing, use for ev in events: ....

Reactions can also be used as normal methods:


In [4]:
ob.on_foo()


received the foo event 0 times

A reaction can also connect to multiple events:


In [5]:
class MyObject(event.Component):
        
    @event.reaction('!foo', '!bar')
    def on_foo_or_bar(self, *events):
        for ev in events:
            print('received the %s event' % ev.type)

ob = MyObject()
ob.emit('foo', {}); ob.emit('foo', {}); ob.emit('bar', {})


Out[5]:
Dict([('type', 'bar'), ('source', <Component 'MyObject2' at 0x1a8d51e49b0>)])
received the foo event
received the foo event
received the bar event

Properties

Properties represent the state of a component.


In [6]:
class MyObject(event.Component):
    
    foo = event.IntProp(2, settable=True)
        
    @event.reaction('foo')
    def on_foo(self, *events):
        print('foo changed from', events[0].old_value, 'to', events[-1].new_value)

ob = MyObject()


foo changed from 2 to 2

In [7]:
ob.set_foo(7)


Out[7]:
<Component 'MyObject3' at 0x1a8d51e44a8>
foo changed from 2 to 7

In [8]:
print(ob.foo)


7

Properties can also be set during initialization.


In [9]:
ob = MyObject(foo=12)


foo changed from 12 to 12

Properties are readonly. This may seem like a limitation at first, but it helps make apps more predictable, especially as they become larger. Properties can be mutated using actions. In the above example, a setter action was created automatically because we specified setter=True in the definition of the property.

Actions

Actions are special functions that are invoked asynchronously, i.e. when they are invoked (called) the action itself is applied in a futre iteration of the event loop. (The %gui asyncio at the top of the notebook makes sure that Flexx' event loop is running.)


In [10]:
class MyObject(event.Component):
    
    foo = event.IntProp(2, settable=True)
    
    @event.action
    def increase_foo(self):
        self._mutate_foo(self.foo + 1)
    
    @event.reaction('foo')
    def on_foo(self, *events):
        print('foo changed from', events[0].old_value, 'to', events[-1].new_value)

ob = MyObject()


foo changed from 2 to 2

In [11]:
ob.increase_foo()


Out[11]:
<Component 'MyObject5' at 0x1a8d51f3f98>
foo changed from 2 to 3

The action above mutates the foo property. Properties can only be mutated by actions. This ensures that the state of a component (and of the whole app) is consistent during the handling of reactions.

Emitters

Emitters make it easy to generate events from specific input (e.g. an event from another kind of event system) and act as a placeholder for the docs of public events.


In [12]:
class MyObject(event.Component):

    @event.emitter
    def mouse_down(self, js_event):
        ''' Event emitted when the mouse is pressed down. '''
        return dict(button=js_event['button'])
    
    @event.reaction('mouse_down')
    def on_bar(self, *events):
        for ev in events:
            print('detected mouse_down, button', ev.button)

ob = MyObject()

In [13]:
ob.mouse_down({'button': 1})
ob.mouse_down({'button': 2})


detected mouse_down, button 1
detected mouse_down, button 2

Implicit reactions

Implicit reactions make it easy to write concise code that needs to keep track of state. To create an implicit reaction, simply provide no connection strings. The reaction will now automatically track all properties that the reaction is accessing. This even works dynamically, e.g. when accessing a property on each element in a list property, the reaction will automatically "reconnect" when the list changes.


In [14]:
class MyObject(event.Component):
    
    foo = event.IntProp(2, settable=True)
      
    @event.reaction
    def on_foo(self):
        print('foo changed is now', self.foo)

ob = MyObject()


foo changed is now 2

In [15]:
ob.set_foo(99)


Out[15]:
<Component 'MyObject7' at 0x1a8d5214198>
foo changed is now 99

Labels

Labels are a feature that makes it possible to infuence the order by which event handlers are called, and provide a means to disconnect specific (groups of) handlers. The label is part of the connection string: 'foo.bar:label'.


In [16]:
class MyObject(event.Component):

    @event.reaction('!foo:bb')
    def foo_handler1(self, *events):
        print('foo B')

    @event.reaction('!foo:cc')
    def foo_handler2(self, *events):
        print('foo C')
    
    @event.reaction('!foo:aa')
    def foo_handler3(self, *events):
        print('foo A')

ob = MyObject()

In [17]:
ob.emit('foo', {})


Out[17]:
Dict([('type', 'foo'), ('source', <Component 'MyObject8' at 0x1a8d52264e0>)])
foo A
foo B
foo C

In [18]:
ob.disconnect('foo:bb')
ob.emit('foo', {})


Out[18]:
Dict([('type', 'foo'), ('source', <Component 'MyObject8' at 0x1a8d52264e0>)])
foo A
foo C

Dynamism

Dynamism is a concept that allows one to connect to events for which the source can change. It essentially allows events to be connected automatically, which greatly reduced boilerplate code. I makes it easy to connect different parts of an application in a robust way.


In [19]:
class Root(event.Component):

    children = event.TupleProp([], settable=True)
    
    @event.reaction('children', 'children*.count')
    def update_total_count(self, *events):
        total_count = sum([child.count for child in self.children])
        print('total count is', total_count)

class Sub(event.Component):    
    count = event.IntProp(0, settable=True)

root = Root()
sub1, sub2, sub3 = Sub(count=1), Sub(count=2), Sub(count=3)
root.set_children([sub1, sub2, sub3])


Out[19]:
<Component 'Root9' at 0x1a8d5233470>
total count is 6

Updating the count property on any of its children will invoke the callback:


In [20]:
sub1.set_count(100)


Out[20]:
<Component 'Sub10' at 0x1a8d5233320>
total count is 105

We also connected to the children property, so that the handler is also invoked when the children are added/removed:


In [21]:
root.set_children([sub2, sub3])


Out[21]:
<Component 'Root9' at 0x1a8d5233470>
total count is 5

Naturally, when the count on new children changes ...


In [22]:
sub4 = Sub()
root.set_children([sub3, sub4])


Out[22]:
<Component 'Root9' at 0x1a8d5233470>
total count is 3

In [23]:
sub4.set_count(10)


Out[23]:
<Component 'Sub13' at 0x1a8d5233ba8>
total count is 13

In [24]:
sub1.set_count(1000)  # no update, sub1 is not part of root's children


Out[24]:
<Component 'Sub10' at 0x1a8d5233320>

Note that the above example is a great candidate for using an implicit reaction. Try it!


In [ ]: