Views
- View Types
- Interactive
- Non-Interactive
- HTTP Pass Through
- URL Args
- Simple Patterns
- Custom Patterns
- Trailing Slashes
- Request Objects
- Attributes
- GET
- POST
- Response Objects
- HTML Responses
- Template Responses
- Dictionary Based
- Class Based
- Redirects
- Dictionary Based
- Class Based
- HTTP Redirects
- Dictionary Based
- Class Based
- JSON Responses
- Dictionary Based
- Class Based
- Binary Responses
- Dictionary Based
- Class Based
- Custom Headers
- Dictionary Based
- Class Based
- View Hooks
- LonaView.handle_request()
- LonaView.handle_input_event_root()
- LonaView.handle_input_event()
- LonaView.on_view_event()
- LonaView.on_stop()
- LonaView.on_cleanup()
- LonaView.on_shutdown()
- View Attributes
- ForbiddenError
- NotFoundError
- Input Events
- Input Event types
- CLICK
- CHANGE
- FOCUS
- BLUR
- Input Event Attributes
- Input Event Methods
- InputEvent.node_has_id()
- InputEvent.node_has_class()
- Handling Input events In A Callback
- Awaiting Input Events
- Handling Input Events In A Hook
- Overriding All Input Event Hooks
- Adding Javascript And CSS To View
- View Events
- View Methods
- LonaView.show()
- LonaView.set_title()
- LonaView.await_input_event()
- LonaView.await_click()
- LonaView.await_change()
- LonaView.await_focus()
- LonaView.await_blur()
- LonaView.daemonize()
- LonaView.is_daemon
- LonaView.subscribe()
- LonaView.unsubscribe_from_all_channels()
- LonaView.iter_objects()
- LonaView.fire_view_event()
- LonaView.send_str()
- LonaView.sleep()
- LonaView.ping()
- LonaView.await_sync()
- LonaView.embed_shell()
View Types
Interactive
By default all Lona views are interactive. Interactive Lona views run over a websocket connection, which makes input events and background views possible.
More information: Basic Concept
Non-Interactive
Non interactive views run on HTTP, not websockets. This is useful to add a JSON-API to your project for example.
To mark a view non interactive, use the interactive keyword in the routes.py.
# routes.py
from lona import Route
routes = [
Route('/json-api.json', 'views/json_api.py::JSONAPIView', interactive=False)
]
# views/json_api.py
from lona import LonaView, JsonResponse
class JSONAPIView(LonaView):
def handle_request(self, request):
return JsonResponse({'exit_code': 0, 'value': 'foo'})
HTTP Pass Through
HTTP pass through views bypass the most of the Lona server infrastructure and speak directly to aiohttp. Therefore HTTP pass through views get aiohttp request objects passed in and have to return aiohttp response objects.
This is useful to use basic HTTP features like file upload, or connect Lona to WSGI based frameworks like Django.
HTTP pass through views can be callbacks instead of classes, to make projects like aiohttp wsgi work. More information: Integrating Django.
To mark a view as HTTP pass through, use the http_pass_through keyword in the routes.py.
# routes.py
from lona import Route
routes = [
Route('/http-pass-through/', 'views/http_pass_through.py::HTTPPassThroughView',
http_pass_through=True),
]
# views/http_pass_through.py
from aiohttp.web import Response
from lona import LonaView
class HTTPPassThroughView(LonaView):
def handle_request(self, request):
return Response(
body='<h1>HTTP Pass Through</h1>',
content_type='text/html',
)
URL Args
Simple Patterns
# routes.py
from lona import Route
routes = [
Route('/<arg1>/<arg2>/', 'views/my_view.py::MyView'),
]
# views/my_view.py
from lona import LonaView
class MyView(LonaView):
def handle_request(self, request):
arg1 = request.match_info['arg1']
arg2 = request.match_info['arg2']
Custom Patterns
Custom patterns can be any valid regex:
# routes.py
from lona import Route
routes = [
Route('/<arg1:[a-z]{3}>/', 'views/my_view.py::MyView'),
]
It is possible to match any character (including the /). The following route matches any URL beginning with prefix:
# routes.py
from lona import Route
routes = [
Route('/prefix<path:.*>', 'views/my_view.py::MyView'),
]
Request Objects
Attributes
Note
- request.user is writable since 1.4
- request.interactive was added in 1.4
- request.id was added in 1.16.1
Name | Description |
---|---|
id | (String) Unique identifier |
interactive | (Bool) Is true when the request came in over a websocket connection |
method | (String) Contains either 'GET' or 'POST' |
GET | (Dict) Contains the URL query |
POST | (Dict) Contains POST arguments. Empty in case of GET requests |
route | (lona.routing.Route) Contains the Lona route that linked to this view |
match_info | (Dict) Contains the routing Match info |
user | Contains the user associated with this request |
url | Python yarl url object |
frontend | (Bool) flag if this is a frontend view |
server | Reference to the running Lona server |
GET
By default all Lona view requests are GET requests. The URL query is stored in request.GET.
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
print(request.method)
print(request.GET)
return ''
POST
It is possible to use traditional POST requests. This doesn't require the view to wait for user input and saves resources.
from lona.html import HTML, Form, TextInput, Submit, H1
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
if request.method == 'POST':
return f'<h1>Hello {request.POST["name"]}</h1>'
return HTML(
H1('Enter your name'),
Form(
TextInput(name='name'),
Submit('Submit'),
action='.',
method='post',
),
)
Response Objects
HTML Responses
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
return """
<h1>Hello World</h1>
"""
from lona import LonaView
from lona.html import H1
class MyLonaView(LonaView):
def handle_request(self, request):
return H1('Hello World')
Template Responses
Dictionary Based
Warning
Dictionary based responses are deprecated since 1.12 and will be removed in 2.0
Use response classes instead
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
return {
'template': 'path/to/your/template.html',
'foo': 'bar',
}
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
return {
'template_string': '<h1>{{ header }}}</h1>',
'header': 'Hello World',
}
Class Based
Note
Added in 1.12
from lona import LonaView, TemplateResponse, TemplateStringResponse
class MyLonaView(LonaView):
def handle_request(self, request):
return TemplateResponse(
'template_name.html',
{
'variable_name': 'foo',
},
)
class MyLonaView(LonaView):
def handle_request(self, request):
return TemplateStringResponse(
'{{ message }}',
{
'variable_name': 'Template String Response',
},
)
Redirects
HTTP Redirects
JSON Responses
Note
JSON responses are only available in non interactive views
Binary Responses
Note
- Binary responses are only available in non interactive views
- Added in 1.8
Dictionary Based
Warning
Dictionary based responses are deprecated since 1.12 and will be removed in 2.0
Use response classes instead
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
return {
'content_type': 'application/pdf',
'body': open('foo.pdf', 'rb').read(),
}
Custom Headers
Note
- Custom headers are only available in non interactive views
- Added in 1.8
View Hooks
Note
- LonaView.on_stop() was added in 1.7.4
- LonaView.on_cleanup() was added in 1.7.4
- LonaView.on_shutdown() was removed in 1.8
All entry points for user code in Lona views are callbacks in the LonaView class. If a hook name starts with handle_ it means that the view can stop the event handler chain for the incoming event. If a hook name starts with on_ the view gets only notified of the event. It can't control further handling of the event.
The main entry point of a view is handle_request(). handle_request() may run indefinitely and wait for events. All other hooks are supposed to run for shorter periods of time.
After handle_request() stops, the view stays accessible until the user closes the tab. That means even after handle_request() returned, hooks like handle_input_event() and on_view_event() are getting called on incoming events. After handle_request() stopped, on_stop() gets called, and on_cleanup() after the user disconnected, reloaded the tab or changed the browser URL.
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
return '<h1>Hello World</h1>'
def handle_input_event_root(self, input_event):
return input_event
def handle_input_event(self, input_event):
return input_event
def on_view_event(self, view_event):
# deprecated since 1.14
# use Channels instead
pass
def on_stop(self, reason):
pass
def on_cleanup(self):
pass
def on_shutdown(self, reason):
# this hook got removed in 1.8
pass
View Attributes
Name | Description |
---|---|
server | Reference to the running Lona server |
request | Reference to the request passed into handle_request() |
ForbiddenError
To raise a forbidden error and run the 403 view you can raise lona.errors.ForbiddenError.
More information: Error views
from lona import LonaView, ForbiddenError
class MyLonaView(LonaView):
def handle_request(self, request):
if not request.user.is_staff:
raise ForbiddenError
return '<h1>Hello Admin</h1>'
NotFoundError
Note
Added in 1.8.3
To raise a not found error and run the 404 view you can raise lona.NotFoundError.
More information: Error views
import os
from lona import LonaView, NotFoundError
class MyLonaView(LonaView):
def handle_request(self, request):
path = request.match_info['path']
if not os.path.exists(path):
raise NotFoundError
return {
'file': path,
}
Input Events
Note
Changed in 1.5: In all versions prior to 1.5, only widgets could handle their own events. In versions after 1.5 all node classes can.
Changed in 1.7: In all versions prior to 1.7, == checked if two nodes have equal attributes, but did not check if node A is the same node as node B. To check if node A is node B is is instead of == was required.
Added in 1.7.4: Redirects
Input events get handled in a chain of hooks. Every hook is required to return the given input event, to pass it down the chain, or return None to mark the event as handled.
The first member of the chain is LonaView.handle_input_event_root(). If the event got returned Lona passes the event into all AbstractNode.handle_input_event() by bubbling the event the HTML tree up. If the event got returned by the last widget in the chain, Lona checks if LonaView.handle_request() awaits an input event using await_[input_event|click|change](). If not LonaView.handle_input_event() gets called as last member of the chain.
Input events can, but don't have to, contain a node that issued the event in input_event.node.
Input event handler can also return redirects, even after handle_request() stopped.
Input Event types
CLICK
from lona.html import HTML, Div, Button, CLICK
from lona import LonaView
class MyLonaView(LonaView):
def handle_click(self, input_event):
print(input_event.tag_name, 'was clicked')
def handle_request(self, request):
return HTML(
Div('Click me', events=[CLICK], handle_click=self.handle_click),
# Buttons have CLICK set by default
Button('Click Me', handle_click=self.handle_click),
)
Click events contain meta data in input_event.data.
Name | Description |
---|---|
alt_key | Boolean. Is True when ALT was pressed while clicking |
ctrl_key | Boolean. Is True when CTRL was pressed while clicking |
shift_key | Boolean. Is True when SHIFT was pressed while clicking |
meta_key | Boolean. Is True when META was pressed while clicking |
node_height | Integer. Contains the height of the clicked node |
node_width | Integer. Contains the width of the clicked node |
x | Integer. Contains the x coordinate of the Cursor |
y | Integer. Contains the y coordinate of the Cursor |
CHANGE
from lona.html import HTML, TextInput
from lona import LonaView
class MyLonaView(LonaView):
def handle_change(self, input_event):
print('TextInput is set to', input_event.node.value)
def handle_request(self, request):
return HTML(
TextInput(value='foo', handle_change=self.handle_change),
)
Input Event Attributes
Note
Added in 1.5: InputEvent.nodes
Added in 1.11: InputEvent.target_node
Name | Description |
---|---|
node | (lona.html.Node) Reference to the node that issued the input_event |
target_node | (lona.html.Node) Reference to the node that that was under the cursor (event.target in JavaScript) |
nodes | (list(lona.html.Node)) Contains a list of all nodes in the chain up to the root |
data | (Dict) For click events this contains meta data from the browser |
tag_name | (String) Contains the tag name of the node in the browser |
id_list | (List) Contains a list of all ids of the node in the browser |
class_list | (List) Contains a list of all classes of the node in the browser |
event_id | (Int) Contains the event id |
connection | (lona.connection.Connection) Contains the connection over that this event came in |
window_id | The window id the browser gave this view |
request | (lona.request.Request) Contains the request over that this event came in |
document | (lona.html.document.Document) Contains the document that contains input_event.node |
payload | (List) Contains the raw event payload |
Handling Input events In A Callback
Note
- Callbacks were Added in 1.5
- The node keywords handle_click and handle_change were added in 1.6
Every subclass of lona.html.AbstractNode can implement handle_input_event() to handle its own input events.
A callbacks can be implemented using inheritance or by simply resetting the values of AbstractNode.handle_input_event(), AbstractNode.handle_click() or AbstractNode.handle_change().
Callbacks can also be set by passing them directly into nodes, using the handle_change or handle_click keywords.
from lona.html import HTML, H1, Button
from lona import LonaView
class MyLonaView(LonaView):
def handle_button_1_click(self, input_event):
print('button 1 was clicked')
def handle_button_2_click(self, input_event):
print('button 2 was clicked')
def handle_request(self, request):
button_1 = Button('Button 1')
button_1.handle_click = self.handle_button_1_click
html = HTML(
H1('Click the buttons'),
button,
Button('Button 2', handle_click=self.handle_button_2_click),
)
self.show(html)
Awaiting Input Events
Input events can be awaited from handle_request(). This makes forms or wizards possible.
Warning
Using LonaView.await_* means that the calling thread blocks until the awaited input events gets send by the browser. This is useful when writing deamonized views for interactive wizards, but might not scale when the user base of your view grows.
If thread-count and scalability are crucial for you, consider using callbacks.
from lona.html import HTML, H1, Button
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
html = HTML(
H1('Click the button'),
Button('click me'),
)
self.show(html)
input_event = self.await_click()
print(input_event.node, 'was clicked')
Handling Input Events In A Hook
Input events can also be handled in handle_input_event() when handle_request() is busy.
from datetime import datetime
from lona.html import HTML, H1, Div, Button
from lona import LonaView
class MyLonaView(LonaView):
def handle_request(self, request):
self.timestamp = Div()
self.message = Div()
self.button = Button('click me')
self.html = HTML(
H1('Click the button'),
self.timestamp,
self.message,
)
while True:
timestamp.set_text(str(datetime.now()))
self.show(self.html)
self.sleep(1)
def handle_input_event(self, input_event):
if not input_event.node == self.button:
return input_event
self.message.set_text(f'Button was clicked at {datetime.now()}')
Adding Javascript And CSS To View
Note
Added in 1.7.3
Views can include stylesheets and javascript files in STATIC_FILES. Such files will be automatically served and included in html.
Static file's name must be unique in the whole project (including static files in nodes and widgets).
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 import LonaView
class MyView(LonaView):
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. That's 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,
),
]
def handle_request(self, request):
return 'SUCCESS'
View Events
Note
- Added in 1.7.3
- Redirect support was added in 1.7.4
Views can communicate with each other by sending events using LonaView.fire_view_event(). A view event consists of a name and data. The name is mandatory and has to be a string, the data is optional but if set has to be a dict.
When a event is send using LonaView.fire_view_event() it is send to every object of the same view class. To send events to multiple view classes Server.fire_view_event() can be used. Incoming view events are getting handled by LonaView.on_view_event().
View event handler can return redirects, even after handle_request() stopped.
from lona import LonaView
class MyLonaView(LonaView):
def on_view_event(self, view_event):
print(view_event.name, view_event.data)
def handle_request(self, request):
self.fire_view_event('my-event-name', {'foo': 'bar'})