HTML API

Nodes

In Lona every HTML element is represented as a python object, derived from lona.html.Node.

from lona.html import Div, H1

div = Div(
    H1('Hello World'),
    Div('Foo Bar Baz'),
)
<div data-lona-node-id="173908150026599">
  <h1 data-lona-node-id="173908149497883">
        Hello World
  </h1>
  <div data-lona-node-id="173908149853937">
        Foo Bar Baz
  </div>
</div>

Sub Nodes

Internally nodes and subnodes behave like python lists and implement all common list interfaces and methods.

from lona.html import Div

>>> div = Div(Div('foo'), Div('bar'))
>>> div
<div data-lona-node-id="65286584612039">
    <div data-lona-node-id="65286584010206">
        foo
    </div>
    <div data-lona-node-id="65286584292299">
        bar
    </div>
</div>

>>> div.nodes
<NodeList([<div data-lona-node-id="65286584010206">
    foo
</div>, <div data-lona-node-id="65286584292299">
    bar
</div>]))>

>>> div[0]  # or div.nodes[0]
<div data-lona-node-id="65286584010206">
    foo
</div>

Sub nodes can be defined as args, as a list or by using the nodes keyword.

Note

The list syntax was added in 1.4.1

from lona.html import Div

Div(Div('foo'), Div('bar'))

Div([
    Div('foo'),
    Div('bar'),
])

Div(nodes=[
    Div('foo'),
    Div('bar'),
])

Selectors

To find nodes in big node trees Lona provides a query selector API similar to Javascript.

AbstractNode.query_selector() returnes the first first matching node in the node tree. AbstractNode.query_selector_all() returnes a list of all matching nodes.

from lona.html import HTML

html = HTML("""
    <div>
        <div id="foo">
            Foo
            <div id="bar">Bar</div>
        </div>
    </div>
""")

foo = html.query_selector('#foo')
bar = foo.query_selector('#bar')

Syntax

Example Description
"div" Selects all nodes with the tag name "div"
"div#foo" Selects all nodes with the tag name "div" and the id "foo"
"div#foo#bar" Selects all nodes with the tag name "div" and the ids "foo" and "bar"
"#foo" Selects all nodes with the id "foo"
"#foo#bar" Selects all nodes with the ids "foo" and "bar"
".foo" Selects all nodes with the class "foo"
".foo.bar" Selects all nodes with the classes "foo" and "bar"
"#foo,#bar" Selects all nodes with the classes "foo" or "bar"
"[foo=bar]" Selects all nodes with the attribute "foo" set to "bar"

Closest(selector)

Note

Added in 1.4.1

AbstractNode.closest() returns the closest parent node that matches the given selector.

from lona.html import Table, Tr, Td, A, CLICK

link = A('click me', href='#', events=[CLICK]

table = Table(
    Tr(
        Td('Foo'),
        Td('bar'),
        Td(a),
    )
)

tr = a.closest('tr')

Using HTML Strings

Note

Added in 1.5: Support for high level nodes, the keyword use_high_level_nodes

To initialize an HTML tree you can use lona.html.HTML. When lona.html.HTML gets a HTML string passed in that does not start with \, the string gets parsed and converted into lona.html.Node objects. The resulting tree behaves like a normal Lona HTML tree.

lona.html.HTML uses high level nodes from the standard library like lona.html.TextInput which implement high level methods and properties. To disable this and parse HTML into blank nodes you can set use_high_level_nodes=False.

from lona.html import HTML

>>> html = HTML('<h1>Hello World</h1><p>Lorem Ipsum</p>')
>>> html
<h1 data-lona-node-id="66513259465059">
    Hello World
</h1>
<p data-lona-node-id="66513260451573">
    Lorem Ipsum
</p>

Attributes

from lona.html import Div

div = Div(foo='bar')
<div data-lona-node-id="174102029578147" id="bar"></div>
>>> div.attributes['foo']
'bar'
>>> div.attributes['foo'] = 'foo'
>>> div.attributes['foo']
'foo'

ID / Class List

from lona.html import Div

div = Div(_id='foo bar baz')
div = Div(_id=['foo', 'bar' 'baz'])
<div data-lona-node-id="174102029578147" id="foo bar baz"></div>

Style

from lona.html import Div

div = Div(_style={'color': 'red'})
div.style['background-color'] = 'blue'
<div data-lona-node-id="182311158684648" style="color: red; background-color: blue"></div>

Helper Methods

Node.hide()

Sets Node.style['display'] to 'none'.

Node.show()

Deletes Node.style['display'] if is set.

Node.set_text(string)

Resets Node.nodes to the given string.

Node.get_text()

Returns a concatenated string of all sub nodes, without HTML syntax.

Adding Custom Nodes

To add a new node class you have to inherit from lona.html.Node.

from lona.html import Node, CLICK


class BootstrapButton(Node):
    TAG_NAME = 'button'
    SELF_CLOSING_TAG = False
    ID_LIST = []
    CLASS_LIST = ['btn', 'btn-primary']
    STYLE = {}
    ATTRIBUTES = {}
    EVENTS = [CLICK]

Extending Nodes

from lona.html import Button


class BootstrapButton(Button):
    CLASS_LIST = ['btn', 'btn-primary']

Locking

Lona is multithreaded and up to three views can be involved at the same time to run a view (more information: Resource management)

To avoid race conditions between threads you can use lona.html.AbstractNode.lock.

The followwing view implements a counter that gets incremented once a second in handle_request(). When the decrement button is clicked, the event gets handled in handle_input_event(). When incrementing and decrementing, the view reads the current value from the HTML tree, changes it and writes back. To avoid race conditions, both callbacks lock the HTML tree, before reading and release it after writing.

from lona.html import HTML, Div, H1, Button
from lona import LonaView


class MyLonaView(LonaView):
    def handle_request(self, request):
        self.counter = Div('0')
        self.button = Button('Decrement Counter')

        self.html = HTML(
            H1('Counter'),
            self.counter,
            self.button,
        )

        while True:

            # increment counter
            with self.html.lock:
                self.counter.set_text(
                    str(int(self.counter.get_text()) + 1)
                )

            # show html
            self.show(self.html)
            self.sleep(1)

    def handle_input_event(self, input_event):
        if input_event.node is not self.button:
            return

        # decrement button
        with self.html.lock:
            self.counter.set_text(
                str(int(self.counter.get_text()) - 1)
            )

Inputs

To receive input events, the client has to be aware which of your nodes should produce input events. There are two different input event types CLICK and CHANGE.

from lona.html import Div, CLICK

div = Div(events=[CLICK])

div2 = Div()
div2.events.add(CLICK)
<div data-lona-node-id="182495819713343" data-lona-events="301"></div>

Inputs handle their CHANGE events internally. When the client sends a CHANGE event Input.value gets set, and the event does not get passed to the next event handler. When bubble_up is set, input events get handled and passed further.

Button

from lona.html import Button

Button('Click me!')
Button('Click me!', _id='foo', _style={'color': 'red'})

Init Arguments:

Name Default Value Description
disabled False (Bool) sets the HTML attribute "disabled"
*args () Node args
**kwargs {} Node kwargs

Attributes:

Name Description
disabled (Bool) sets the HTML attribute "disabled"
id_list (List) contains all ids
class_list (List) contains all classes
style (Dict) contains all styling attributes

TextInput / TextArea

Note

readonly was added in 1.6

from lona.html import TextInput, TextArea

TextInput()
TextInput(value='foo', _id='bar', _style={'color': 'red'})

Init Arguments:

Name Default Value Description
value None (Str,None) Initial value
bubble_up False (Bool) Pass input events further
disabled False (Bool) sets the HTML attribute "disabled"
readonly False (Bool) Accepts no input, but can be read and selected
input_delay 300 (Int) Input delay in milliseconds
*args () Node args
**kwargs {} Node kwargs

input_delay: When input_delay is set to 0, the Javascript client uses onchange events. This means the change event gets send when the text input loses focus or the user hits enter after changing the input. When input_delay is set to an integer greater than 0, the Javascript client uses oninput events with input_delay as timeout. The Javascript client then delays sending input events by input_delay ms, and newer input events cancel older, pendings events. This is also known as debouncing of input events in reactive programming.

Attributes:

Name Description
value (Str) Currently set value
disabled (Bool) sets the HTML attribute "disabled"
readonly (Bool) Accepts no input, but can be read and selected
id_list (List) contains all ids
class_list (List) contains all classes
style (Dict) contains all styling attributes

CheckBox

from lona.html import Checkbox

CheckBox()
CheckBox(value=True, _id='bar')

Init Arguments:

Name Default Value Description
value False (Bool) Initial value
bubble_up False (Bool) Pass input events further
disabled False (Bool) sets the HTML attribute "disabled"
*args () Node args
**kwargs {} Node kwargs

Attributes:

Name Description
value (Bool) Currently set value
disabled (Bool) sets the HTML attribute "disabled"
id_list (List) contains all ids
class_list (List) contains all classes
style (Dict) contains all styling attributes

Select

Note

multiple was added in 1.6

from lona.html import Select

Select([
    # value, label, is_selected
    ('foo', 'Foo', True),
    ('bar', 'Bar', False),
])

Init Arguments:

Name Default Value Description
values None (List of Tuples) Initial values
bubble_up False (Bool) Pass input events further
disabled False (Bool) sets the HTML attribute "disabled"
multiple False (Bool) Enables multi selection
*args () Node args
**kwargs {} Node kwargs

Attributes:

Name Description
values (List of Tuples) All options
value Currently set value
disabled (Bool) sets the HTML attribute "disabled"
multiple (Bool) Enables multi selection
id_list (List) contains all ids
class_list (List) contains all classes
style (Dict) contains all styling attributes

Adding Javascript And CSS To HTML Nodes

HTML nodes can include stylesheets and javascript files in STATIC_FILES. This makes packaging of widgets and nodes possible.

To control the include order, sort_order is used. sort_order is a simple integer, but to make the code more readable lona.static_files.SORT_ORDER is used.

from lona.static_files import StyleSheet, Script, SORT_ORDER
from lona.html import Widget, Div

class ChartJsWidget(Widget):
    STATIC_FILES = [
        # styesheets
        StyleSheet(
            name='chart_css_min',
            path='static/Chart.min.css',
            url='Chart.min.css',
            sort_order=SORT_ORDER.FRAMEWORK,
        ),
        StyleSheet(
            name='chart_css',
            path='static/Chart.css',
            url='Chart.css',
            sort_order=SORT_ORDER.FRAMEWORK,
            link=False,  # When link is set to False the given file
                         # gets collected, but not linked. Thats necessary
                         # to make map files possible.
        ),

        # scripts
        Script(
            name='chart_bundle_js_min',
            path='static/Chart.bundle.min.js',
            url='Chart.bundle.min.js',
            sort_order=SORT_ORDER.FRAMEWORK,
        ),
        Script(
            name='chart_bundle_js',
            path='static/Chart.bundle.js',
            url='Chart.bundle.js',
            sort_order=SORT_ORDER.FRAMEWORK,
            link=False,
        ),
        Script(
            name='chart_js_widget_js',
            path='static/chart-js-widget.js',
            url='chart-js-widget.js',
            sort_order=SORT_ORDER.LIBRARY,
        ),
    ]

Static files, included in HTML nodes, get included in the frontend template with template tags.

{{ Lona.load_scripts() }}
{{ Lona.load_stylesheets() }}

More information: Frontends

Widgets

Widgets are a collections of Nodes that are used to encapsulate logic and input event handling.

from lona.html import Widget, Span


class Counter(Widget):
    def __init__(self, initial_value=0):
        self.nodes = [
            Span(initial_value),
        ]

    def set_value(self, new_value):
        self.nodes[0].set_text(new_value)

Handling Input Events

from lona.html import Widget, Div, Span, Button


class Counter(Widget):
    def __init__(self, initial_value=0):
        self.counter = initial_value

        self.counter_label = Span(str(self.counter))
        self.inc_button = Button('+')
        self.dec_button = Button('-')

        self.nodes = [
            Div(
                self.counter_label,
                self.inc_button,
                self.dec_button,
            ),
        ]

    def handle_input_event(self, input_event):
        if input_event.node is self.inc_button:
            self.counter = self.counter + 1
            self.counter_label.set_text(str(self.counter))

        elif input_event.node is self.dec_button:
            self.counter = self.counter - 1
            self.counter_label.set_text(str(self.counter))

        else:
            return input_event

Event Bubbling

When an input event gets issued by the frontend, Lona runs all Widget input event handler from the innermost to the outermost until one of them does not return the event. In this case the event is regarded as handled. If all handler return the event LonaView.handle_input_event() gets to handle the event.

MyWidget(  # last
    MyWidget(  # second
        MyWidget(  # first
            Button('Click me!'),
        ),
    ),
)

Frontend Widgets

Widgets can define a Javascript based frontend widget, to include client side code. This is useful to integrate with third party Javascript libraries.

To communicate between the backend widget and the frontend widget, the backend can set its state in Widget.state, a dict like object, and the frontend can issue events with custom data.

# my_widget.py

from lona.static_files import Script
from lona.html import Widget, Div

class MyWidget(Widget):
    FRONTEND_WIDGET_CLASS = 'MyFrontendWidget'

    STATIC_FILES = [
        # the path is always relative to the current file
        Script(name='MyFrontendWidget', path='my_frontend_widget.js'),
    ]

    def __init__(self):
        self.nodes = [
            Div('foo'),
        ]

        self.data = {'foo': 'bar'}
// my_frontend_widget.js

function MyFrontendWidget(lona_window) {
    this.lona_window = lona_window;

    this.setup = function() {
        // gets called when the widget gets initialized

        console.log('setup', this.nodes);
    };

    this.deconstruct = function() {
        // gets called when the widget gets destroyed

        console.log('deconstruct', this.nodes);
    };

    this.data_updated = function() {
        // gets called every time Widget.data gets updated in the backend

        console.log('data updated:', this.data);
    };
};

Lona.register_widget_class('MyFrontendWidget', MyFrontendWidget);

Firing Custom Input Events

// my_frontend_widget.js

function MyFrontendWidget(lona_window) {
    this.lona_window = lona_window;

    this.setup = function() {
        this.nodes[0].onclick = function(event) {

            // the node argument is optional and can be undefined
            lona_window.fire_input_event(this.nodes[0], 'custom-event', {foo: 'bar'});
        };
    };