htmlayout
Dash HTMLayout
This package provides the ability to build dashboard layouts in Dash by writing HTML documents.
Installation
The package is published on
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")
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>
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
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")
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.
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.