From a9ddbaf9690b414f152106baeb90ee850c437356 Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Sun, 24 Sep 2023 15:46:59 +0200 Subject: [PATCH 1/4] Add dynamic components implementation --- dash_extensions/pages.py | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 dash_extensions/pages.py diff --git a/dash_extensions/pages.py b/dash_extensions/pages.py new file mode 100644 index 0000000..701de93 --- /dev/null +++ b/dash_extensions/pages.py @@ -0,0 +1,97 @@ +import dash +from collections import OrderedDict +from typing import Optional +from dash import html, Input, Output, clientside_callback +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" +_PATH_REGISTRY = OrderedDict() +_CONTAINER_REGISTRY = {} +_COMPONENT_CONTAINER = html.Div(id=_ID_CONTENT, disable_n_clicks=True) + +# region Monkey patch page registration function + +_original_register_page = dash.register_page + + +def _register_page(*args, components=None, **kwargs): + _original_register_page(*args, **kwargs) + if components is None: + return + module = kwargs['module'] if 'module' in kwargs else args[0] + page = dash.page_registry[module] + for component in components: + set_visible(component, page['path']) + + +dash.register_page = _register_page + + +# endregion + +# region Public interface + +def assign_container(component: Component, container: Component): + """ + By default, dynamic 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 container: the container into which the component will be rendered + :return: None + """ + if component in _CONTAINER_REGISTRY: + raise ValueError("You can only register a component once.") + _CONTAINER_REGISTRY[component] = container + + +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 path: the (url) path for which the component should be visible + :return: None + """ + _PATH_REGISTRY.setdefault(component, []).append(path) + + +def setup_dynamic_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 registered to custom containers (via 'register_component') + """ + _setup_callbacks() + return _COMPONENT_CONTAINER + + +# endregion + +# region Utils +def _prepare_container(container: Optional[Component] = None): + container = _COMPONENT_CONTAINER if container is None else container + # Make sure children is a list. + if container.children is None: + container.children = [] + if not isinstance(container.children, list): + container.children = [container.children] + return container + + +def _setup_callbacks(): + location = dash.dash._ID_LOCATION + components = list(_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) + # Add to container. + container = _prepare_container(_CONTAINER_REGISTRY.get(component, _COMPONENT_CONTAINER)) + container.children.append(wrapper) + # Setup callback. + f = f"function(x){{const paths = {_PATH_REGISTRY[component]}; return !paths.includes(x);}}" + clientside_callback(f, Output(wrapper, "hidden"), Input(location, "pathname")) + +# endregion From e48f5a6766b43a457093b3c7625b271a0a68403e Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Sun, 24 Sep 2023 16:12:09 +0200 Subject: [PATCH 2/4] Docstring update --- dash_extensions/pages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash_extensions/pages.py b/dash_extensions/pages.py index 701de93..887db46 100644 --- a/dash_extensions/pages.py +++ b/dash_extensions/pages.py @@ -44,7 +44,7 @@ def assign_container(component: Component, container: Component): :return: None """ if component in _CONTAINER_REGISTRY: - raise ValueError("You can only register a component once.") + raise ValueError("You can assign a component to one container.") _CONTAINER_REGISTRY[component] = container @@ -62,7 +62,7 @@ def setup_dynamic_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 registered to custom containers (via 'register_component') + unless all (dynamic) components are assigned to custom containers (via 'assign_container') """ _setup_callbacks() return _COMPONENT_CONTAINER From 840832ad7b1c6414107eb72e972dd4100daf78d5 Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Sun, 24 Sep 2023 16:13:10 +0200 Subject: [PATCH 3/4] Docstring update --- dash_extensions/pages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dash_extensions/pages.py b/dash_extensions/pages.py index 887db46..7d9c55c 100644 --- a/dash_extensions/pages.py +++ b/dash_extensions/pages.py @@ -71,6 +71,7 @@ def setup_dynamic_components() -> html.Div: # endregion # region Utils + def _prepare_container(container: Optional[Component] = None): container = _COMPONENT_CONTAINER if container is None else container # Make sure children is a list. From 0236a6f772f275e5b3cb25d8c3dc901346937ed2 Mon Sep 17 00:00:00 2001 From: Emil Haldrup Eriksen Date: Mon, 25 Sep 2023 13:09:56 +0200 Subject: [PATCH 4/4] Minor changes to dynamic components --- dash_extensions/pages.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dash_extensions/pages.py b/dash_extensions/pages.py index 7d9c55c..61370d7 100644 --- a/dash_extensions/pages.py +++ b/dash_extensions/pages.py @@ -18,13 +18,13 @@ _original_register_page = dash.register_page -def _register_page(*args, components=None, **kwargs): +def _register_page(*args, dynamic_components=None, **kwargs): _original_register_page(*args, **kwargs) - if components is None: + if dynamic_components is None: return module = kwargs['module'] if 'module' in kwargs else args[0] page = dash.page_registry[module] - for component in components: + for component in dynamic_components: set_visible(component, page['path']) @@ -83,6 +83,7 @@ def _prepare_container(container: Optional[Component] = None): def _setup_callbacks(): + store = dash.dash._ID_STORE location = dash.dash._ID_LOCATION components = list(_PATH_REGISTRY.keys()) for component in components: @@ -92,7 +93,9 @@ def _setup_callbacks(): container = _prepare_container(_CONTAINER_REGISTRY.get(component, _COMPONENT_CONTAINER)) container.children.append(wrapper) # Setup callback. - f = f"function(x){{const paths = {_PATH_REGISTRY[component]}; return !paths.includes(x);}}" - clientside_callback(f, Output(wrapper, "hidden"), Input(location, "pathname")) + f = f"function(y, x){{const paths = {_PATH_REGISTRY[component]}; return !paths.includes(x);}}" + clientside_callback(f, Output(wrapper, "hidden"), + Input(store, "data"), + State(location, "pathname")) # endregion