12. Widgets

Lona is all about managing state and business logic on the backend, using a stateless frontend. This approach creates a certain delay because for every reaction to an event, a round-trip to the server is necessary. This is not a problem for simple tasks, like handling a simple button press, but becomes an issue when dealing with time-critical operations like animations.

To perform operations on the client, Lona nodes can define widgets, which are JavaScript classes that get initialized when the node get rendered in the browser. This widget has a reference to the node and its state and has full control over it.

The server-side node can share state with the browser-side widget, by setting Node.widget_data. This property behaves like a Python dictionary, and is thread-safe, using the lock of its node.

Note

Widget data can only be changed on the server. Changes to Widget.data on the browser-side are not supported.

To issue a change from the browser-side, fire a custom event, and apply your change on the server.

This example shows a widget that renders a rotation animation on the browser-side. The server-side does not render the animation itself, but controls its parameters using Widget.data.

from lona_picocss.html import (
    NumberInput,
    TextInput,
    Switch,
    Label,
    HTML,
    Grid,
    Div,
    H1,
    Br,
)
from lona_picocss import install_picocss

from lona.static_files import Script
from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


class RotatingContainer(Div):

    # this tells the frontend the name of the widget to use
    WIDGET = 'RotatingContainer'

    # this tells the frontend that this file has to be loaded
    # before the node can be rendered
    STATIC_FILES = [
        Script(
            name='rotating-container',
            path='rotating-container.js',
        ),
    ]

    STYLE = {
        'display': 'inline-block',
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # setting up widget data
        # will be available as `this.data` in the widget
        self.widget_data = {
            'animation_running': True,
            'animation_speed': 2,
        }

    def set_animation_running(self, running):
        self.widget_data['animation_running'] = running

    def set_animation_speed(self, speed):
        self.widget_data['animation_speed'] = speed


@app.route('/')
class Index(View):

    # input event handling
    def set_animation_running(self, input_event):
        self.rotating_container.set_animation_running(input_event.node.value)

    def set_animation_speed(self, input_event):
        self.rotating_container.set_animation_speed(input_event.node.value)

    def set_animation_text(self, input_event):
        self.rotating_container.set_text(input_event.node.value)

    # request handling
    def handle_request(self, request):
        initial_text = 'Weeeeee!'
        self.rotating_container = RotatingContainer(initial_text)

        return HTML(
            H1('Rotating Container'),

            Grid(
                Div(
                    self.rotating_container,
                    Br(),
                    Br(),
                    Br(),
                    Br(),
                    Br(),
                ),
                Div(
                    Label(
                        Switch(
                            value=True,
                            handle_change=self.set_animation_running,
                        ),
                        'Animation',
                    ),
                    Label(
                        'Animation Speed',
                        NumberInput(
                            value=2,
                            handle_change=self.set_animation_speed,
                        ),
                    ),
                    Label(
                        'Text',
                        TextInput(
                            value=initial_text,
                            handle_change=self.set_animation_text,
                        ),
                    ),
                )
            ),
        )


if __name__ == '__main__':
    app.run()
// rotating-container.js

class RotatingContainer {
    constructor(lona_window) {
        this.lona_window = lona_window;
    }

    animate(time) {
        const container = this.root_node;

        this.angle = this.angle += this.data['animation_speed'];

        if(this.angle >= 360) {
            this.angle = 0;
        }

        container.style['transform'] = `rotate(${this.angle}deg)`;

        if(this.data['animation_running']) {
            requestAnimationFrame(time => {
                this.animate(time);
            });
        }
    }

    // gets called on initial setup
    setup() {
        this.angle = 0;

        if(this.data['animation_running']) {
            requestAnimationFrame(time => {
                this.animate(time);
            });
        }
    }

    // gets called every time the data gets updated
    data_updated() {
        if(this.data['animation_running']) {
            requestAnimationFrame(time => {
                this.animate(time);
            });
        }
    }

    // gets called when the widget gets destroyed
    deconstruct() {

    }
}


// register widget class with the same name we used
// in `RotatingContainer.WIDGET`
Lona.register_widget_class('RotatingContainer', RotatingContainer);