Skip to content

Commit acd2a2a

Browse files
authored
Merge pull request #3170 from plotly/api-render
Add ExternalWrapper to dash_component_api
2 parents c2cd4a2 + 6418096 commit acd2a2a

File tree

10 files changed

+169
-13
lines changed

10 files changed

+169
-13
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
5+
const ExternalComponent = ({ id, text, input_id }) => {
6+
const ctx = window.dash_component_api.useDashContext();
7+
const ExternalWrapper = window.dash_component_api.ExternalWrapper;
8+
9+
return (
10+
<div id={id}>
11+
<ExternalWrapper
12+
id={input_id}
13+
componentType="Input"
14+
componentNamespace="dash_core_components"
15+
value={text}
16+
componentPath={[...ctx.componentPath, 'external']}
17+
/>
18+
</div>
19+
)
20+
}
21+
22+
ExternalComponent.propTypes = {
23+
id: PropTypes.string,
24+
text: PropTypes.string,
25+
input_id: PropTypes.string,
26+
};
27+
28+
export default ExternalComponent;

@plotly/dash-test-components/src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import AddPropsComponent from "./components/AddPropsComponent";
1313
import ReceivePropsComponent from "./components/ReceivePropsComponent";
1414
import ShapeOrExactKeepOrderComponent from "./components/ShapeOrExactKeepOrderComponent";
1515
import ArrayOfExactOrShapeWithNodePropAssignNone from './components/ArrayOfExactOrShapeWithNodePropAssignNone';
16+
import ExternalComponent from './components/ExternalComponent';
1617

1718

1819
export {
@@ -29,5 +30,6 @@ export {
2930
AddPropsComponent,
3031
ReceivePropsComponent,
3132
ShapeOrExactKeepOrderComponent,
32-
ArrayOfExactOrShapeWithNodePropAssignNone
33+
ArrayOfExactOrShapeWithNodePropAssignNone,
34+
ExternalComponent,
3335
};

dash/dash-renderer/src/actions/constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ const actionList = {
88
SET_CONFIG: 1,
99
ADD_HTTP_HEADERS: 1,
1010
ON_ERROR: 1,
11-
SET_HOOKS: 1
11+
SET_HOOKS: 1,
12+
INSERT_COMPONENT: 1,
13+
REMOVE_COMPONENT: 1
1214
};
1315

1416
export const getAction = action => {

dash/dash-renderer/src/actions/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const setLayout = createAction(getAction('SET_LAYOUT'));
1818
export const setPaths = createAction(getAction('SET_PATHS'));
1919
export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE'));
2020
export const updateProps = createAction(getAction('ON_PROP_CHANGE'));
21+
export const insertComponent = createAction(getAction('INSERT_COMPONENT'));
22+
export const removeComponent = createAction(getAction('REMOVE_COMPONENT'));
2123

2224
export const dispatchError = dispatch => (message, lines) =>
2325
dispatch(

dash/dash-renderer/src/dashApi.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {path} from 'ramda';
22
import {DashContext, useDashContext} from './wrapper/DashContext';
33
import {getPath} from './actions/paths';
44
import {getStores} from './utils/stores';
5+
import ExternalWrapper from './wrapper/ExternalWrapper';
56

67
/**
78
* Get the dash props from a component path or id.
@@ -28,6 +29,7 @@ function getLayout(componentPathOrId: string[] | string): any {
2829
}
2930

3031
(window as any).dash_component_api = {
32+
ExternalWrapper,
3133
DashContext,
3234
useDashContext,
3335
getLayout

dash/dash-renderer/src/reducers/layout.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import {includes, mergeRight, append, view, lensPath, assocPath} from 'ramda';
1+
import {
2+
includes,
3+
mergeRight,
4+
append,
5+
view,
6+
lensPath,
7+
assocPath,
8+
dissocPath
9+
} from 'ramda';
210

311
import {getAction} from '../actions/constants';
412

@@ -20,6 +28,14 @@ const layout = (state = {}, action) => {
2028
const mergedProps = mergeRight(existingProps, action.payload.props);
2129
return assocPath(propPath, mergedProps, state);
2230
}
31+
// Custom component rendered out of tree.
32+
else if (action.type === getAction('INSERT_COMPONENT')) {
33+
const {component, componentPath} = action.payload;
34+
return assocPath(componentPath, component, state);
35+
} else if (action.type === getAction('REMOVE_COMPONENT')) {
36+
const {componentPath} = action.payload;
37+
return dissocPath(componentPath, state);
38+
}
2339

2440
return state;
2541
};

dash/dash-renderer/src/wrapper/DashContext.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ type LoadingFilterFunc = (loading: LoadingPayload) => boolean;
99

1010
type LoadingOptions = {
1111
/**
12-
*
12+
* Path to add after the current component if loading.
13+
* Ex `["props"]` will return true only for when that component load.
1314
*/
1415
extraPath?: DashLayoutPath;
1516
/**
16-
*
17+
* A raw path used instead of the current component.
18+
* Useful if you want the loading of a child component
19+
* as the path is available in `child.props.componentPath`.
1720
*/
1821
rawPath?: boolean;
1922
/**
2023
* Function used to filter the properties of the loading component.
24+
* Filter argument is an Entry of `{path, property, id}`.
2125
*/
2226
filterFunc?: LoadingFilterFunc;
2327
};
@@ -113,5 +117,12 @@ export function DashContextProvider(props: DashContextProviderProps) {
113117
}
114118

115119
export function useDashContext() {
116-
return useContext(DashContext);
120+
const ctx = useContext(DashContext);
121+
if (!ctx) {
122+
// eslint-disable-next-line no-console
123+
console.error(
124+
'Dash Context was not found, component was rendered without a wrapper. Use `window.dash_component_api.ExternalWrapper` to make sure the component is properly connected.'
125+
);
126+
}
127+
return ctx || {};
117128
}

dash/dash-renderer/src/wrapper/DashWrapper.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -422,10 +422,24 @@ function DashWrapper({
422422
);
423423
}
424424

425-
export default memo(
426-
DashWrapper,
427-
(prevProps, nextProps) =>
428-
JSON.stringify(prevProps.componentPath) ===
429-
JSON.stringify(nextProps.componentPath) &&
430-
prevProps._dashprivate_error === nextProps._dashprivate_error
431-
);
425+
function wrapperEquality(prev: any, next: any) {
426+
const {
427+
componentPath: prevPath,
428+
_dashprivate_error: prevError,
429+
...prevProps
430+
} = prev;
431+
const {
432+
componentPath: nextPath,
433+
_dashprivate_error: nextError,
434+
...nextProps
435+
} = next;
436+
if (JSON.stringify(prevPath) !== JSON.stringify(nextPath)) {
437+
return false;
438+
}
439+
if (prevError !== nextError) {
440+
return false;
441+
}
442+
return equals(prevProps, nextProps);
443+
}
444+
445+
export default memo(DashWrapper, wrapperEquality);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, {useState, useEffect} from 'react';
2+
import {useDispatch} from 'react-redux';
3+
4+
import {DashLayoutPath} from '../types/component';
5+
import DashWrapper from './DashWrapper';
6+
import {insertComponent, removeComponent} from '../actions';
7+
8+
type Props = {
9+
componentPath: DashLayoutPath;
10+
componentType: string;
11+
componentNamespace: string;
12+
[k: string]: any;
13+
};
14+
15+
/**
16+
* For rendering components that are out of the regular layout tree.
17+
*/
18+
function ExternalWrapper({
19+
componentType,
20+
componentNamespace,
21+
componentPath,
22+
...props
23+
}: Props) {
24+
const dispatch = useDispatch();
25+
const [inserted, setInserted] = useState(false);
26+
27+
useEffect(() => {
28+
// Give empty props for the inserted components.
29+
// The props will come from the parent so they can be updated.
30+
dispatch(
31+
insertComponent({
32+
component: {
33+
type: componentType,
34+
namespace: componentNamespace,
35+
props: {}
36+
},
37+
componentPath
38+
})
39+
);
40+
setInserted(true);
41+
return () => {
42+
dispatch(removeComponent({componentPath}));
43+
};
44+
}, []);
45+
46+
if (!inserted) {
47+
return null;
48+
}
49+
// Render a wrapper with the actual props.
50+
return <DashWrapper componentPath={componentPath} {...props} />;
51+
}
52+
export default ExternalWrapper;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from dash import Dash, html, dcc, html, Input, Output, State
2+
from dash_test_components import ExternalComponent
3+
4+
5+
def test_rext001_render_external_component(dash_duo):
6+
app = Dash()
7+
app.layout = html.Div(
8+
[
9+
dcc.Input(id="sync", value="synced"),
10+
html.Button("sync", id="sync-btn"),
11+
ExternalComponent("ext", input_id="external", text="external"),
12+
]
13+
)
14+
15+
@app.callback(
16+
Output("ext", "text"),
17+
Input("sync-btn", "n_clicks"),
18+
State("sync", "value"),
19+
prevent_initial_call=True,
20+
)
21+
def on_sync(_, value):
22+
return value
23+
24+
dash_duo.start_server(app)
25+
dash_duo.wait_for_text_to_equal("#external", "external")
26+
dash_duo.find_element("#sync-btn").click()
27+
dash_duo.wait_for_text_to_equal("#external", "synced")

0 commit comments

Comments
 (0)