Repository: plotly/dash

Issue:
Render arbitrary keys of components inside dictionaries.
It should be possible to render arbitrary keys as component inside an object prop.

Example typescript props:
```ts
type Props = {
  dynamic: {[k: string]: JSX.Element}
}
```
prop-types: `PropTypes.objectOf(PropTypes.node)` 


Discussion:

Resolving patch:
diff --git a/dash/development/_collect_nodes.py b/dash/development/_collect_nodes.py
--- 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 ("[]", ".", "{}"))]


Test patch:
diff --git a/tests/integration/renderer/test_component_as_prop.py b/tests/integration/renderer/test_component_as_prop.py
--- 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
--- 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",
     ]