From 57585b4b5d6d96277cef4223067422ae78337235 Mon Sep 17 00:00:00 2001 From: emilhe Date: Tue, 5 Mar 2024 15:43:04 +0100 Subject: [PATCH] Preparing 1.0.13 release --- CHANGELOG.md | 5 ++- dash_extensions/pages.py | 82 ++++++++++++++++++++++++---------------- package-lock.json | 2 +- package.json | 2 +- pyproject.toml | 8 ++-- 5 files changed, 58 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d30b58..fcaad35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/dash_extensions/pages.py b/dash_extensions/pages.py index af4637c..6407054 100644 --- a/dash_extensions/pages.py +++ b/dash_extensions/pages.py @@ -2,7 +2,7 @@ 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 """ @@ -10,30 +10,30 @@ """ _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 @@ -43,11 +43,23 @@ 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 @@ -55,9 +67,9 @@ def set_default_container(container: Component): 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 """ @@ -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 @@ -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 @@ -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]: diff --git a/package-lock.json b/package-lock.json index 2360644..ef6a443 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dash-extensions", - "version": "1.0.8rc2", + "version": "1.0.13", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 5a4efee..c5e92bd 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pyproject.toml b/pyproject.toml index c33374e..a254fa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dash-extensions" -version = "1.0.8rc2" +version = "1.0.13" description = "Extensions for Plotly Dash." authors = ["emher "] license = "MIT" @@ -24,10 +24,10 @@ 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" @@ -35,7 +35,7 @@ dataclass-wizard = "^0.22.2" 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"