From d91fe7c0f1225737cef46c64709a081ee0e27cdc Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:55:19 -0400 Subject: [PATCH 1/6] adds flag to `ExternalWrapper` to identify if the component is a temporary insertion --- dash/dash-renderer/src/wrapper/ExternalWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx index a6b1dad13f..5bb26fef73 100644 --- a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx @@ -18,7 +18,7 @@ type Props = { /** * For rendering components that are out of the regular layout tree. */ -function ExternalWrapper({component, componentPath}: Props) { +function ExternalWrapper({component, componentPath}: Props, temp?: boolean = false) { const dispatch: any = useDispatch(); const [inserted, setInserted] = useState(false); @@ -33,7 +33,7 @@ function ExternalWrapper({component, componentPath}: Props) { ); setInserted(true); return () => { - dispatch(removeComponent({componentPath})); + if (temp) dispatch(removeComponent({componentPath})); }; }, []); From 249dfc4e4ae602bf3eb6c3fca347746eb9e92b71 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 2 Jun 2025 13:12:51 -0400 Subject: [PATCH 2/6] fixing function and lint --- dash/dash-renderer/src/wrapper/ExternalWrapper.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx index 5bb26fef73..807a8e7040 100644 --- a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx @@ -18,7 +18,11 @@ type Props = { /** * For rendering components that are out of the regular layout tree. */ -function ExternalWrapper({component, componentPath}: Props, temp?: boolean = false) { +function ExternalWrapper({ + component, + componentPath, + temp = false +}: Props & {temp?: boolean}) { const dispatch: any = useDispatch(); const [inserted, setInserted] = useState(false); @@ -33,7 +37,9 @@ function ExternalWrapper({component, componentPath}: Props, temp?: boolean = fal ); setInserted(true); return () => { - if (temp) dispatch(removeComponent({componentPath})); + if (temp) { + dispatch(removeComponent({componentPath})); + } }; }, []); From 9dd243d2e35bca822fc380ef14c950d6a34bb725 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:16:05 -0400 Subject: [PATCH 3/6] updating definition for temp --- dash/dash-renderer/src/wrapper/ExternalWrapper.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx index 807a8e7040..bdf7848318 100644 --- a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx @@ -13,6 +13,7 @@ import { type Props = { component: DashComponent; componentPath: DashLayoutPath; + temp?: boolean; // If true, the component will be removed on unmount. }; /** @@ -22,7 +23,7 @@ function ExternalWrapper({ component, componentPath, temp = false -}: Props & {temp?: boolean}) { +}: Props) { const dispatch: any = useDispatch(); const [inserted, setInserted] = useState(false); From 0988888bf4b415b038ecc16b50fe63685107f29a Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:32:24 -0400 Subject: [PATCH 4/6] adding test for temp component with `ExternalWrapper` --- .../src/components/ExternalComponent.js | 10 ++-- .../renderer/test_external_component.py | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/@plotly/dash-test-components/src/components/ExternalComponent.js b/@plotly/dash-test-components/src/components/ExternalComponent.js index 64d2efc515..8c592e33de 100644 --- a/@plotly/dash-test-components/src/components/ExternalComponent.js +++ b/@plotly/dash-test-components/src/components/ExternalComponent.js @@ -2,10 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -const ExternalComponent = ({ id, text, input_id, extra_component }) => { +const ExternalComponent = ({ id, text, input_id, extra_component, extra_component_temp }) => { const ctx = window.dash_component_api.useDashContext(); const ExternalWrapper = window.dash_component_api.ExternalWrapper; - return (
{text && { id: input_id } }} - componentPath={[...ctx.componentPath, 'external']} + componentPath={[JSON.stringify(ctx.componentPath), 'text']} + temp={true} />} { extra_component && }
) @@ -39,6 +40,7 @@ ExternalComponent.propTypes = { namespace: PropTypes.string, props: PropTypes.object, }), + extra_component_temp: PropTypes.bool, }; export default ExternalComponent; diff --git a/tests/integration/renderer/test_external_component.py b/tests/integration/renderer/test_external_component.py index c0ab76fcc3..7ce7e8fdc2 100644 --- a/tests/integration/renderer/test_external_component.py +++ b/tests/integration/renderer/test_external_component.py @@ -59,3 +59,51 @@ def click(*_): dash_duo.wait_for_text_to_equal("#out", "clicked") assert dash_duo.get_logs() == [] + +def test_rext002_render_external_component_temp(dash_duo): + app = Dash() + app.layout = html.Div( + [ + dcc.Tabs( + [ + dcc.Tab(label="Tab 1", children=[ + ExternalComponent( + id="ext", + extra_component={ + "type": "Div", + "namespace": "dash_html_components", + "props": { + "id": "extra", + "children": [ + html.Div("extra children", id={"type": "extra", "index": 1}) + ], + }, + }, + extra_component_temp=True + ), + ]), + dcc.Tab(label='Tab 2', children=[ + ExternalComponent( + id="without-id", + text="without-id", + ), + ]) + ] + ), + ] + ) + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#extra", "extra children") + + dash_duo.find_element('.tab:nth-child(2)').click() + assert dash_duo.find_element("#without-id > input").get_attribute('value') == "without-id" + + dash_duo.find_element('.tab').click() + dash_duo.find_element("#ext") + assert len(dash_duo.find_elements('#ext > *')) == 0, "extra component should be removed" + + dash_duo.find_element('.tab:nth-child(2)').click() + assert dash_duo.find_element("#without-id > input").get_attribute('value') == "without-id" + + assert dash_duo.get_logs() == [] From 7de127a3677a5b53f3377fa5590e31087bf2dd3b Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:34:17 -0400 Subject: [PATCH 5/6] fixing for lint --- .../src/wrapper/ExternalWrapper.tsx | 6 +- .../renderer/test_external_component.py | 72 ++++++++++++------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx index bdf7848318..78807b2145 100644 --- a/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/ExternalWrapper.tsx @@ -19,11 +19,7 @@ type Props = { /** * For rendering components that are out of the regular layout tree. */ -function ExternalWrapper({ - component, - componentPath, - temp = false -}: Props) { +function ExternalWrapper({component, componentPath, temp = false}: Props) { const dispatch: any = useDispatch(); const [inserted, setInserted] = useState(false); diff --git a/tests/integration/renderer/test_external_component.py b/tests/integration/renderer/test_external_component.py index 7ce7e8fdc2..ec2fcfef12 100644 --- a/tests/integration/renderer/test_external_component.py +++ b/tests/integration/renderer/test_external_component.py @@ -60,34 +60,44 @@ def click(*_): assert dash_duo.get_logs() == [] + def test_rext002_render_external_component_temp(dash_duo): app = Dash() app.layout = html.Div( [ dcc.Tabs( [ - dcc.Tab(label="Tab 1", children=[ - ExternalComponent( - id="ext", - extra_component={ - "type": "Div", - "namespace": "dash_html_components", - "props": { - "id": "extra", - "children": [ - html.Div("extra children", id={"type": "extra", "index": 1}) - ], + dcc.Tab( + label="Tab 1", + children=[ + ExternalComponent( + id="ext", + extra_component={ + "type": "Div", + "namespace": "dash_html_components", + "props": { + "id": "extra", + "children": [ + html.Div( + "extra children", + id={"type": "extra", "index": 1}, + ) + ], + }, }, - }, - extra_component_temp=True - ), - ]), - dcc.Tab(label='Tab 2', children=[ - ExternalComponent( - id="without-id", - text="without-id", - ), - ]) + extra_component_temp=True, + ), + ], + ), + dcc.Tab( + label="Tab 2", + children=[ + ExternalComponent( + id="without-id", + text="without-id", + ), + ], + ), ] ), ] @@ -96,14 +106,22 @@ def test_rext002_render_external_component_temp(dash_duo): dash_duo.start_server(app) dash_duo.wait_for_text_to_equal("#extra", "extra children") - dash_duo.find_element('.tab:nth-child(2)').click() - assert dash_duo.find_element("#without-id > input").get_attribute('value') == "without-id" + dash_duo.find_element(".tab:nth-child(2)").click() + assert ( + dash_duo.find_element("#without-id > input").get_attribute("value") + == "without-id" + ) - dash_duo.find_element('.tab').click() + dash_duo.find_element(".tab").click() dash_duo.find_element("#ext") - assert len(dash_duo.find_elements('#ext > *')) == 0, "extra component should be removed" + assert ( + len(dash_duo.find_elements("#ext > *")) == 0 + ), "extra component should be removed" - dash_duo.find_element('.tab:nth-child(2)').click() - assert dash_duo.find_element("#without-id > input").get_attribute('value') == "without-id" + dash_duo.find_element(".tab:nth-child(2)").click() + assert ( + dash_duo.find_element("#without-id > input").get_attribute("value") + == "without-id" + ) assert dash_duo.get_logs() == [] From 13042c83bb4777f608a00466f5cf82de76f8e454 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:36:49 -0400 Subject: [PATCH 6/6] updating changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 499c393eca..90c2d0bd2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3279](https://github.com/plotly/dash/pull/3279) Fix an issue where persisted values were incorrectly pruned when updated via callback. Now, callback returned values are correctly stored in the persistence storage. Fix [#2678](https://github.com/plotly/dash/issues/2678) - [#3298](https://github.com/plotly/dash/pull/3298) Fix dev_only resources filtering. - [#3315](https://github.com/plotly/dash/pull/3315) Fix pages module is package check. +- [#3319](https://github.com/plotly/dash/pull/3319) Fix issue where `ExternalWrapper` would remove props from the parent component, now there is a `temp` that is passed to check if it should be removed on unmount. ## Added - [#3294](https://github.com/plotly/dash/pull/3294) Added the ability to pass `allow_optional` to Input and State to allow callbacks to work even if these components are not in the dash layout.