HTML API
- Nodes
- Sub Nodes
- Selectors
- Syntax
- Closest()
- Using HTML Strings
- Using lona.html.HTML
- Using Raw HTML
- Attributes
- ID / Class List
- Style
- Helper Methods
- Node.hide()
- Node.show()
- Node.set_text()
- Node.get_text()
- Links
- Adding Custom Nodes
- Extending Nodes
- Locking
- State
- Inputs
- Button
- TextInput / TextArea
- NumberInput
- CheckBox
- Select
- Select2
- RadioGroup and RadioButton
- Adding Javascript And CSS To HTML Nodes
- Widgets
- Handling Input Events
- Event Bubbling
- Frontend Widgets
- Firing Custom Input Events
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" |
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>
Links
from lona.html import A
# internal link
A('Internal Link', href='/internal-link/')
# external link
A('Lona Documentation', href='https://lona-web.org/', interactive=False)
# internal link to a non-interactive (or HTTP-pass-through) view that
# serves a downloadable file. Without "target='_blank'", the browser would
# try to download the file in the current browser tab, terminating the
# websocket connection, which would break the currently opened,
# interactive view.
A('Internal Download Link', href='/foo.pdf',
interactive=False, target='_blank')
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'});
};
};