2. HTML

In Lona, any HTML element can be represented using Python objects, derived from lona.html.Node.

This

from lona.html import Div, Span

Div(
    Span('Hello World'),
    _class='hello-world-div',
    _style={
        'color': 'red',
    },
)

is the equivalent to this

<div class="hello-world-div" style="color: red">
    <span>Hello World</span>
</div>

Like in traditional HTML, Lona nodes form a tree. Child nodes, like the span in the first example, are available via Node.nodes. Lona nodes have support for element classes, ids, attributes and styling via the Node.class_list, Node.id_list, Node.attributes and Node.style properties.

When a node gets initialized, all arguments are treated as child nodes of the new node and all keyword arguments are treated as element attributes, later available in Node.attributes. Special keywords like class, id, style or nodes will end up in their respective property counterparts.

Nodes, as well as their properties Node.nodes, Node.id_list, and Node.class_list, behave like Python lists. They support indexing, slicing, and common interfaces like append() or remove(). Node.attributes and Node.style behave like Python dictionaries.

All strings that get passed to a node, or to Node.nodes, get converted to a lona.html.TextNode, which behave like strings.

Nodes Are Unique

Lona nodes are unique, and can be part of exactly one node tree at a time. If you add a node to another node, and it was part of another node before, it gets unmounted before. That means even if you append a node to another node multiple times, there will never be more than one reference to the same node.

>>> from lona.html import Div, Span
>>> div = Div()
>>> span = Span()

>>> div.append(span)
>>> div.append(span)
>>> div.append(span)

>>> div
<div>
    <span></span>
</div>

Showing HTML In The Browser

There are two ways to show HTML in the browser: Returning HTML from the View.handle_request() method or by running View.show().

Returning HTML

This example returns an HTML tree, showing it once and exiting View.handle_request().

from datetime import datetime

from lona_picocss import install_picocss

from lona.html import Strong, Span, HTML, H1, P
from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


@app.route('/')
class Index(View):
    def handle_request(self, request):
        html = HTML(
            H1('Clock'),
            P(
                Span('The current time is: '),
                Strong(str(datetime.now())),
            ),
        )

        return html


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

View.show()

The second example runs View.show() in a loop continuously, to update the HTML that is shown in the browser.

When View.show() gets called with the same HTML node object twice, Lona sends only updates. If the HTML was not changed View.show() does nothing.

from datetime import datetime

from lona_picocss import install_picocss

from lona.html import Strong, Span, HTML, H1, P
from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


@app.route('/')
class Index(View):
    def handle_request(self, request):
        timestamp = Strong()

        html = HTML(
            H1('Clock'),
            P(
                Span('The current time is: '),
                timestamp,
            ),
        )

        while True:

            # set current time
            timestamp.set_text(datetime.now())

            # show html
            self.show(html)

            # sleep for one second
            self.sleep(1)


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

More Complex Example

from datetime import datetime

from lona_picocss import install_picocss

from lona.html import Strong, Span, HTML, Div, H2, H1, P
from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


@app.route('/')
class Index(View):
    def handle_request(self, request):
        colors = [
            'Tomato',
            'Orange',
            'DodgerBlue',
            'MediumSeaGreen',
            'Gray',
            'SlateBlue',
            'Violet',
            'LightGray',
        ]

        current_color = 0

        # setup the HTML
        timestamp = Strong(str(datetime.now()))
        previous_timestamps = Div()

        html = HTML(
            H1('Clock'),
            P(
                Span('The current time is: '),
                timestamp,
            ),

            H2('Previous Timestamps'),
            previous_timestamps,
        )

        # main loop
        while True:

            # pick a color
            current_color = current_color % len(colors)

            # move the last timestamp to the log
            previous_timestamps.append(
                Div(
                    timestamp.get_text(),
                    style={
                        'color': colors[current_color],
                    },
                ),
            )

            # shorten the previous timestamps if needed
            if len(previous_timestamps) > 5:
                previous_timestamps.nodes[0].remove()

            # set the current time
            timestamp.set_text(datetime.now())
            timestamp.style['color'] = colors[current_color]

            # show html
            self.show(html)

            self.sleep(1)
            current_color += 1


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

HTML Strings

When initializing big HTML trees it can be more convenient to write HTML as a string. Lona can parse any given HTML string into a Lona Node tree, that then can be manipulated using the Lona node API. The HTML class is special in this respect.

>>> from lona.html import HTML

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

>>> html[0].id_list.append('baz')

>>> html
<div id="foo">
    <span id="bar baz"></span>
</span>

More information: Using HTML Strings

Raw HTML

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>
    <h1>Hello World</h1>
</div>

Selectors

When handling deeply nested HTML trees, lookups can become tedious. Lona implements Node.query_selector() and Node.query_selector_all() to find and iterate over matching nodes.

>>> from lona.html import Div

>>> div = Div(
        Div(
            Div(
                Div(id='foo'),
            ),
        ),
    )

>>> foo_div = div.query_selector('div#foo')

More information: Selectors

Locking

Lona is highly multi-threaded and View.show() gets called implicitly in some cases, for example when handling click events. When making multiple modifications to a HTML tree, it can become important that none of the intermediate steps get sent to the browser. Let's say you want to open a popup: You have to create the popup, set its header, set its body, configure its buttons and then show it. Any state, but the last one could lead to bad user experience.

To accomplish that, Lona nodes implement locks. All modifications, made to a node, trigger an implicit lock, if the current thread does not hold one. This makes sure that you do not modify a node accidentally, that is currently locked by another thread.

>>> from lona_picocss.html import Modal, H3, P

>>> modal = Modal()

>>> with modal.lock:
        modal.get_body().nodes = [
            H3('Lorem'),
            P('Lorem ipsum'),
        ]

        modal.open()

More information: Locking

Extending Node Classes

Lona HTML nodes are just simple Python classes. To extend node classes, create custom ones, or to create high-level components, just inherit from lona.html.Node or a node class like lona.html.Div.

>>> from lona.html import Node

>>> class Counter(Node):
        TAG_NAME = 'div'
        CLASS_LIST = ['counter']

        def __init__(self, initial_value):
            super().__init__()

            self.nodes = [
                str(initial_value),
            ]

        def increment(self):
            with self.lock:
                current_value = int(self.nodes[0])
                new_value = current_value += 1

                self.nodes = [
                    str(new_value),
                ]

>>> counter = Counter(10)
>>> counter
<div class="counter">10</div>

>>> counter.increment()
>>> counter
<div class="counter">11</div>