diff --git a/@plotly/dash-test-components/src/components/ExternalComponent.js b/@plotly/dash-test-components/src/components/ExternalComponent.js
index bb3369d87e..d82fbd93a0 100644
--- a/@plotly/dash-test-components/src/components/ExternalComponent.js
+++ b/@plotly/dash-test-components/src/components/ExternalComponent.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
-const ExternalComponent = ({ id, text, input_id }) => {
+const ExternalComponent = ({ id, text, input_id, extra_component }) => {
const ctx = window.dash_component_api.useDashContext();
const ExternalWrapper = window.dash_component_api.ExternalWrapper;
@@ -15,6 +15,14 @@ const ExternalComponent = ({ id, text, input_id }) => {
value={text}
componentPath={[...ctx.componentPath, 'external']}
/>
+ {
+ extra_component &&
+ }
)
}
@@ -23,6 +31,11 @@ ExternalComponent.propTypes = {
id: PropTypes.string,
text: PropTypes.string,
input_id: PropTypes.string,
+ extra_component: PropTypes.exact({
+ type: PropTypes.string,
+ namespace: PropTypes.string,
+ props: PropTypes.object,
+ }),
};
export default ExternalComponent;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45914dda5e..839cb2e4d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,19 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
+## [3.0.0-rc3] - 2025-02-21
+
+## Added
+
+- [#3121](https://github.com/plotly/dash/pull/3121) Restyle and add version checker to dev tools.
+- [#3175](https://github.com/plotly/dash/pull/3175) Add `custom_data` hook.
+- [#3175](https://github.com/plotly/dash/pull/3175) Improved error for removed Dash app attribute, run_server and long_callback
+- [#3175](https://github.com/plotly/dash/pull/3175) Expose `stringifyId` in `window.dash_component_api`.
+
+## Fixed
+
+- [#3175](https://github.com/plotly/dash/pull/3175) Fix `ExternalWrapper` rendering children and support pattern matching ids.
+
## [3.0.0-rc2] - 2025-02-18
## Added
diff --git a/dash/_callback_context.py b/dash/_callback_context.py
index 42e4c506d9..55d3cf0a49 100644
--- a/dash/_callback_context.py
+++ b/dash/_callback_context.py
@@ -304,6 +304,14 @@ def origin(self):
"""
return _get_from_context("origin", "")
+ @property
+ @has_context
+ def custom_data(self):
+ """
+ Custom data set by hooks.custom_data.
+ """
+ return _get_from_context("custom_data", {})
+
callback_context = CallbackContext()
diff --git a/dash/_dash_renderer.py b/dash/_dash_renderer.py
index 41d4e6452f..36588b30b5 100644
--- a/dash/_dash_renderer.py
+++ b/dash/_dash_renderer.py
@@ -1,6 +1,6 @@
import os
-__version__ = "2.0.1"
+__version__ = "2.0.2"
_available_react_versions = {"18.3.1", "18.2.0", "16.14.0"}
_available_reactdom_versions = {"18.3.1", "18.2.0", "16.14.0"}
@@ -64,7 +64,7 @@ def _set_react_version(v_react, v_reactdom=None):
{
"relative_package_path": "dash-renderer/build/dash_renderer.min.js",
"dev_package_path": "dash-renderer/build/dash_renderer.dev.js",
- "external_url": "https://unpkg.com/dash-renderer@2.0.1"
+ "external_url": "https://unpkg.com/dash-renderer@2.0.2"
"/build/dash_renderer.min.js",
"namespace": "dash",
},
diff --git a/dash/_hooks.py b/dash/_hooks.py
index 7c4b2389f0..1790aa3e3d 100644
--- a/dash/_hooks.py
+++ b/dash/_hooks.py
@@ -45,6 +45,7 @@ def __init__(self) -> None:
"error": [],
"callback": [],
"index": [],
+ "custom_data": [],
}
self._js_dist = []
self._css_dist = []
@@ -191,6 +192,28 @@ def wrap(func):
return wrap
+ def custom_data(
+ self, namespace: str, priority: _t.Optional[int] = None, final=False
+ ):
+ """
+ Add data to the callback_context.custom_data property under the namespace.
+
+ The hook function takes the current context_value and before the ctx is set
+ and has access to the flask request context.
+ """
+
+ def wrap(func: _t.Callable[[_t.Dict], _t.Any]):
+ self.add_hook(
+ "custom_data",
+ func,
+ priority=priority,
+ final=final,
+ data={"namespace": namespace},
+ )
+ return func
+
+ return wrap
+
hooks = _Hooks()
diff --git a/dash/_obsolete.py b/dash/_obsolete.py
new file mode 100644
index 0000000000..c2b682f8c9
--- /dev/null
+++ b/dash/_obsolete.py
@@ -0,0 +1,23 @@
+# pylint: disable=too-few-public-methods
+from .exceptions import ObsoleteAttributeException
+
+
+class ObsoleteAttribute:
+ def __init__(self, message: str, exc=ObsoleteAttributeException):
+ self.message = message
+ self.exc = exc
+
+
+class ObsoleteChecker:
+ _obsolete_attributes = {
+ "run_server": ObsoleteAttribute("app.run_server has been replaced by app.run"),
+ "long_callback": ObsoleteAttribute(
+ "app.long_callback has been removed, use app.callback(..., background=True) instead"
+ ),
+ }
+
+ def __getattr__(self, name: str):
+ if name in self._obsolete_attributes:
+ err = self._obsolete_attributes[name]
+ raise err.exc(err.message)
+ return getattr(self, name)
diff --git a/dash/dash-renderer/package-lock.json b/dash/dash-renderer/package-lock.json
index ed4d4819c4..a4821a1f19 100644
--- a/dash/dash-renderer/package-lock.json
+++ b/dash/dash-renderer/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "dash-renderer",
- "version": "2.0.1",
+ "version": "2.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "dash-renderer",
- "version": "2.0.1",
+ "version": "2.0.2",
"license": "MIT",
"dependencies": {
"@babel/polyfill": "^7.12.1",
diff --git a/dash/dash-renderer/package.json b/dash/dash-renderer/package.json
index 5ab97ae90c..2d481f8522 100644
--- a/dash/dash-renderer/package.json
+++ b/dash/dash-renderer/package.json
@@ -1,6 +1,6 @@
{
"name": "dash-renderer",
- "version": "2.0.1",
+ "version": "2.0.2",
"description": "render dash components in react",
"main": "build/dash_renderer.min.js",
"scripts": {
@@ -13,7 +13,7 @@
"build:dev": "webpack",
"build:local": "renderer build local",
"build": "renderer build && npm run prepublishOnly",
- "postbuild": "es-check es2018 ../deps/*.js build/*.js",
+ "postbuild": "es-check es2015 ../deps/*.js build/*.js",
"test": "karma start karma.conf.js --single-run",
"format": "run-s private::format.*",
"lint": "run-s private::lint.* --continue-on-error"
diff --git a/dash/dash-renderer/src/actions/index.js b/dash/dash-renderer/src/actions/index.js
index e46644ff9d..86649c2d7d 100644
--- a/dash/dash-renderer/src/actions/index.js
+++ b/dash/dash-renderer/src/actions/index.js
@@ -6,7 +6,7 @@ import {getAction} from './constants';
import cookie from 'cookie';
import {validateCallbacksToLayout} from './dependencies';
import {includeObservers, getLayoutCallbacks} from './dependencies_ts';
-import {getPath} from './paths';
+import {computePaths, getPath} from './paths';
export const onError = createAction(getAction('ON_ERROR'));
export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE'));
@@ -21,6 +21,14 @@ export const updateProps = createAction(getAction('ON_PROP_CHANGE'));
export const insertComponent = createAction(getAction('INSERT_COMPONENT'));
export const removeComponent = createAction(getAction('REMOVE_COMPONENT'));
+export const addComponentToLayout = payload => (dispatch, getState) => {
+ const {paths} = getState();
+ dispatch(insertComponent(payload));
+ dispatch(
+ setPaths(computePaths(payload.component, payload.componentPath, paths))
+ );
+};
+
export const dispatchError = dispatch => (message, lines) =>
dispatch(
onError({
diff --git a/dash/dash-renderer/src/dashApi.ts b/dash/dash-renderer/src/dashApi.ts
index a0a914c5e4..75365f731c 100644
--- a/dash/dash-renderer/src/dashApi.ts
+++ b/dash/dash-renderer/src/dashApi.ts
@@ -3,6 +3,7 @@ import {DashContext, useDashContext} from './wrapper/DashContext';
import {getPath} from './actions/paths';
import {getStores} from './utils/stores';
import ExternalWrapper from './wrapper/ExternalWrapper';
+import {stringifyId} from './actions/dependencies';
/**
* Get the dash props from a component path or id.
@@ -32,5 +33,6 @@ function getLayout(componentPathOrId: string[] | string): any {
ExternalWrapper,
DashContext,
useDashContext,
- getLayout
+ getLayout,
+ stringifyId
};
diff --git a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx
index 025bb2ea55..79bcbb5162 100644
--- a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx
+++ b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx
@@ -1,9 +1,14 @@
import React, {useState, useEffect} from 'react';
-import {useDispatch} from 'react-redux';
+import {batch, useDispatch} from 'react-redux';
import {DashLayoutPath} from '../types/component';
import DashWrapper from './DashWrapper';
-import {insertComponent, removeComponent} from '../actions';
+import {
+ addComponentToLayout,
+ notifyObservers,
+ removeComponent,
+ updateProps
+} from '../actions';
type Props = {
componentPath: DashLayoutPath;
@@ -21,18 +26,18 @@ function ExternalWrapper({
componentPath,
...props
}: Props) {
- const dispatch = useDispatch();
+ const dispatch: any = useDispatch();
const [inserted, setInserted] = useState(false);
useEffect(() => {
// Give empty props for the inserted components.
// The props will come from the parent so they can be updated.
dispatch(
- insertComponent({
+ addComponentToLayout({
component: {
type: componentType,
namespace: componentNamespace,
- props: {}
+ props: props
},
componentPath
})
@@ -43,10 +48,17 @@ function ExternalWrapper({
};
}, []);
+ useEffect(() => {
+ batch(() => {
+ dispatch(updateProps({itempath: componentPath, props}));
+ dispatch(notifyObservers({id: props.id, props}));
+ });
+ }, [props]);
+
if (!inserted) {
return null;
}
// Render a wrapper with the actual props.
- return ;
+ return ;
}
export default ExternalWrapper;
diff --git a/dash/dash.py b/dash/dash.py
index b7b5f18b35..d890bb49c6 100644
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -68,6 +68,7 @@
from . import _get_app
from ._grouping import map_grouping, grouping_len, update_args_group
+from ._obsolete import ObsoleteChecker
from . import _pages
from ._pages import (
@@ -207,7 +208,7 @@ def _do_skip(error):
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments, too-many-locals
-class Dash:
+class Dash(ObsoleteChecker):
"""Dash is a framework for building analytical web applications.
No JavaScript required.
@@ -1388,6 +1389,10 @@ def dispatch(self):
g.path = flask.request.full_path
g.remote = flask.request.remote_addr
g.origin = flask.request.origin
+ g.custom_data = AttributeDict({})
+
+ for hook in self._hooks.get_hooks("custom_data"):
+ g.custom_data[hook.data["namespace"]] = hook(g)
except KeyError as missing_callback_function:
msg = f"Callback function not found for output '{output}', perhaps you forgot to prepend the '@'?"
diff --git a/dash/exceptions.py b/dash/exceptions.py
index a971d2e5a3..00bd2c1553 100644
--- a/dash/exceptions.py
+++ b/dash/exceptions.py
@@ -10,6 +10,10 @@ class ObsoleteKwargException(DashException):
pass
+class ObsoleteAttributeException(DashException):
+ pass
+
+
class NoLayoutException(DashException):
pass
diff --git a/dash/version.py b/dash/version.py
index f7bbf541c6..0b609eb386 100644
--- a/dash/version.py
+++ b/dash/version.py
@@ -1 +1 @@
-__version__ = "3.0.0rc2"
+__version__ = "3.0.0rc3"
diff --git a/tests/integration/renderer/test_external_component.py b/tests/integration/renderer/test_external_component.py
index db095214a2..72657125f5 100644
--- a/tests/integration/renderer/test_external_component.py
+++ b/tests/integration/renderer/test_external_component.py
@@ -1,4 +1,4 @@
-from dash import Dash, html, dcc, html, Input, Output, State
+from dash import Dash, html, dcc, html, Input, Output, State, MATCH
from dash_test_components import ExternalComponent
@@ -8,7 +8,22 @@ def test_rext001_render_external_component(dash_duo):
[
dcc.Input(id="sync", value="synced"),
html.Button("sync", id="sync-btn"),
- ExternalComponent("ext", input_id="external", text="external"),
+ ExternalComponent(
+ id="ext",
+ input_id="external",
+ text="external",
+ extra_component={
+ "type": "Div",
+ "namespace": "dash_html_components",
+ "props": {
+ "id": "extra",
+ "children": [
+ html.Div("extra children", id={"type": "extra", "index": 1})
+ ],
+ },
+ },
+ ),
+ html.Div(html.Div(id={"type": "output", "index": 1}), id="out"),
]
)
@@ -21,7 +36,20 @@ def test_rext001_render_external_component(dash_duo):
def on_sync(_, value):
return value
+ @app.callback(
+ Output({"type": "output", "index": MATCH}, "children"),
+ Input({"type": "extra", "index": MATCH}, "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def click(*_):
+ return "clicked"
+
dash_duo.start_server(app)
dash_duo.wait_for_text_to_equal("#external", "external")
dash_duo.find_element("#sync-btn").click()
dash_duo.wait_for_text_to_equal("#external", "synced")
+
+ dash_duo.wait_for_text_to_equal("#extra", "extra children")
+
+ dash_duo.find_element("#extra > div").click()
+ dash_duo.wait_for_text_to_equal("#out", "clicked")
diff --git a/tests/integration/test_hooks.py b/tests/integration/test_hooks.py
index ca4143eadb..cbb1f44551 100644
--- a/tests/integration/test_hooks.py
+++ b/tests/integration/test_hooks.py
@@ -2,7 +2,7 @@
import requests
import pytest
-from dash import Dash, Input, Output, html, hooks, set_props
+from dash import Dash, Input, Output, html, hooks, set_props, ctx
@pytest.fixture
@@ -14,6 +14,7 @@ def hook_cleanup():
hooks._ns["error"] = []
hooks._ns["callback"] = []
hooks._ns["index"] = []
+ hooks._ns["custom_data"] = []
hooks._css_dist = []
hooks._js_dist = []
hooks._finals = {}
@@ -188,3 +189,24 @@ def test_hook009_hook_clientside_callback(hook_cleanup, dash_duo):
dash_duo.wait_for_element("#hook-start").click()
dash_duo.wait_for_text_to_equal("#hook-output", "Called 1")
+
+
+def test_hook010_hook_custom_data(hook_cleanup, dash_duo):
+ @hooks.custom_data("custom")
+ def custom_data(_):
+ return "custom-data"
+
+ app = Dash()
+ app.layout = [html.Button("insert", id="btn"), html.Div(id="output")]
+
+ @app.callback(
+ Output("output", "children"),
+ Input("btn", "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def cb(_):
+ return ctx.custom_data.custom
+
+ dash_duo.start_server(app)
+ dash_duo.wait_for_element("#btn").click()
+ dash_duo.wait_for_text_to_equal("#output", "custom-data")