Containers
Containers are structural elements that group other UI elements into logical sections. They provide visual organisation in the portal without displaying data themselves.
Container
Container is the base class for all container types. It manages an ordered collection of child elements.
from pydoover.ui import Container, NumericVariable, TextVariable
section = Container(
"Sensor Data",
children=[
NumericVariable("Temperature", value=22.5, units="C"),
NumericVariable("Humidity", value=65, units="%"),
],
)
Managing Children
Containers provide three methods for managing their child elements:
add_children() -- Adds one or more elements to the container. Each child is assigned a position automatically if it does not already have one. The child becomes accessible as an attribute on the container.
from pydoover.ui import NumericVariable
section.add_children(
NumericVariable("Pressure", value=101.3, units="kPa"),
NumericVariable("Wind Speed", value=12, units="km/h"),
)
# Access by name
print(section.pressure.value)
remove_children() -- Removes elements from the container. Also recursively removes from any nested containers.
section.remove_children(pressure_element)
The recommended approach is to set hidden=True on elements you want to hide rather than removing them. This preserves the element's position and avoids potential issues with interaction routing.
clear_children() -- Removes all children from the container.
section.clear_children()
Class-Level Children
Containers also collect any Element instances defined as class attributes during initialisation. This allows a subclass-based pattern for defining container contents.
from pydoover.ui import Container, NumericVariable
class SensorPanel(Container):
temperature = NumericVariable("Temperature", value=0, units="C")
humidity = NumericVariable("Humidity", value=0, units="%")
panel = SensorPanel("Sensors")
# panel.temperature and panel.humidity are both children
Submodule
Submodule is the primary container for organising UI sections. It renders as a collapsible panel with an optional status string.
from pydoover.ui import Submodule, NumericVariable, BooleanVariable, tag_ref
pump_section = Submodule(
"Pump Control",
children=[
BooleanVariable("Running", value=tag_ref("pump_running")),
NumericVariable("Flow Rate", value=tag_ref("flow_rate"), units="L/min"),
],
status=tag_ref("pump_status", tag_type="string"),
default_open=True,
)
| Parameter | Description |
|---|---|
display_name | Title of the collapsible panel |
children | List of child elements |
status | Status string shown in the panel header (can be a tag reference for dynamic status) |
is_collapsed | Whether the panel starts collapsed (legacy; prefer default_open) |
default_open | Whether the panel starts expanded (can be a tag reference for dynamic behaviour) |
The status parameter is particularly useful for showing a summary value in the panel header without requiring the user to expand the section.
diagnostics = Submodule(
"Diagnostics",
children=[
TextVariable("Firmware", value="v2.1.0"),
NumericVariable("CPU Temp", value=tag_ref("cpu_temp"), units="C"),
NumericVariable("Memory", value=tag_ref("mem_pct"), units="%"),
],
status=tag_ref("diag_summary", tag_type="string"),
default_open=False,
)
Application
Application is the top-level container that wraps all UI elements for a single app. It is used internally by the framework and is generally not instantiated directly in application code.
from pydoover.ui import Application, ApplicationVariant
app_container = Application(
"My Application",
children=[pump_section, diagnostics],
variant=ApplicationVariant.submodule,
)
| Parameter | Description |
|---|---|
variant | Display mode: ApplicationVariant.submodule (default) or ApplicationVariant.stacked |
The submodule variant embeds the application inside a collapsible submodule, providing visual separation between multiple apps on a device. The stacked variant places elements directly on the page without submodule wrapping.
RemoteComponent
RemoteComponent loads an externally hosted UI component from a URL. This allows embedding custom web components that are not part of the standard UI element set.
from pydoover.ui import RemoteComponent
custom_chart = RemoteComponent(
"Custom Visualization",
component_url="https://example.com/components/custom-chart.js",
children=[],
)
| Parameter | Description |
|---|---|
display_name | Label for the component |
component_url | URL to the hosted component module |
children | Optional list of child elements passed to the component |
**kwargs | Any additional keyword arguments are included in the serialised output and passed to the remote component |
Module Federation kwargs
When the remote component is loaded via module federation (e.g., a React micro-frontend), pass scope and module to tell the portal's federation runtime where to find the component entry point.
WIDGET_NAME = "MyWidget"
widget = RemoteComponent(
WIDGET_NAME,
component_url="$config.app().dv_widget_url",
scope=WIDGET_NAME,
module="./" + WIDGET_NAME,
)
scope-- the module federation container name (typically matches the widget name)module-- the exposed module path within the container (typically"./" + widget_name)component_url-- can use a$config.app()variable reference so the URL is resolved from the deployment config at runtime
A device UI that loads a custom widget via module federation, paired with a built-in ConnectionInfo element:
from pydoover import ui
from pydoover.ui import ConnectionInfo
WIDGET_NAME = "CustomDevice"
class DeviceUI(ui.UI, default_open=True):
widget = ui.RemoteComponent(
WIDGET_NAME,
component_url="$config.app().dv_widget_url",
scope=WIDGET_NAME,
module="./" + WIDGET_NAME,
)
connection_info = ConnectionInfo()
TabContainer
TabContainer renders its children as tabs, with each child container appearing as a separate tab panel.
from pydoover.ui import TabContainer, Submodule, NumericVariable
tabs = TabContainer(
"Data Views",
children=[
Submodule("Live Data", children=[
NumericVariable("Temperature", value=tag_ref("temp"), units="C"),
]),
Submodule("Statistics", children=[
NumericVariable("Average", value=tag_ref("temp_avg"), units="C"),
NumericVariable("Maximum", value=tag_ref("temp_max"), units="C"),
]),
],
)
Each direct child typically corresponds to one tab. The child's display_name becomes the tab label. Both Submodule and Container work as tab panels — use Container for simple flat layouts within a tab.
A water meter UI uses TabContainer with Container children for tab panels, then modifies existing elements in setup() based on config — adjusting precision, building dynamic ranges, and hiding elements:
from pydoover import ui
class MaceWaterMeterUI(ui.UI, display_name=MaceWaterMeterTags.app_display_name):
tabs = ui.TabContainer(
"Tabs",
children=[
ui.Container("Meter", children=[
ui.NumericVariable("Flow", units="ML/day", value=MaceWaterMeterTags.last_flow, form=ui.Widget.radial),
ui.NumericVariable("Meter Total", units="ML", value=MaceWaterMeterTags.last_total),
ui.Timestamp("Last Read", value=MaceWaterMeterTags.time_last_update),
ui.Button("Get Now"),
]),
ui.Container("Event", children=[
ui.NumericVariable("This Event", units="ML", value=MaceWaterMeterTags.last_event_counter),
ui.FloatInput("Send Alert At", name="alert_counter", units="ML", min_val=0, default=None),
ui.FloatInput("Shutdown Pump At", name="shutdown_counter", units="ML", min_val=0, default=None),
ui.Button("Reset Event", requires_confirm=True),
]),
],
)
async def setup(self):
max_flow = self.config.max_flow.value
for elem in (self.tabs.meter.flow, self.tabs.meter.meter_total, self.tabs.event.this_event):
elem.precision = 1 if max_flow >= 100 else 2
self.tabs.meter.flow.ranges = [
ui.Range(None, 0, max_flow * 0.15, ui.Colour.blue),
ui.Range(None, max_flow * 0.15, max_flow, ui.Colour.green),
]
self.tabs.event.shutdown_counter.hidden = not self.config.allow_shutdown.value
Unlike add_element() for adding new elements, this pattern modifies properties of existing class-level elements. Access nested children via chained attribute names — self.tabs.meter.flow reaches the Flow element inside the Meter container inside the Tabs tab container.
Source: mace-water-meter
A sensor monitoring UI dynamically chooses between Submodule and TabContainer layout based on a configuration enum, building per-sensor elements in setup():
from pydoover import ui
class SensorMonitorUI(ui.UI, display_name=MyTags.app_display_name):
alarms_submodule = ui.Submodule("Alarms", position=202, children=[...])
details_submodule = ui.Submodule("Details", position=203, children=[...])
async def setup(self):
sensors = self.config.sensor_labels.elements
if self.config.display_mode.value is DisplayMode.TAB:
tabs = [
build_sensor_tab(e.id.value, e.name.value, position=i)
for i, e in enumerate(sensors)
]
if tabs:
self.add_element(
ui.TabContainer("Sensors", position=60, children=tabs)
)
else:
for i, elem in enumerate(sensors):
self.add_element(
build_sensor_submodule(
elem.id.value, elem.name.value,
default_open=True, position=i,
)
)
Static elements (alarms, details) are declared at class level, while dynamic per-sensor elements are added in setup() via add_element(). The position parameter controls ordering so dynamic elements appear before the static sections.
Nesting Containers
Containers can be nested arbitrarily to create complex layouts.
from pydoover.ui import (
UI, Submodule, NumericVariable, BooleanVariable,
Button, Select, Option, tag_ref,
)
class PlantUI(UI):
overview = Submodule(
"Overview",
children=[
NumericVariable("Total Flow", value=tag_ref("total_flow"), units="L"),
BooleanVariable("System OK", value=tag_ref("system_ok")),
],
default_open=True,
)
pump_1 = Submodule(
"Pump 1",
children=[
BooleanVariable("Running", value=tag_ref("p1_running")),
NumericVariable("Speed", value=tag_ref("p1_speed"), units="RPM"),
Select("Mode", options=[Option("Auto"), Option("Manual"), Option("Off")]),
],
status=tag_ref("p1_status", tag_type="string"),
)
pump_2 = Submodule(
"Pump 2",
children=[
BooleanVariable("Running", value=tag_ref("p2_running")),
NumericVariable("Speed", value=tag_ref("p2_speed"), units="RPM"),
Select("Mode", options=[Option("Auto"), Option("Manual"), Option("Off")]),
],
status=tag_ref("p2_status", tag_type="string"),
)
controls = Submodule(
"Controls",
children=[
Button("Emergency Stop"),
Button("Reset Alarms"),
],
default_open=False,
)
Next Steps
- UI Overview -- introduction to the UI system
- Declarative UI -- the
UIbase class and dynamic element management - Widgets -- building custom widget micro-frontends for
RemoteComponent - Styling -- colours, icons, and conditional display