diff --git a/@plotly/dash-test-components/src/components/ComponentAsProp.js b/@plotly/dash-test-components/src/components/ComponentAsProp.js
index 52cf52d848..79ea4e5946 100644
--- a/@plotly/dash-test-components/src/components/ComponentAsProp.js
+++ b/@plotly/dash-test-components/src/components/ComponentAsProp.js
@@ -3,7 +3,17 @@ import PropTypes from 'prop-types';
const ComponentAsProp = (props) => {
- const { element, id, shapeEl, list_of_shapes, multi_components } = props;
+ const {
+ element,
+ id,
+ shapeEl,
+ list_of_shapes,
+ multi_components,
+ dynamic,
+ dynamic_list,
+ dynamic_dict,
+ dynamic_nested_list,
+ } = props;
return (
{shapeEl && shapeEl.header}
@@ -11,6 +21,18 @@ const ComponentAsProp = (props) => {
{shapeEl && shapeEl.footer}
{list_of_shapes &&
{list_of_shapes.map(e => - {e.label}
)}
}
{multi_components &&
{multi_components.map(m =>
{m.first} - {m.second}
)}
}
+ {
+ dynamic &&
{Object.keys(dynamic).map(key =>
{dynamic[key]}
)}
+ }
+ {
+ dynamic_dict && dynamic_dict.node &&
{Object.keys(dynamic_dict.node).map(key =>
{dynamic_dict.node[key]}
)}
+ }
+ {
+ dynamic_list &&
{dynamic_list.map((obj, i) => Object.keys(obj).map(key =>
{obj[key]}
))}
+ }
+ {
+ dynamic_nested_list &&
{dynamic_nested_list.map((e => <>{Object.values(e.obj)}>))}
+ }
)
}
@@ -37,7 +59,18 @@ ComponentAsProp.propTypes = {
first: PropTypes.node,
second: PropTypes.node,
})
- )
+ ),
+
+ dynamic: PropTypes.objectOf(PropTypes.node),
+
+ dynamic_list: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.node)),
+
+ dynamic_dict: PropTypes.shape({
+ node: PropTypes.objectOf(PropTypes.node),
+ }),
+ dynamic_nested_list: PropTypes.arrayOf(
+ PropTypes.shape({ obj: PropTypes.objectOf(PropTypes.node)})
+ ),
}
export default ComponentAsProp;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4600b8a068..6ac8ca9c53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## Fixed
- [#2508](https://github.com/plotly/dash/pull/2508) Fix error message, when callback output has different length than spec
+- [#2207](https://github.com/plotly/dash/pull/2207) Fix object of components support.
## [2.9.3] - 2023-04-13
diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js
index 47fb8ee0da..259fab801b 100644
--- a/dash/dash-renderer/src/TreeContainer.js
+++ b/dash/dash-renderer/src/TreeContainer.js
@@ -14,6 +14,7 @@ import {
has,
keys,
map,
+ mapObjIndexed,
mergeRight,
pick,
pickBy,
@@ -252,6 +253,14 @@ class BaseTreeContainer extends Component {
for (let i = 0; i < childrenProps.length; i++) {
const childrenProp = childrenProps[i];
+
+ const handleObject = (obj, opath) => {
+ return mapObjIndexed(
+ (node, k) => this.wrapChildrenProp(node, [...opath, k]),
+ obj
+ );
+ };
+
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
@@ -259,17 +268,33 @@ class BaseTreeContainer extends Component {
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
- found = false;
+ found = false,
+ hasObject = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
- frontPath.push(p.replace('[]', ''));
+ if (p.includes('{}')) {
+ hasObject = true;
+ frontPath.push(
+ p.replace('{}', '').replace('[]', '')
+ );
+ } else {
+ frontPath.push(p.replace('[]', ''));
+ }
+ } else if (p.includes('{}')) {
+ hasObject = true;
+ frontPath.push(p.replace('{}', ''));
} else {
frontPath.push(p);
}
} else {
- backPath.push(p);
+ if (p.includes('{}')) {
+ hasObject = true;
+ backPath.push(p.replace('{}', ''));
+ } else {
+ backPath.push(p);
+ }
}
});
@@ -281,38 +306,120 @@ class BaseTreeContainer extends Component {
if (!firstNode) {
continue;
}
+
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
- return assocPath(
- backPath,
- this.wrapChildrenProp(
+ let listValue;
+ if (hasObject) {
+ if (backPath.length) {
+ listValue = handleObject(
+ rpath(backPath, element),
+ elementPath
+ );
+ } else {
+ listValue = handleObject(element, elementPath);
+ }
+ } else {
+ listValue = this.wrapChildrenProp(
rpath(backPath, element),
elementPath
- ),
- element
- );
+ );
+ }
+ return assocPath(backPath, listValue, element);
});
path = frontPath;
} else {
- node = rpath(path, props);
- if (node === undefined) {
- continue;
+ if (childrenProp.includes('{}')) {
+ // Only supports one level of nesting.
+ const front = [];
+ let dynamic = [];
+ let hasBack = false;
+ const backPath = [];
+
+ for (let j = 0; j < path.length; j++) {
+ const cur = path[j];
+ if (cur.includes('{}')) {
+ dynamic = concat(front, [
+ cur.replace('{}', '')
+ ]);
+ if (j < path.length - 1) {
+ hasBack = true;
+ }
+ } else {
+ if (hasBack) {
+ backPath.push(cur);
+ } else {
+ front.push(cur);
+ }
+ }
+ }
+
+ const dynValue = rpath(dynamic, props);
+ if (dynValue !== undefined) {
+ nodeValue = mapObjIndexed(
+ (d, k) =>
+ this.wrapChildrenProp(
+ hasBack ? rpath(backPath, d) : d,
+ hasBack
+ ? concat(
+ dynamic,
+ concat([k], backPath)
+ )
+ : concat(dynamic, [k])
+ ),
+ dynValue
+ );
+ path = dynamic;
+ }
+ } else {
+ node = rpath(path, props);
+ if (node === undefined) {
+ continue;
+ }
+ nodeValue = this.wrapChildrenProp(node, path);
}
- nodeValue = this.wrapChildrenProp(node, path);
}
props = assocPath(path, nodeValue, props);
- continue;
- }
- const node = props[childrenProp];
- if (node !== undefined) {
- props = assoc(
- childrenProp,
- this.wrapChildrenProp(node, [childrenProp]),
- props
- );
+ } else {
+ if (childrenProp.includes('{}')) {
+ let opath = childrenProp.replace('{}', '');
+ const isArray = childrenProp.includes('[]');
+ if (isArray) {
+ opath = opath.replace('[]', '');
+ }
+ const node = props[opath];
+
+ if (node !== undefined) {
+ if (isArray) {
+ for (let j = 0; j < node.length; j++) {
+ const aPath = concat([opath], [j]);
+ props = assocPath(
+ aPath,
+ handleObject(node[j], aPath),
+ props
+ );
+ }
+ } else {
+ props = assoc(
+ opath,
+ handleObject(node, [opath]),
+ props
+ );
+ }
+ }
+ } else {
+ const node = props[childrenProp];
+ if (node !== undefined) {
+ props = assoc(
+ childrenProp,
+ this.wrapChildrenProp(node, [childrenProp]),
+ props
+ );
+ }
+ }
}
}
diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts
index 33426c7238..cb04852349 100644
--- a/dash/dash-renderer/src/actions/callbacks.ts
+++ b/dash/dash-renderer/src/actions/callbacks.ts
@@ -163,7 +163,7 @@ function fillVals(
inputList.map(({id, property, path: path_}: any) => ({
id,
property,
- value: (path(path_, layout) as any).props[property]
+ value: path([...path_, 'props', property], layout) as any
})),
specs[i],
cb.anyVals,
diff --git a/dash/dash-renderer/src/actions/utils.js b/dash/dash-renderer/src/actions/utils.js
index 621c3421e1..eb9f382adc 100644
--- a/dash/dash-renderer/src/actions/utils.js
+++ b/dash/dash-renderer/src/actions/utils.js
@@ -1,4 +1,14 @@
-import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';
+import {
+ append,
+ concat,
+ has,
+ path,
+ pathOr,
+ type,
+ findIndex,
+ includes,
+ slice
+} from 'ramda';
/*
* requests_pathname_prefix is the new config parameter introduced in
@@ -37,11 +47,44 @@ export const crawlLayout = (
// children array
object.forEach((child, i) => {
if (extraPath) {
- crawlLayout(
- rpath(extraPath, child),
- func,
- concat(currentPath, concat([i], extraPath))
- );
+ const objOf = findIndex(p => includes('{}', p), extraPath);
+ if (objOf !== -1) {
+ const front = slice(0, objOf, extraPath);
+ const back = slice(objOf, extraPath.length, extraPath);
+ if (front.length) {
+ crawlLayout(
+ path(front, child),
+ func,
+ concat(currentPath, concat([i], front)),
+ back
+ );
+ } else {
+ const backPath = back
+ .map(p => p.replace('{}', ''))
+ .filter(e => e);
+ let childObj,
+ childPath = concat([i], backPath);
+ if (backPath.length) {
+ childObj = path(backPath, child);
+ } else {
+ childObj = child;
+ }
+ for (const key in childObj) {
+ const value = childObj[key];
+ crawlLayout(
+ value,
+ func,
+ concat(currentPath, childPath.concat([key]))
+ );
+ }
+ }
+ } else {
+ crawlLayout(
+ path(extraPath, child),
+ func,
+ concat(currentPath, concat([i], extraPath))
+ );
+ }
} else {
crawlLayout(child, func, append(i, currentPath));
}
@@ -65,19 +108,61 @@ export const crawlLayout = (
let [frontPath, backPath] = childrenProp
.split('[]')
.map(p => p.split('.').filter(e => e));
+
const front = concat(['props'], frontPath);
const basePath = concat(currentPath, front);
crawlLayout(path(front, object), func, basePath, backPath);
} else {
- const newPath = concat(currentPath, [
- 'props',
- ...childrenProp.split('.')
- ]);
- crawlLayout(
- path(['props', ...childrenProp.split('.')], object),
- func,
- newPath
- );
+ if (childrenProp.includes('{}')) {
+ const opath = childrenProp.split('.');
+ const frontPath = [];
+ const backPath = [];
+ let found = false;
+
+ for (let i = 0; i < opath.length; i++) {
+ const curPath = opath[i];
+ if (!found && curPath.includes('{}')) {
+ found = true;
+ frontPath.push(curPath.replace('{}', ''));
+ } else {
+ if (found) {
+ backPath.push(curPath);
+ } else {
+ frontPath.push(curPath);
+ }
+ }
+ }
+ const newPath = concat(currentPath, [
+ 'props',
+ ...frontPath
+ ]);
+
+ const oValue = path(['props', ...frontPath], object);
+ if (oValue !== undefined) {
+ for (const key in oValue) {
+ const value = oValue[key];
+ if (backPath.length) {
+ crawlLayout(
+ path(backPath, value),
+ func,
+ concat(newPath, [key, ...backPath])
+ );
+ } else {
+ crawlLayout(value, func, [...newPath, key]);
+ }
+ }
+ }
+ } else {
+ const newPath = concat(currentPath, [
+ 'props',
+ ...childrenProp.split('.')
+ ]);
+ crawlLayout(
+ path(['props', ...childrenProp.split('.')], object),
+ func,
+ newPath
+ );
+ }
}
});
}
diff --git a/dash/development/_collect_nodes.py b/dash/development/_collect_nodes.py
index c2e04a69ef..da19df021d 100644
--- a/dash/development/_collect_nodes.py
+++ b/dash/development/_collect_nodes.py
@@ -14,6 +14,8 @@ def collect_array(a_value, base, nodes):
nodes = collect_nodes(a_value["value"], base + "[]", nodes)
elif a_type == "union":
nodes = collect_union(a_value["value"], base + "[]", nodes)
+ elif a_type == "objectOf":
+ nodes = collect_object(a_value["value"], base + "[]", nodes)
return nodes
@@ -25,6 +27,22 @@ def collect_union(type_list, base, nodes):
nodes = collect_nodes(t["value"], base, nodes)
elif t["name"] == "arrayOf":
nodes = collect_array(t["value"], base, nodes)
+ elif t["name"] == "objectOf":
+ nodes = collect_object(t["value"], base, nodes)
+ return nodes
+
+
+def collect_object(o_value, base, nodes):
+ o_name = o_value.get("name")
+ o_key = base + "{}"
+ if is_node(o_name):
+ nodes.append(o_key)
+ elif is_shape(o_name):
+ nodes = collect_nodes(o_value.get("value", {}), o_key, nodes)
+ elif o_name == "union":
+ nodes = collect_union(o_value.get("value"), o_key, nodes)
+ elif o_name == "arrayOf":
+ nodes = collect_array(o_value, o_key, nodes)
return nodes
@@ -49,9 +67,12 @@ def collect_nodes(metadata, base="", nodes=None):
nodes = collect_nodes(t_value["value"], key, nodes)
elif p_type == "union":
nodes = collect_union(t_value["value"], key, nodes)
+ elif p_type == "objectOf":
+ o_value = t_value.get("value", {})
+ nodes = collect_object(o_value, key, nodes)
return nodes
def filter_base_nodes(nodes):
- return [n for n in nodes if not any(e in n for e in ("[]", "."))]
+ return [n for n in nodes if not any(e in n for e in ("[]", ".", "{}"))]
diff --git a/tests/integration/renderer/test_component_as_prop.py b/tests/integration/renderer/test_component_as_prop.py
index 9abb3a813b..b1f3324cb4 100644
--- a/tests/integration/renderer/test_component_as_prop.py
+++ b/tests/integration/renderer/test_component_as_prop.py
@@ -121,6 +121,38 @@ def test_rdcap001_component_as_prop(dash_duo):
},
],
),
+ ComponentAsProp(
+ dynamic={
+ "inside-dynamic": Div("dynamic", "inside-dynamic"),
+ "output-dynamic": Div(id="output-dynamic"),
+ "clicker": Button("click-dynamic", id="click-dynamic"),
+ "clicker-dict": Button("click-dict", id="click-dict"),
+ "clicker-list": Button("click-list", id="click-list"),
+ "clicker-nested": Button("click-nested", id="click-nested"),
+ },
+ dynamic_dict={
+ "node": {
+ "dict-dyn": Div("dict-dyn", id="inside-dict"),
+ "dict-2": Div("dict-2", id="inside-dict-2"),
+ }
+ },
+ dynamic_list=[
+ {
+ "list": Div("dynamic-list", id="inside-list"),
+ "list-2": Div("list-2", id="inside-list-2"),
+ },
+ {"list-3": Div("list-3", id="inside-list-3")},
+ ],
+ dynamic_nested_list=[
+ {"obj": {"nested": Div("nested", id="nested-dyn")}},
+ {
+ "obj": {
+ "nested": Div("nested-2", id="nested-2"),
+ "nested-again": Div("nested-again", id="nested-again"),
+ },
+ },
+ ],
+ ),
]
)
@@ -171,6 +203,38 @@ def send_to_list_of_dict(n_clicks):
def updated_from_list(*_):
return callback_context.triggered[0]["prop_id"]
+ @app.callback(
+ Output("output-dynamic", "children"),
+ Input("click-dynamic", "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def on_click(n_clicks):
+ return f"Clicked {n_clicks}"
+
+ @app.callback(
+ Output("inside-dict", "children"),
+ Input("click-dict", "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def on_click(n_clicks):
+ return f"Clicked {n_clicks}"
+
+ @app.callback(
+ Output("inside-list", "children"),
+ Input("click-list", "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def on_click(n_clicks):
+ return f"Clicked {n_clicks}"
+
+ @app.callback(
+ Output("nested-dyn", "children"),
+ Input("click-nested", "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def on_click(n_clicks):
+ return f"Clicked {n_clicks}"
+
dash_duo.start_server(app)
assert dash_duo.get_logs() == []
@@ -221,6 +285,28 @@ def updated_from_list(*_):
dash_duo.wait_for_text_to_equal("#multi", "first - second")
dash_duo.wait_for_text_to_equal("#multi2", "foo - bar")
+ dash_duo.wait_for_text_to_equal("#inside-dynamic", "dynamic")
+ dash_duo.wait_for_text_to_equal("#dict-dyn", "dict-dyn")
+ dash_duo.wait_for_text_to_equal("#inside-dict-2", "dict-2")
+ dash_duo.wait_for_text_to_equal("#nested-2", "nested-2")
+ dash_duo.wait_for_text_to_equal("#nested-again", "nested-again")
+
+ dash_duo.wait_for_text_to_equal("#inside-list", "dynamic-list")
+ dash_duo.wait_for_text_to_equal("#inside-list-2", "list-2")
+ dash_duo.wait_for_text_to_equal("#inside-list-3", "list-3")
+
+ dash_duo.find_element("#click-dynamic").click()
+ dash_duo.wait_for_text_to_equal("#output-dynamic", "Clicked 1")
+
+ dash_duo.find_element("#click-dict").click()
+ dash_duo.wait_for_text_to_equal("#inside-dict", "Clicked 1")
+
+ dash_duo.find_element("#click-list").click()
+ dash_duo.wait_for_text_to_equal("#inside-list", "Clicked 1")
+
+ dash_duo.find_element("#click-nested").click()
+ dash_duo.wait_for_text_to_equal("#nested-dyn", "Clicked 1")
+
assert dash_duo.get_logs() == []
diff --git a/tests/unit/development/test_collect_nodes.py b/tests/unit/development/test_collect_nodes.py
index 294e9a7a95..0ac4c119f3 100644
--- a/tests/unit/development/test_collect_nodes.py
+++ b/tests/unit/development/test_collect_nodes.py
@@ -61,6 +61,34 @@
},
}
},
+ "dynamic": {
+ "type": {"name": "objectOf", "value": {"name": "node"}},
+ },
+ "dynamic_list": {
+ "type": {
+ "name": "arrayOf",
+ "value": {"name": "objectOf", "value": {"name": "node"}},
+ }
+ },
+ "dynamic_node_in_dict": {
+ "type": {
+ "name": "shape",
+ "value": {"a": {"name": "objectOf", "value": {"name": "node"}}},
+ }
+ },
+ "dynamic_in_object": {
+ "type": {
+ "name": "objectOf",
+ "value": {
+ "name": "shape",
+ "value": {
+ "a": {
+ "name": "node",
+ }
+ },
+ },
+ }
+ },
}
@@ -76,6 +104,10 @@ def test_dcn001_collect_nodes():
"mixed",
"direct",
"nested_list.list[].component",
+ "dynamic{}",
+ "dynamic_list[]{}",
+ "dynamic_node_in_dict.a{}",
+ "dynamic_in_object{}.a",
]