htmlayout

Dash HTMLayout

This package provides the ability to build dashboard layouts in Dash by writing HTML documents.

Installation

The package is published on

PyPI

You can install this package by using a tool like pip:

pip install dash-htmlayout

How to use

In Python code

To load an HTML layout for your Dash application, you need to instantiate a dash.htmlayout.Builder object with an HTML file path. URLs are supported through the lxml library.

from dash import Dash
from dash.htmlayout import Builder

app = Dash()
builder = Builder(file="dash_app.html")
app.layout = builder.layout

In the HTML file

Since the usual way to build a Dash layout does not involve making the whole HTML5 structure but only what's in the <body> tag, the same has to be observed in your HTML/XML file. For example:

<section>
    <h1 id="dash-title">Title</h1>
    <dcc-dropdown id="select-1" data-options="['Red', 'Blue']"/>
</section>

Only HTML comments and tags backed up by a component offered by the Dash libraries would be accepted in your document. Also, your document must have a valid root element (anything usable).

Non-string parameters

Any parameter for a component class can be defined as a tag attribute, as long as it is a str. Parameters that are not of type str can be provided in HTML using the following scheme:

<div data-parameter="python literal to evaluate" ...>

For example, the Dropdown and Slider components from dash.dcc accept some arguments that are lists or integers, like options and value. You may pass those in HTML like follows:

<dcc-slider data-value="1" data-min="0" data-max="10" data-step="5" />
<dcc-dropdown data-options="['Option 1', 'Option 2']" />

Every attribute starting with the name data-<name> will be treated as the argument <name>, with its text value evaluated as a Python literal.

What tag names to use for non-HTML components?

By default, any component that is not in the dash.html module has to be named with a prefix. Generally, the prefix is the most relevant part of the module name, eg. table for the dash_table module. The prefix must be used as following:

<dcc-dropdown/>
<table-datatable/>
<daq-booleanswitch data-on="False"/>

Dot notation is not used for prefixing because it interferes with content editor helpers like Emmet, and will be considered as a class.

Finding elements in layout by ID

If you give an ID to a component in your layout, you can find it easily in the builder by using the get_component method:

component = builder.get_component("tag-identifier")
1"""
2.. include:: ../../DOCUMENTATION.md
3"""
4from .builder import Builder
5
6__all__ = [Builder]
class Builder:
 16class Builder:
 17    """
 18    Class to build layouts using HTML snippets.
 19
 20    Objects of this class allow loading HTML files describing
 21    a Dash application layout. All tag names of HTML5 are supported
 22    by default. Components that are not strictly matching an HTML
 23    component, but are available in Dash are also supported using
 24    a prefix.
 25
 26    ```python
 27    from dash import Dash
 28    from dash.htmlayout import Builder
 29
 30    app = Dash("myapp")
 31    builder = Builder("URL or filename")
 32    dropdown = builder.get_component("test-dropdown")
 33    app.layout = builder.layout
 34    app.run(debug=True)
 35    ```
 36
 37    ```html
 38    <div>
 39        <h1>Simple Dashboard</h1>
 40        <div class="content">
 41            <dcc.dropdown id="test-dropdown"/>
 42        </div>
 43    </div>
 44    ```
 45    """
 46
 47    _module_registry = {
 48        "dash.html": None,
 49        "dash.dcc": "dcc",
 50        "dash.dash_table": "table",
 51        "dash_daq": "daq",
 52        "dash_bio": "bio",
 53        "dash_slicer": "slicer",
 54        "dash_player": "player",
 55        "dash_bootstrap_components": "bootstrap",
 56    }
 57    _component_registry: dict[str, type] = {}
 58    # List of modules with already autodetected components
 59    _autodetected_modules: set = set()
 60    # Components with id
 61    _components: dict[str, Component] = {}
 62    # Layout root component
 63    layout: Optional[Component] = None
 64
 65    def __new__(cls, *args, file: PathLike = None, **kwargs) -> "Builder":
 66        """
 67        Instanciate a new LayoutBuilder object.
 68
 69        Every new object will start an autodetection.
 70
 71        See Also:
 72            `Builder.autodetect`
 73        """
 74        instance = super().__new__(cls, *args, **kwargs)
 75        cls._autodetect_components()
 76        if file is not None:
 77            instance.load(file)
 78        return instance
 79
 80    @classmethod
 81    def _autodetect_components(cls):
 82        """
 83        Autodetect and register components from the module registry.
 84
 85        This class method registers *all* the Dash `Component` classes
 86        found in various modules, mainly the default ones provided with
 87        Dash.
 88
 89        All those classes will be recognized as HTML tags in input documents.
 90        The name of the tag is derived from the name of the component class,
 91        but all lowercase. When the component class is not in the `dash.html`
 92        module, the name must be prefixed with a namespace. For example:
 93
 94        ```
 95        dash.html.Section → <section>
 96        dash_daq.ColorPicker → <daq-colorpicker>
 97        ```
 98
 99        When there is a prefix, it is derived from the name of the module
100        it represents, but without the redundant `dash` name. Words like
101        `component` or `components` are also removed from the prefix.
102
103        Notes:
104            This method is called automatically when instantiating your
105            first `Builder` object, and everytime you register a new library
106            of components with `Builder.register_library`.
107        """
108        for module_name, prefix in cls._module_registry.items():
109            try:
110                if module_name not in cls._autodetected_modules:
111                    module = import_module(module_name)
112                    cls._autodetected_modules.add(module_name)
113                    for attribute in dir(module):
114                        symbol = getattr(module, attribute)
115                        if isinstance(symbol, type) and issubclass(symbol, Component):
116                            full_prefix: str = f"{prefix}-" if prefix else ""
117                            tag_name: str = f"{symbol.__name__.lower()}"
118                            full_name: str = f"{full_prefix}{tag_name}"
119                            cls._component_registry[full_name] = symbol
120            except ImportError:
121                print(
122                    f"Warning: {module_name} is listed in LayoutBuilder "
123                    f"registry but could not be imported.",
124                    file=sys.stderr,
125                )
126
127    @classmethod
128    def register_library(
129            cls, path: str, prefix: Optional[str], replace: bool = False
130    ) -> bool:
131        """
132        Register a new module providing Dash components.
133
134        A sensible default is already provided for known libraries,
135        but you can custom libraries with this method. You just have to
136        call this by passing a module path and a prefix to apply to detected
137        components:
138
139        ```python
140        from dash.htmlayout import Builder
141
142        Builder.register_library("dash_colorful_lib", "colorful")
143        ```
144        """
145        if path not in cls._module_registry or replace:
146            cls._module_registry[path] = prefix
147            cls._autodetect_components()
148            return True
149        return False
150
151    def load(self, path: PathLike) -> Component:
152        """
153        Build a layout from an HTML file.
154
155        Returns:
156            The root of the layout tree with all its descendants.
157        """
158        parser = etree.XMLParser(
159            remove_comments=True, ns_clean=True, remove_pis=True,
160            resolve_entities=False, remove_blank_text=True)
161        root = etree.parse(path, parser=parser).getroot()
162        self._components = {}
163        print(etree.tostring(root))
164        if root:
165            self.layout = self._build_tree(root)
166        return self.layout
167
168    def get_component(self, identifier: str) -> Optional[Component]:
169        """
170        Get component instance in the loqded lqyout given an ID.
171
172        Args:
173            identifier: id given to the component.
174
175        Returns:
176            If found, the component with the given id.
177            `None` if the id is not found in the current layout.
178        """
179        return self._components.get(identifier)
180
181    @classmethod
182    def _to_component(cls, tag: str, **options) -> Optional[Component]:
183        """Build a component from an HTML tag, with options."""
184        component: Optional[Type[Component]] = cls._component_registry.get(
185            tag.lower(), None
186        )
187        if component is not None:
188            component: Component = component(**options)
189        return component
190
191    def _build_tree(self, element: _Element):
192        """
193        Build a layout recursively from an HTML element.
194
195        Returns:
196            A Dash component with children and attributes.
197        """
198        tag_name: str = element.tag
199        tag_text: str = (element.text or "").strip()
200        tag_children: list = list(filter(None, [tag_text]))
201        tag_attrs: dict = self._convert_data_attributes(element.attrib)
202        tag_id: Optional[str] = element.attrib.get("id")
203        component: Component = self._to_component(tag_name, **tag_attrs)
204        if tag_id is not None:
205            self._components[tag_id] = component
206        if hasattr(component, "children"):
207            component.children = tag_children
208            for child in element.getchildren():  # type: _Element
209                child_component: Component = self._build_tree(child)
210                component.children.append(child_component)
211        return component
212
213    @classmethod
214    def _convert_data_attributes(cls, attributes: dict) -> dict:
215        """
216        Convert `data-attr` attributes to `attr` as an evaluated literal
217
218        Args:
219            attributes: dictionary of HTML/XML attributes.
220
221        Returns:
222            Dictionary of attributes with `data-*` attributes
223            converted to Python objects.
224        """
225        output: dict[str, Any] = dict(sorted(attributes.items()))
226        converted_keys: set = set()
227        generated_keys: dict = {}
228        for key in output:
229            key = key.lower()
230            # Match keys named data-<name> and create a python object from that
231            matching = re.match(r"^(data-)(\w+)", key)
232            if matching:
233                new_key = matching.groups()[1]
234                generated_keys[new_key]: Any = evaluate(output[key])
235                converted_keys.add(key)
236        output.update(generated_keys)
237        for key in converted_keys:
238            del output[key]
239        return output

Class to build layouts using HTML snippets.

Objects of this class allow loading HTML files describing a Dash application layout. All tag names of HTML5 are supported by default. Components that are not strictly matching an HTML component, but are available in Dash are also supported using a prefix.

from dash import Dash
from dash.htmlayout import Builder

app = Dash("myapp")
builder = Builder("URL or filename")
dropdown = builder.get_component("test-dropdown")
app.layout = builder.layout
app.run(debug=True)
<div>
    <h1>Simple Dashboard</h1>
    <div class="content">
        <dcc.dropdown id="test-dropdown"/>
    </div>
</div>
Builder(*args, file: os.PathLike = None, **kwargs)
65    def __new__(cls, *args, file: PathLike = None, **kwargs) -> "Builder":
66        """
67        Instanciate a new LayoutBuilder object.
68
69        Every new object will start an autodetection.
70
71        See Also:
72            `Builder.autodetect`
73        """
74        instance = super().__new__(cls, *args, **kwargs)
75        cls._autodetect_components()
76        if file is not None:
77            instance.load(file)
78        return instance

Instanciate a new LayoutBuilder object.

Every new object will start an autodetection.

See Also: Builder.autodetect

layout: Optional[dash.development.base_component.Component] = None
@classmethod
def register_library(cls, path: str, prefix: Optional[str], replace: bool = False) -> bool:
127    @classmethod
128    def register_library(
129            cls, path: str, prefix: Optional[str], replace: bool = False
130    ) -> bool:
131        """
132        Register a new module providing Dash components.
133
134        A sensible default is already provided for known libraries,
135        but you can custom libraries with this method. You just have to
136        call this by passing a module path and a prefix to apply to detected
137        components:
138
139        ```python
140        from dash.htmlayout import Builder
141
142        Builder.register_library("dash_colorful_lib", "colorful")
143        ```
144        """
145        if path not in cls._module_registry or replace:
146            cls._module_registry[path] = prefix
147            cls._autodetect_components()
148            return True
149        return False

Register a new module providing Dash components.

A sensible default is already provided for known libraries, but you can custom libraries with this method. You just have to call this by passing a module path and a prefix to apply to detected components:

from dash.htmlayout import Builder

Builder.register_library("dash_colorful_lib", "colorful")
def load(self, path: os.PathLike) -> dash.development.base_component.Component:
151    def load(self, path: PathLike) -> Component:
152        """
153        Build a layout from an HTML file.
154
155        Returns:
156            The root of the layout tree with all its descendants.
157        """
158        parser = etree.XMLParser(
159            remove_comments=True, ns_clean=True, remove_pis=True,
160            resolve_entities=False, remove_blank_text=True)
161        root = etree.parse(path, parser=parser).getroot()
162        self._components = {}
163        print(etree.tostring(root))
164        if root:
165            self.layout = self._build_tree(root)
166        return self.layout

Build a layout from an HTML file.

Returns: The root of the layout tree with all its descendants.

def get_component( self, identifier: str) -> Optional[dash.development.base_component.Component]:
168    def get_component(self, identifier: str) -> Optional[Component]:
169        """
170        Get component instance in the loqded lqyout given an ID.
171
172        Args:
173            identifier: id given to the component.
174
175        Returns:
176            If found, the component with the given id.
177            `None` if the id is not found in the current layout.
178        """
179        return self._components.get(identifier)

Get component instance in the loqded lqyout given an ID.

Args: identifier: id given to the component.

Returns: If found, the component with the given id. None if the id is not found in the current layout.