HTML API

Nodes

Note

Changed in 1.11: Nodes are comparable now

>>> Div() == Div()            # True
>>> Div() is Div()            # False
>>> Div(a=1) == Div()         # False
>>> Span() == Div()           # False
>>> Div(Div()) == Div(Div())  # True

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.15

To initialize an HTML tree you can use lona.html.parse_html, which returns a Lona HTML node or a list of Lona HTML nodes.

lona.html.parse_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.

When lona.html.parse_html parses a HTML string, that results in a HTML tree with exacly one root node, and flat is set to True, which is the default, lona.html.parse_html will flatten the tree, by returning the root node instead of the list.

from lona.html import parse_html

>>> parse_html('<h1>Hello World</h1><p>Lorem Ipsum</p>')
[<h1 data-lona-node-id="9">
  Hello World
</h1>,
 <p data-lona-node-id="11">
  Lorem Ipsum
</p>]

>>> parse_html('<h1>Hello World</h1>')
<h1 data-lona-node-id="14">
  Hello World
</h1>
Using lona.html.HTML

Warning

Using lona.html.HTML for HTML string parsing is deprecated as of 1.15. Use lona.html.parse_html instead.

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>

Using Raw HTML

Note

Added in 1.13

Lona represents HTML nodes as high-level Python objects, which have an inherit overhead, especially when handling big HTML trees as strings, that have to be parsed before.

lona.html.RawHTML takes HTML as a string, and does not convert it to a Lona node tree on the server, but renders it as HTML on the client. The HTML string can be updated by setting RawHTML.inner_html.

>>> from lona.html import RawHTML
>>> RawHTML('<h1>Hello World</h1>')
<div data-lona-node-id="1">
    <h1>Hello World</h1>
</div>

Attributes

from lona.html import Div

div = Div(foo='bar')
<div data-lona-node-id="174102029578147" foo="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 threads 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)
            )

State

Note

Added in 1.10

Changed in 1.11: Node.state now can be initialized using Node(state={})

Lona nodes can store state that is not send to the client in node.state. This data store can be used to transport state between scopes, for example when handling input events.

node.state is thread safe and is coupled with node.lock.

DATA = [
    ('Alice', 'Alison', 1, ),
    ('Bob', 'Brown', 2, ),
]

class MyLonaView(LonaView):
    def handle_request(self, request):
        # show all entries in DATA
        html = HTML(
            Table(
                Tr(
                    Th('First Name'),
                    Th('Last Name'),
                )
            )
        )

        for first_name, last_name, secret_id in DATA:
            tr = Tr(
                Td(first_name),
                Td(last_name),
                events=[CLICK],
            )

            tr.handle_click = self.handle_click_on_tr

            # set secret id
            tr.state['secret_id'] = secret_id

        self.show(html)

    def handle_click_on_tr(self, input_event):
        # retrieve secret id
        secret_id = input_event.node.state['secret_id']

        # remove entry by secret_id
        DATABASE.remove_user(secret_id)

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
NumberInput

Note

Added in 1.8

from lona.html import NumberInput

NumberInput()
NumberInput(min=2, max=8, step=2)

Init Arguments:

Name Default Value Description
value None (Float,None) Initial value
min None (Float,None) Minimal value
max None (Float,None) Maximal value
step None (Float,None) Valid steps for 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

Attributes:

Name Description
value (Float) Currently set value
raw_value (Str) Currently raw value set by the user
min (Float,None) Minimal value
max (Float,None) Maximal value
step (Float,None) Valid steps for value
valid (Bool) value meets all constrains set by min, max and step
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

Warning

Deprecated since 1.12. Use Select2 instead.

from lona.html import Select

Select(
    values=[
        # 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
Select2

Note

Added in 1.12

from lona.html import Select2, Option2

select2 = Select2(
    Option2('Option 1', value='1'),
    Option2('Option 2', value=2),
    Option2('Option 3', value=3.0, selected=True),
)

# disable first option
select2.options[0].disabled = True

# select second option by selected property
select2.options[1].selected = True

# select second option by value property
select2.value = 2

A Select2 consist of one or more Option2 objects, which hold information on value, selection state and disabled state.

Option2 objects consist of a label text and a value. The value can be anything. If Option2.render_value is set, which is set by default, the content of Option2.value gets typecasted to a string and rendered into the HTML tree. This can be disabled if the actually values of the select shouldn't be disclosed to end users.

Select2.value returns the value of the option that is currently selected. If the Select2 is a multi select, Select2.value returns a tuple of all selected options values.

An option can be selected by setting Select2.value to the value of the option that should be selected, or by setting Option2.selected. If the select is no multi select, all other options get unselected automatically.

Select Attributes:

Name Description
value Value of the currently selected option or tuple of values of selected options
values (Tuple) tuple of all possible values
options (Tuple) tuple of all options
selected_options (Tuple) tuple of all selected options
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

Option Attributes:

Name Description
value value of the option
selected (Bool) sets selection state
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
RadioGroup and RadioButton

Note

Added in 1.16

from lona.html import RadioGroup, RadioButton, Label

radio_group = RadioGroup(
    Label('Option 1', RadioButton(value=1)),
    Label('Option 2', RadioButton(value=2.0)),
    Label('Option 3', RadioButton(value='3', checked=True)),
)

# adding radio buttons
# `RadioGroup.add_button()` takes any amount of nodes and connects
# the first `Label` with the first `RadioButton` object, using a
# random number and the HTML attribute `for`
radio_group.add_button(Label('Foo'), RadioButton(value='foo'))
radio_group.add_button(Div(Label('Foo'), RadioButton(value='foo')))

# if two non-node values are given, and the first one is a string,
# a `Label` and a `RadioButton` get inserted automatically
radio_group.add_button('Foo', 'foo')

A RadioGroup consist of one or more RadioButton and Label object pairs, which hold information on value, checked state, and disabled state.

RadioButton objects consist of a value and a checked state. The value can be anything. If RadioButton.render_value is set, which is set by default, the content of RadioButton.value gets typecasted to a string and rendered into the HTML tree. This can be disabled if the actually values of the select shouldn't be disclosed to end users.

RadioGroup.value returns the value of the radio button that is currently checked.

A radio button can be checked by setting RadioGroup.value to the value of the radio button that should be checked, or by setting RadioButton.checked.

RadioGroup Attributes:

Name Description
bubble_up (Bool) Pass input events further
radio_buttons (Tuple) tuple of all radio buttons
checked_radio_button (Tuple) tuple of all selected options
value Value of the currently checked radio button
values (Tuple) tuple of all possible values
id_list (List) contains all ids
class_list (List) contains all classes
style (Dict) contains all styling attributes

RadioButton Attributes:

Name Description
name name of the radio button
value value of the radio button
checked (Bool) sets checked state
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

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

Note

Frontend widget support for lona.html.Node was added in 1.10.5

Widgets and nodes 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, or in Node.widget_data 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_node.py

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

class MyNode(Div):
    WIDGET = 'MyFrontendWidget'

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

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

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

        self.widget_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'});
        };
    };