From 23d6803cf2961a01176dbb46140a4b03b21588cc Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 24 Aug 2022 12:35:13 -0400 Subject: [PATCH 01/10] Collect objectOf nodes. --- dash/development/_collect_nodes.py | 23 +++++++++++++- tests/unit/development/test_collect_nodes.py | 32 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) 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/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", ] From 2eb3ddb2ce31111a0b234d186bdf728576903845 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 26 Aug 2022 15:41:54 -0400 Subject: [PATCH 02/10] Add object of component as props test cases. --- .../src/components/ComponentAsProp.js | 30 ++++++++++++++-- .../renderer/test_component_as_prop.py | 34 ++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/@plotly/dash-test-components/src/components/ComponentAsProp.js b/@plotly/dash-test-components/src/components/ComponentAsProp.js index 52cf52d848..35d6ea9447 100644 --- a/@plotly/dash-test-components/src/components/ComponentAsProp.js +++ b/@plotly/dash-test-components/src/components/ComponentAsProp.js @@ -3,7 +3,16 @@ 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, + } = props; return (
{shapeEl && shapeEl.header} @@ -11,6 +20,15 @@ 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]}
))}
+ }
) } @@ -37,7 +55,15 @@ 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), + }) } export default ComponentAsProp; diff --git a/tests/integration/renderer/test_component_as_prop.py b/tests/integration/renderer/test_component_as_prop.py index 08b279fa94..e0c171c532 100644 --- a/tests/integration/renderer/test_component_as_prop.py +++ b/tests/integration/renderer/test_component_as_prop.py @@ -102,7 +102,22 @@ def test_rdcap001_component_as_prop(dash_duo): "id": "multi2", "first": Span("foo"), "second": Span("bar"), - } + }, + ], + ), + ComponentAsProp(id="dynamic", dynamic={"inside-dynamic": Div("dynamic")}), + ComponentAsProp( + id="dynamic-dict", dynamic_dict={"node": {"dict-dyn": Div("dict-dyn")}} + ), + ComponentAsProp( + dynamic={ + "output-dynamic": Div(id="output-dynamic"), + "dyn-clicker": Button("click", id="click-dynamic"), + }, + ), + ComponentAsProp( + dynamic_list=[ + {"dyn-list": Div("dynamic-list")}, ], ), ] @@ -155,6 +170,14 @@ 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}" + dash_duo.start_server(app) assert dash_duo.get_logs() == [] @@ -205,4 +228,13 @@ 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("#dyn-list", "dynamic-list") + + dash_duo.find_element("#click-dynamic").click() + + dash_duo.wait_for_text_to_equal("#dynamic-output", "Clicked 1") + assert dash_duo.get_logs() == [] From 0b9c04004c70a0933cdd62ac03e7c430de210955 Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 26 Aug 2022 15:43:41 -0400 Subject: [PATCH 03/10] support objectOf component type. --- dash/dash-renderer/src/TreeContainer.js | 151 +++++++++++++++--- dash/dash-renderer/src/actions/callbacks.ts | 2 +- dash/dash-renderer/src/actions/utils.js | 69 ++++++-- .../renderer/test_component_as_prop.py | 2 +- 4 files changed, 187 insertions(+), 37 deletions(-) diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js index 04864c9db3..7882ef1526 100644 --- a/dash/dash-renderer/src/TreeContainer.js +++ b/dash/dash-renderer/src/TreeContainer.js @@ -20,7 +20,8 @@ import { propOr, path as rpath, pathOr, - type + type, + toPairs } from 'ramda'; import {notifyObservers, updateProps} from './actions'; import isSimpleComponent from './isSimpleComponent'; @@ -249,6 +250,17 @@ class BaseTreeContainer extends Component { for (let i = 0; i < childrenProps.length; i++) { const childrenProp = childrenProps[i]; + + const handleObject = (obj, opath) => { + return keys(obj).reduce((acc, k) => { + const node = acc[k]; + return { + ...acc, + [k]: this.wrapChildrenProp(node, [...opath, k]) + }; + }, obj); + }; + if (childrenProp.includes('.')) { let path = childrenProp.split('.'); let node; @@ -256,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); + } } }); @@ -278,38 +306,115 @@ 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) { + 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 = toPairs(dynValue).reduce( + (acc, [k, d]) => ({ + ...acc, + [k]: this.wrapChildrenProp( + hasBack ? rpath(backPath, d) : d, + hasBack + ? concat( + dynamic, + concat([k], backPath) + ) + : concat(dynamic, [k]) + ) + }), + {} + ); + 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 63f4f7b633..964c1df321 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -160,7 +160,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..e5eee177fd 100644 --- a/dash/dash-renderer/src/actions/utils.js +++ b/dash/dash-renderer/src/actions/utils.js @@ -65,19 +65,64 @@ 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); + if (childrenProp.includes('{}')) { + // TODO + } else { + 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) { + Object.keys(oValue).forEach(key => { + 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/tests/integration/renderer/test_component_as_prop.py b/tests/integration/renderer/test_component_as_prop.py index e0c171c532..0cc47110d0 100644 --- a/tests/integration/renderer/test_component_as_prop.py +++ b/tests/integration/renderer/test_component_as_prop.py @@ -235,6 +235,6 @@ def on_click(n_clicks): dash_duo.find_element("#click-dynamic").click() - dash_duo.wait_for_text_to_equal("#dynamic-output", "Clicked 1") + dash_duo.wait_for_text_to_equal("#output-dynamic", "Clicked 1") assert dash_duo.get_logs() == [] From dc685e96e7fed639c29ef1e56b262bb196ba5b5e Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 25 Apr 2023 15:20:08 -0400 Subject: [PATCH 04/10] Handle object of components inside of lists. --- .../src/components/ComponentAsProp.js | 9 ++- dash/dash-renderer/src/TreeContainer.js | 9 ++- dash/dash-renderer/src/actions/utils.js | 67 +++++++++++++++---- .../renderer/test_component_as_prop.py | 54 ++++++++++++--- 4 files changed, 114 insertions(+), 25 deletions(-) diff --git a/@plotly/dash-test-components/src/components/ComponentAsProp.js b/@plotly/dash-test-components/src/components/ComponentAsProp.js index 35d6ea9447..79ea4e5946 100644 --- a/@plotly/dash-test-components/src/components/ComponentAsProp.js +++ b/@plotly/dash-test-components/src/components/ComponentAsProp.js @@ -12,6 +12,7 @@ const ComponentAsProp = (props) => { dynamic, dynamic_list, dynamic_dict, + dynamic_nested_list, } = props; return (
@@ -29,6 +30,9 @@ const ComponentAsProp = (props) => { { 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)}))}
+ }
) } @@ -63,7 +67,10 @@ ComponentAsProp.propTypes = { 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/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js index df55ed2b87..8e3bdb0190 100644 --- a/dash/dash-renderer/src/TreeContainer.js +++ b/dash/dash-renderer/src/TreeContainer.js @@ -317,7 +317,14 @@ class BaseTreeContainer extends Component { ); let listValue; if (hasObject) { - listValue = handleObject(element, elementPath); + if (backPath.length) { + listValue = handleObject( + rpath(backPath, element), + elementPath + ); + } else { + listValue = handleObject(element, elementPath); + } } else { listValue = this.wrapChildrenProp( rpath(backPath, element), diff --git a/dash/dash-renderer/src/actions/utils.js b/dash/dash-renderer/src/actions/utils.js index e5eee177fd..1eaa899160 100644 --- a/dash/dash-renderer/src/actions/utils.js +++ b/dash/dash-renderer/src/actions/utils.js @@ -1,4 +1,15 @@ -import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda'; +import { + append, + concat, + has, + path, + pathOr, + type, + path as rpath, + findIndex, + includes, + slice +} from 'ramda'; /* * requests_pathname_prefix is the new config parameter introduced in @@ -37,11 +48,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( + rpath(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 = rpath(backPath, child); + } else { + childObj = child; + } + Object.keys(childObj).forEach(key => { + const value = childObj[key]; + crawlLayout( + value, + func, + concat(currentPath, childPath.concat([key])) + ); + }); + } + } else { + crawlLayout( + rpath(extraPath, child), + func, + concat(currentPath, concat([i], extraPath)) + ); + } } else { crawlLayout(child, func, append(i, currentPath)); } @@ -65,13 +109,10 @@ export const crawlLayout = ( let [frontPath, backPath] = childrenProp .split('[]') .map(p => p.split('.').filter(e => e)); - if (childrenProp.includes('{}')) { - // TODO - } else { - const front = concat(['props'], frontPath); - const basePath = concat(currentPath, front); - crawlLayout(path(front, object), func, basePath, backPath); - } + + const front = concat(['props'], frontPath); + const basePath = concat(currentPath, front); + crawlLayout(path(front, object), func, basePath, backPath); } else { if (childrenProp.includes('{}')) { const opath = childrenProp.split('.'); diff --git a/tests/integration/renderer/test_component_as_prop.py b/tests/integration/renderer/test_component_as_prop.py index cdd48955da..de1d6c4085 100644 --- a/tests/integration/renderer/test_component_as_prop.py +++ b/tests/integration/renderer/test_component_as_prop.py @@ -121,19 +121,21 @@ def test_rdcap001_component_as_prop(dash_duo): }, ], ), - ComponentAsProp(id="dynamic", dynamic={"inside-dynamic": Div("dynamic")}), - ComponentAsProp( - id="dynamic-dict", dynamic_dict={"node": {"dict-dyn": Div("dict-dyn")}} - ), ComponentAsProp( dynamic={ + "inside-dynamic": Div("dynamic", "inside-dynamic"), "output-dynamic": Div(id="output-dynamic"), - "dyn-clicker": Button("click", id="click-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"), }, - ), - ComponentAsProp( + dynamic_dict={"node": {"dict-dyn": Div("dict-dyn", id="inside-dict")}}, dynamic_list=[ - {"dyn-list": Div("dynamic-list")}, + {"list": Div("dynamic-list", id="inside-list")}, + ], + dynamic_nested_list=[ + {"obj": {"nested": Div("nested", id="nested-dyn")}} ], ), ] @@ -194,6 +196,30 @@ def updated_from_list(*_): 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() == [] @@ -247,12 +273,20 @@ def on_click(n_clicks): 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("#dyn-list", "dynamic-list") + dash_duo.wait_for_text_to_equal("#inside-list", "dynamic-list") 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() == [] From a2cf8b0abff5fa27c787c4837e34fa35c52c12a9 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 26 Apr 2023 14:03:11 -0400 Subject: [PATCH 05/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 From b9e803070a6b85e158bfffebddede036ea2aff72 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 26 Apr 2023 14:54:09 -0400 Subject: [PATCH 06/10] Refactor keys reduce to ramda mapObjIndexed --- dash/dash-renderer/src/TreeContainer.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js index 8e3bdb0190..f67e3870ff 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, @@ -255,13 +256,10 @@ class BaseTreeContainer extends Component { const childrenProp = childrenProps[i]; const handleObject = (obj, opath) => { - return keys(obj).reduce((acc, k) => { - const node = acc[k]; - return { - ...acc, - [k]: this.wrapChildrenProp(node, [...opath, k]) - }; - }, obj); + return mapObjIndexed( + (node, k) => this.wrapChildrenProp(node, [...opath, k]), + obj + ); }; if (childrenProp.includes('.')) { From 16d9d2959ff171fa9181baf1f183188676e1eed0 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 26 Apr 2023 15:06:08 -0400 Subject: [PATCH 07/10] More mapObjIndexed. --- dash/dash-renderer/src/TreeContainer.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js index f67e3870ff..259fab801b 100644 --- a/dash/dash-renderer/src/TreeContainer.js +++ b/dash/dash-renderer/src/TreeContainer.js @@ -21,8 +21,7 @@ import { propOr, path as rpath, pathOr, - type, - toPairs + type } from 'ramda'; import {notifyObservers, updateProps} from './actions'; import isSimpleComponent from './isSimpleComponent'; @@ -360,10 +359,9 @@ class BaseTreeContainer extends Component { const dynValue = rpath(dynamic, props); if (dynValue !== undefined) { - nodeValue = toPairs(dynValue).reduce( - (acc, [k, d]) => ({ - ...acc, - [k]: this.wrapChildrenProp( + nodeValue = mapObjIndexed( + (d, k) => + this.wrapChildrenProp( hasBack ? rpath(backPath, d) : d, hasBack ? concat( @@ -371,9 +369,8 @@ class BaseTreeContainer extends Component { concat([k], backPath) ) : concat(dynamic, [k]) - ) - }), - {} + ), + dynValue ); path = dynamic; } From b2624f08c42f27339b529c56ded4c9ce718f07d5 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 26 Apr 2023 15:09:21 -0400 Subject: [PATCH 08/10] Remove extra path import. --- dash/dash-renderer/src/actions/utils.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dash/dash-renderer/src/actions/utils.js b/dash/dash-renderer/src/actions/utils.js index 1eaa899160..bf67d3e024 100644 --- a/dash/dash-renderer/src/actions/utils.js +++ b/dash/dash-renderer/src/actions/utils.js @@ -5,7 +5,6 @@ import { path, pathOr, type, - path as rpath, findIndex, includes, slice @@ -54,7 +53,7 @@ export const crawlLayout = ( const back = slice(objOf, extraPath.length, extraPath); if (front.length) { crawlLayout( - rpath(front, child), + path(front, child), func, concat(currentPath, concat([i], front)), back @@ -66,7 +65,7 @@ export const crawlLayout = ( let childObj, childPath = concat([i], backPath); if (backPath.length) { - childObj = rpath(backPath, child); + childObj = path(backPath, child); } else { childObj = child; } @@ -81,7 +80,7 @@ export const crawlLayout = ( } } else { crawlLayout( - rpath(extraPath, child), + path(extraPath, child), func, concat(currentPath, concat([i], extraPath)) ); From 50f76b634938410b87abd30ebda84525916fa1cf Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 26 Apr 2023 15:41:41 -0400 Subject: [PATCH 09/10] Refactor keys forEach with for in. --- dash/dash-renderer/src/actions/utils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dash/dash-renderer/src/actions/utils.js b/dash/dash-renderer/src/actions/utils.js index bf67d3e024..eb9f382adc 100644 --- a/dash/dash-renderer/src/actions/utils.js +++ b/dash/dash-renderer/src/actions/utils.js @@ -69,14 +69,14 @@ export const crawlLayout = ( } else { childObj = child; } - Object.keys(childObj).forEach(key => { + for (const key in childObj) { const value = childObj[key]; crawlLayout( value, func, concat(currentPath, childPath.concat([key])) ); - }); + } } } else { crawlLayout( @@ -139,7 +139,7 @@ export const crawlLayout = ( const oValue = path(['props', ...frontPath], object); if (oValue !== undefined) { - Object.keys(oValue).forEach(key => { + for (const key in oValue) { const value = oValue[key]; if (backPath.length) { crawlLayout( @@ -150,7 +150,7 @@ export const crawlLayout = ( } else { crawlLayout(value, func, [...newPath, key]); } - }); + } } } else { const newPath = concat(currentPath, [ From cef7bc736867cec9dcb5ee1021fd1b01f686a3e8 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 26 Apr 2023 15:52:54 -0400 Subject: [PATCH 10/10] Add more test cases. --- .../renderer/test_component_as_prop.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/integration/renderer/test_component_as_prop.py b/tests/integration/renderer/test_component_as_prop.py index de1d6c4085..b1f3324cb4 100644 --- a/tests/integration/renderer/test_component_as_prop.py +++ b/tests/integration/renderer/test_component_as_prop.py @@ -130,12 +130,27 @@ def test_rdcap001_component_as_prop(dash_duo): "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")}}, + 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": 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", id="nested-dyn")}}, + { + "obj": { + "nested": Div("nested-2", id="nested-2"), + "nested-again": Div("nested-again", id="nested-again"), + }, + }, ], ), ] @@ -272,8 +287,13 @@ def on_click(n_clicks): 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")