Skip to content

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)
Information Circle

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,
)
ParameterDescription
display_nameTitle of the collapsible panel
childrenList of child elements
statusStatus string shown in the panel header (can be a tag reference for dynamic status)
is_collapsedWhether the panel starts collapsed (legacy; prefer default_open)
default_openWhether 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,
)
ParameterDescription
variantDisplay 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=[],
)
ParameterDescription
display_nameLabel for the component
component_urlURL to the hosted component module
childrenOptional list of child elements passed to the component
**kwargsAny 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
Information Circle
Example

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.

Information Circle
Real-World Example — Modifying Elements in setup()

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

Information Circle
Example — Dynamic Layout

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 UI base class and dynamic element management
  • Widgets -- building custom widget micro-frontends for RemoteComponent
  • Styling -- colours, icons, and conditional display