Skip to content

Commit

Permalink
Preparing 1.0.13 release
Browse files Browse the repository at this point in the history
  • Loading branch information
emilhe committed Mar 5, 2024
1 parent ffeebc0 commit 57585b4
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 41 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

All notable changes to this project will be documented in this file.

## [1.0.13] - UNRELEASED
## [1.0.13] - 05-03-24

### Added

- Add new dynamic props concept (for pages)
- Add new `pages` module, which introduces the `page components` and `page properties` concepts
- Add new `validate` module, which adds an `assert_no_random_ids` that assets that Dash didn't generate any random component ids

## [1.0.12] - 04-02-23

Expand Down
82 changes: 49 additions & 33 deletions dash_extensions/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,38 @@
import dash
from collections import OrderedDict
from typing import Optional, Any
from dash import html, Input, Output, State, clientside_callback
from dash import html, Input, Output, State, clientside_callback, page_container
from dash.development.base_component import Component

"""
This module holds utilities related to the [Dash pages](https://dash.plotly.com/urls).
"""

_ID_CONTENT = "_component_content"
_COMPONENT_PATH_REGISTRY = OrderedDict()
_PROP_PATH_REGISTRY = OrderedDict()
_CONTAINER_REGISTRY = {}
_COMPONENT_CONTAINER = html.Div(id=_ID_CONTENT, disable_n_clicks=True)
_COMPONENT_PATH_REGISTRY: dict[Component, list[str]] = OrderedDict()
_PROP_PATH_REGISTRY: dict[Component, dict[str, list[str]]] = OrderedDict()
_CONTAINER_REGISTRY: dict[Component, Component] = {}
_COMPONENT_CONTAINER = html.Div(id=_ID_CONTENT, disable_n_clicks=True, style=dict(display="contents"))

# region Monkey patch page registration function

_original_register_page = dash.register_page


def _register_page(*args, dynamic_components=None, dynamic_props=None, **kwargs):
def _register_page(*args, page_components=None, page_properties=None, **kwargs):
_original_register_page(*args, **kwargs)
# Resolve page.
module = kwargs['module'] if 'module' in kwargs else args[0]
page = dash.page_registry[module]
# Register callbacks for dynamic props.
if dynamic_props is not None:
for component in dynamic_props:
set_props(component, page['path'], dynamic_props[component])
# Resolve any dynamic components.
if dynamic_components is None:
# Register callbacks for page props.
if page_properties is not None:
for component in page_properties:
_set_props(component, page['path'], page_properties[component])
# Resolve any page components.
if page_components is None:
return
for component in dynamic_components:
set_visible(component, page['path'])
for component in page_components:
_set_visible(component, page['path'])


dash.register_page = _register_page
Expand All @@ -43,21 +43,33 @@ def _register_page(*args, dynamic_components=None, dynamic_props=None, **kwargs)

# region Public interface

def set_page_container_style_display_contents():
"""
Changes the style of the page container (and the page content container) so that their children are rendered
as if they were children of the page container's parent (see https://caniuse.com/css-display-contents). This is
an advantage if you are using css grid, as it makes it possible to mix the page components with other components.
"""
page_container.style = dict(display="contents")
for child in page_container.children:
if child.id == "_pages_content":
child.style = dict(display="contents")


def set_default_container(container: Component):
"""
Per default, dynamic components are rendered into the '_COMPONENT_CONTAINER' declared above.
Per default, page components are rendered into the '_COMPONENT_CONTAINER' declared above.
Use this function to change the default container.
:param container: the container into which components will be rendered by default
:param container: the container into which page components will be rendered by default
:return: None
"""
_COMPONENT_CONTAINER = container


def assign_container(component: Component, container: Component):
"""
By default, dynamic components are rendered into the '_COMPONENT_CONTAINER' declared above. Call this function to
By default, page components are rendered into the '_COMPONENT_CONTAINER' declared above. Call this function to
specify that the component should be rendered in a different container.
:param component: the (dynamic) component in question
:param component: the (page) component in question
:param container: the container into which the component will be rendered
:return: None
"""
Expand All @@ -66,20 +78,20 @@ def assign_container(component: Component, container: Component):
_CONTAINER_REGISTRY[component] = container


def set_visible(component: Component, path: str):
def _set_visible(component: Component, path: str):
"""
Register path(s) for which a component should be visible.
:param component: the (dynamic) component in question
:param component: the (page) component in question
:param path: the (url) path for which the component should be visible
:return: None
"""
_COMPONENT_PATH_REGISTRY.setdefault(component, []).append(path)


def set_props(component: Component, path: str, prop_map: dict[str, Any]):
def _set_props(component: Component, path: str, prop_map: dict[str, Any]):
"""
Register path(s) for which a particular props should be set.
:param component: the (dynamic) component in question
:param component: the (page) component in question
:param path: the (url) path for which the props should be set
:param prop_map: the props, i.e. (prop name, prop value) pairs
:return: None
Expand All @@ -88,11 +100,11 @@ def set_props(component: Component, path: str, prop_map: dict[str, Any]):
_PROP_PATH_REGISTRY.setdefault(component, OrderedDict()).setdefault(prop, {})[path] = prop_map[prop]


def setup_dynamic_components() -> html.Div:
def setup_page_components() -> html.Div:
"""
Initializes the dynamic components and returns the (default) container into which the components are rendered.
:return: the default container, into which dynamic components are rendered. Should be included in the layout,
unless all (dynamic) components are assigned to custom containers (via 'assign_container')
Initializes the page components and returns the (default) container into which the components are rendered.
:return: the default container, into which page components are rendered. Must be included in the layout,
unless all (page) components are assigned to custom containers (via 'assign_container')
"""
_setup_callbacks()
return _COMPONENT_CONTAINER
Expand All @@ -115,24 +127,28 @@ def _prepare_container(container: Optional[Component] = None):
def _setup_callbacks():
store = dash.dash._ID_STORE
location = dash.dash._ID_LOCATION
# Setup callbacks for dynamic components.
# Setup callbacks for page components.
components = list(_COMPONENT_PATH_REGISTRY.keys())
for component in components:
# Wrap in div to ensure 'hidden' prop exists.
wrapper = html.Div(component, disable_n_clicks=True, hidden=True)
# Wrap in div container, so we can hide it.
cid = component._set_random_id()
wrapper = html.Div(component, disable_n_clicks=True, style=dict(display="none"), id=f"{cid}_wrapper")
# Add to container.
container = _prepare_container(_CONTAINER_REGISTRY.get(component, _COMPONENT_CONTAINER))
container.children.append(wrapper)
# Setup callback.
f = f"""function(y, x){{
const paths = {_COMPONENT_PATH_REGISTRY[component]};
return !paths.includes(x);
const paths = {_COMPONENT_PATH_REGISTRY[component]};
if(paths.includes(x)){{
return {{display: "contents"}};
}}
return {{display: "none"}};
}}"""
clientside_callback(f, Output(wrapper, "hidden", allow_duplicate=True),
clientside_callback(f, Output(wrapper, "style", allow_duplicate=True),
Input(store, "data"),
State(location, "pathname"),
prevent_initial_call='initial_duplicate')
# Setup callbacks for dynamic props.
# Setup callbacks for page props.
components = list(_PROP_PATH_REGISTRY.keys())
for component in components:
for prop in _PROP_PATH_REGISTRY[component]:
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-extensions",
"version": "1.0.8rc2",
"version": "1.0.13",
"description": "Extensions for Plotly Dash.",
"main": "build/index.js",
"scripts": {
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "dash-extensions"
version = "1.0.8rc2"
version = "1.0.13"
description = "Extensions for Plotly Dash."
authors = ["emher <[email protected]>"]
license = "MIT"
Expand All @@ -24,18 +24,18 @@ include = [

[tool.poetry.dependencies]
python = ">=3.8,<4"
dash = ">=2.9.3"
dash = ">=2.15.0"
more-itertools = "^9.0.0"
jsbeautifier = "^1.14.3"
Flask-Caching = "2.0.2"
Flask-Caching = "^2.1.0"
dash-mantine-components = {version = "^0.11.1", optional = true}
dataclass-wizard = "^0.22.2"

[tool.poetry.extras]
mantine = ["dash-mantine-components"]

[tool.poetry.dev-dependencies]
dash = {extras = ["dev", "testing"], version = "^2.8.1"}
dash = {extras = ["dev", "testing"], version = "^2.15.0"}
pytest-cov = "^4.0.0"
black = "^22.12.0"
pandas = ">=1.5.3"
Expand Down

0 comments on commit 57585b4

Please sign in to comment.