3. Events

Lona nodes can produce events like click events, so views can react to button clicks for example. To receive input events you have to tell Lona which node should produce which event by setting Node.events, which is a list of event types. Supported events are lona.html.CLICK, lona.html.CHANGE, lona.html.FOCUS and lona.html.BLUR.

Note

The browser has to be able to fire the set event type, for your event to work. For example a div element can not fire a CHANGE event.

Input events get handled in a chain of hooks. Every hook is required to return the given input event, to pass it down the chain, or return None to mark the event as handled.

The first member of the chain is View.handle_input_event_root(). If the event got returned, Lona searches for the node instance that issued the event and passes the event into its Node.handle_input_event() which tries multiple node internal callbacks like Node.handle_click or Node.handle_change. As long as the event gets returned, Lona bubbles the event up the HTML tree. If the event got returned by the outer most node in the tree, Lona checks if View.handle_request() awaits an input event using View.await_[input_event|click|change](). If not, View.handle_input_event() gets called as last member of the chain.

Input events contain a reference to the node that issued the event in input_event.node.

Input event handler can return redirect responses, even after View.handle_request() stopped.

When a callback-based input event handler makes changes to the currently shown HTML, Lona calls View.show() implicitly, to send the changes to the browser.

This example shows some simple callback-based input event handlers.

from lona_picocss.html import InlineButton, CLICK, HTML, H1, P
from lona_picocss import install_picocss

from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


@app.route('/')
class Index(View):
    def handle_click(self, input_event):
        input_event.node.set_text('Success!')

    def handle_request(self, request):
        return HTML(
            H1('Click On Something'),

            # we tell Lona with `events=[CLICK]` that this node can
            # produces click events, and with `handle_click` which code
            # should be called when a click event is issued
            P('Click Me', events=[CLICK], handle_click=self.handle_click),

            # some nodes from the standard library come pre-configured with
            # events, set in `Node.EVENTS`
            InlineButton('Or Me', handle_click=self.handle_click),
        )


if __name__ == '__main__':
    app.run()

HTML Nodes

Like mentioned before, Lona nodes can handle input events on their own, without bothering the view and application code. That is useful to encapsulate functionality and behavior, to make it reusable.

The Lona standard library defines nodes like lona.html.TextInput, which handle their issued CHANGE events on their own to update their inner state, available in lona.html.TextInput.value. To receive events from these nodes set their bubble_up property to True.

When a node handles an input event and updates its HTML, View.show() gets called implicitly, to send the updates to the browser.

This example implements a simple node, that contains a counter and two buttons to increment or decrement it. All input events get handled internally, without the rest of the HTML or view knowing.

from lona_picocss.html import InlineButton, Label, Span, Icon, HTML, Div, H1
from lona_picocss import install_picocss

from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


class Counter(Div):
    def __init__(self, value=0):
        super().__init__()

        self.nodes = [
            Label(
                InlineButton(Icon('minus-circle'), handle_click=self.decrease),
                ' ',
                Span(),
                ' ',
                InlineButton(Icon('plus-circle'), handle_click=self.increase),
            ),
        ]

        self.set_value(value)

    def set_value(self, value):
        with self.lock:
            self.query_selector('span').set_text(f'{value:03}')

    def get_value(self):
        with self.lock:
            return int(self.query_selector('span').get_text())

    def decrease(self, input_event):
        with self.lock:
            current_value = self.get_value()
            self.set_value(current_value - 1)

    def increase(self, input_event):
        with self.lock:
            current_value = self.get_value()
            self.set_value(current_value + 1)


@app.route('/')
class Index(View):
    def handle_request(self, request):
        return HTML(
            H1('Counter'),
            Counter(10),
            Counter(20),
            Counter(30),
        )


if __name__ == '__main__':
    app.run()

Async API

Lona events can also be awaited, instead of being handled in callbacks. This makes for a more clear control flow, and is useful when building wizards, or long running views that need confirmation at some point.

Note

When using View.await_[input_event|click|change]() one Python thread is blocked, until a matching event was issued, or the view stops.

This example implements a simple wizard, that uses View.await_input_event() to await input of two numbers, to then display their sum.

from lona_picocss.html import InlineButton, NumberInput, HTML, H1, P
from lona_picocss import install_picocss

from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


@app.route('/')
class Index(View):
    def handle_request(self, request):
        first_number = 0
        second_number = 0
        number_input = NumberInput()

        # first number
        number_input.value = first_number

        html = HTML(
            H1('Enter A Number'),
            number_input,
            InlineButton('Enter'),
        )

        input_event = self.await_click(html=html)
        first_number = number_input.value

        # second number
        number_input.value = second_number

        html = HTML(
            H1('Enter A Second Number'),
            number_input,
            InlineButton('Enter'),
        )

        input_event = self.await_click(html=html)
        second_number = number_input.value

        # result
        return HTML(
            H1('Result'),
            P(f'{first_number} + {second_number} = {first_number+second_number}')
        )


if __name__ == '__main__':
    app.run()

More information: Input Events