Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9eb975a
Add test component as prop.
T4rk1n Mar 11, 2022
3c91fd0
Add component as prop support
T4rk1n Mar 11, 2022
7f1f5eb
Add childrenProps to components json only if there is any.
T4rk1n Mar 14, 2022
be3fea4
Remove test component not children.
T4rk1n Mar 14, 2022
60367ec
Handle components in list.
T4rk1n Apr 14, 2022
253f03a
Add support for component in dictionaries props.
T4rk1n Apr 19, 2022
56bd57a
Lint fix.
T4rk1n Apr 21, 2022
e2cf8dc
Support components in list of dictionary.
T4rk1n Apr 25, 2022
cf7e6b3
Add checklist label support for components.
T4rk1n Apr 25, 2022
f8a2f3e
Add RadioItems label support for components.
T4rk1n Apr 25, 2022
dce32fd
Add dropdown label support for components.
T4rk1n Apr 25, 2022
d4a4998
Merge branch 'dev' into component-as-props
T4rk1n Apr 29, 2022
6e0b8f3
Add collect node, store children_props on generated class, renderer h…
T4rk1n Apr 29, 2022
63f7312
Improve checks.
T4rk1n Apr 29, 2022
fa1bc34
Exclude children from node collect, json childrenProps only if any.
T4rk1n May 2, 2022
7ea2ecb
Fix union rendering.
T4rk1n May 2, 2022
3a26a40
Fix render of union in arrayOf.
T4rk1n May 2, 2022
642fbea
Collect children props in component registry, serve in config.
T4rk1n May 2, 2022
854446c
Extract config to use in crawl layout.
T4rk1n May 2, 2022
8814d44
Merge branch 'dev' into component-as-props
T4rk1n May 3, 2022
62caf4e
Export renderer store.
T4rk1n May 3, 2022
d43e212
Cleanup/Proper handling of nodes.
T4rk1n May 4, 2022
c45f7ac
Store childrenProps on window.
T4rk1n May 5, 2022
8e4b1cf
Get base nodes to return non nested components props only.
T4rk1n May 26, 2022
8d4edfc
Move bracket to end of children prop path.
T4rk1n May 26, 2022
9b92930
Merge branch 'dev' into component-as-props
T4rk1n May 27, 2022
463b4ba
Change labels prop type to node only.
T4rk1n May 27, 2022
30b4b86
Remove elements from test ComponentAsProp
T4rk1n May 27, 2022
c20399f
Move base node iteration to component generator.
T4rk1n Jun 2, 2022
f27bee8
Merge branch 'dev' into component-as-props
T4rk1n Jun 2, 2022
3761b80
Update changelog.
T4rk1n Jun 2, 2022
6e57b0b
Add back deleted test debc026
T4rk1n Jun 2, 2022
ebc309a
Add test legacy components.
T4rk1n Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions @plotly/dash-test-components/src/components/ComponentAsProp.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';


const ComponentAsProp = (props) => {
const { element, id, shapeEl, list_of_shapes } = props;
return (
<div id={id}>
{shapeEl && shapeEl.header}
{element}
{shapeEl && shapeEl.footer}
{list_of_shapes && <ul>{list_of_shapes.map(e => <li key={e.value}>{e.label}</li>)}</ul> }
</div>
)
}

ComponentAsProp.propTypes = {
id: PropTypes.string,
element: PropTypes.node,

shapeEl: PropTypes.shape({
header: PropTypes.node,
footer: PropTypes.node,
}),

list_of_shapes: PropTypes.arrayOf(
PropTypes.exact({
label: PropTypes.node,
value: PropTypes.number,
})
)
}

export default ComponentAsProp;
4 changes: 3 additions & 1 deletion @plotly/dash-test-components/src/index.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import MyPersistedComponent from './components/MyPersistedComponent';
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';

export {
AsyncComponent,
Expand All@@ -15,5 +16,6 @@ export {
MyPersistedComponent,
MyPersistedComponentNested,
StyledComponent,
WidthComponent
WidthComponent,
ComponentAsProp,
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added

- [#2049](https://github.com/plotly/dash/pull/2043) Added `wait_for_class_to_equal` and `wait_for_contains_class` methods to `dash.testing`
- [#1965](https://github.com/plotly/dash/pull/1965) Add component as props.

## [2.4.1] - 2022-05-11

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,11 +104,7 @@ Checklist.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,8 +56,7 @@ Dropdown.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,11 +99,7 @@ RadioItems.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
from dash import Dash, dcc, html


def test_mdcap001_dcc_components_as_props(dash_dcc):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Checklist(
[
{"label": html.H2("H2 label"), "value": "h2"},
{"label": html.A("Link in checklist", href="#"), "value": "a"},
],
id="checklist",
),
dcc.RadioItems(
[
{"label": html.H3("on"), "value": "on"},
{"label": html.P("off"), "value": "off"},
],
id="radio-items",
),
dcc.Dropdown(
[
{"label": html.H4("h4"), "value": "h4"},
{"label": html.H6("h6"), "value": "h6"},
],
id="dropdown",
),
]
)

dash_dcc.start_server(app)

dash_dcc.wait_for_text_to_equal("#checklist h2", "H2 label")
dash_dcc.wait_for_text_to_equal("#checklist a", "Link in checklist")

dash_dcc.wait_for_text_to_equal("#radio-items h3", "on")
dash_dcc.wait_for_text_to_equal("#radio-items p", "off")

dash_dcc.find_element("#dropdown").click()
dash_dcc.wait_for_text_to_equal("#dropdown h4", "h4")
dash_dcc.wait_for_text_to_equal("#dropdown h6", "h6")
5 changes: 2 additions & 3 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import APIController from './APIController.react';
import Loading from './components/core/Loading.react';
import Toolbar from './components/core/Toolbar.react';
import Reloader from './components/core/Reloader.react';
import getConfigFromDOM from './config';
import {setHooks, setConfig} from './actions/index';
import {type, memoizeWith, identity} from 'ramda';

Expand DownExpand Up@@ -35,9 +36,7 @@ class UnconnectedAppContainer extends React.Component {

UNSAFE_componentWillMount() {
const {dispatch} = this.props;
const config = JSON.parse(
document.getElementById('_dash-config').textContent
);
const config = getConfigFromDOM();

// preset common request params in the config
config.fetch = {
Expand Down
1 change: 0 additions & 1 deletion dash/dash-renderer/src/AppProvider.react.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import AppContainer from './AppContainer.react';

const AppProvider = ({hooks}: any) => {
const [{store}] = useState(() => new Store());

return (
<Provider store={store}>
<AppContainer hooks={hooks} />
Expand Down
122 changes: 121 additions & 1 deletion dash/dash-renderer/src/TreeContainer.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,17 +4,23 @@ import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
concat,
dissoc,
equals,
isEmpty,
isNil,
has,
keys,
map,
mergeRight,
pick,
pickBy,
pipe,
propOr,
path as rpath,
pathOr,
type
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
Expand DownExpand Up@@ -67,6 +73,15 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}

function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
has('type', obj) &&
has('namespace', obj) &&
has('props', obj)
);
}

const TreeContainer = memo(props => (
<DashContext.Consumer>
{context => (
Expand DownExpand Up@@ -180,6 +195,32 @@ class BaseTreeContainer extends Component {
);
}

wrapChildrenProp(node, childrenProp) {
if (Array.isArray(node)) {
return node.map((n, i) =>
isDryComponent(n)
? this.createContainer(
this.props,
n,
concat(this.props._dashprivate_path, [
'props',
...childrenProp,
i
])
)
: n
);
}
if (!isDryComponent(node)) {
return node;
}
return this.createContainer(
this.props,
node,
concat(this.props._dashprivate_path, ['props', ...childrenProp])
);
}

getComponent(_dashprivate_layout, children, loading_state, setProps) {
const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} =
this.props;
Expand All@@ -195,7 +236,86 @@ class BaseTreeContainer extends Component {

const element = Registry.resolve(_dashprivate_layout);

const props = dissoc('children', _dashprivate_layout.props);
// Hydrate components props
const childrenProps = pathOr(
[],
[
'children_props',
_dashprivate_layout.namespace,
_dashprivate_layout.type
],
_dashprivate_config
);
const props = pipe(
dissoc('children'),
...childrenProps
.map(childrenProp => {
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
let nodeValue;
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
found = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
frontPath.push(p.replace('[]', ''));
} else {
frontPath.push(p);
}
} else {
backPath.push(p);
}
});

node = rpath(frontPath, _dashprivate_layout.props);
if (node === undefined) {
return;
}
if (!node.length) {
return assocPath(frontPath, node);
}
const firstNode = rpath(backPath, node[0]);
if (!firstNode) {
return assocPath(frontPath, node);
}
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
return assocPath(
backPath,
this.wrapChildrenProp(
rpath(backPath, element),
elementPath
),
element
);
});
path = frontPath;
} else {
node = rpath(path, _dashprivate_layout.props);
if (node === undefined) {
return;
}
nodeValue = this.wrapChildrenProp(node, path);
}
return assocPath(path, nodeValue);
}
const node = _dashprivate_layout.props[childrenProp];
if (node !== undefined) {
return assoc(
childrenProp,
this.wrapChildrenProp(node, [childrenProp])
);
}
})
.filter(e => e !== undefined)
)(_dashprivate_layout.props);

if (type(props.id) === 'Object') {
// Turn object ids (for wildcards) into unique strings.
Expand Down
45 changes: 42 additions & 3 deletions dash/dash-renderer/src/actions/utils.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import {append, concat, has, path, type} from 'ramda';
import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';

/*
* requests_pathname_prefix is the new config parameter introduced in
Expand DownExpand Up@@ -27,11 +27,24 @@ export function urlBase(config) {
const propsChildren = ['props', 'children'];

// crawl a layout object or children array, apply a function on every object
export const crawlLayout = (object, func, currentPath = []) => {
export const crawlLayout = (
object,
func,
currentPath = [],
extraPath = undefined
) => {
if (Array.isArray(object)) {
// children array
object.forEach((child, i) => {
crawlLayout(child, func, append(i, currentPath));
if (extraPath) {
crawlLayout(
rpath(extraPath, child),
func,
concat(currentPath, concat([i], extraPath))
);
} else {
crawlLayout(child, func, append(i, currentPath));
}
});
} else if (type(object) === 'Object') {
func(object, currentPath);
Expand All@@ -41,6 +54,32 @@ export const crawlLayout = (object, func, currentPath = []) => {
const newPath = concat(currentPath, propsChildren);
crawlLayout(children, func, newPath);
}

const childrenProps = pathOr(
[],
[object.namespace, object.type],
window.__dashprivate_childrenProps
);
childrenProps.forEach(childrenProp => {
if (childrenProp.includes('[]')) {
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
);
}
});
}
};

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9eb975a
Add test component as prop.
T4rk1n Mar 11, 2022
3c91fd0
Add component as prop support
T4rk1n Mar 11, 2022
7f1f5eb
Add childrenProps to components json only if there is any.
T4rk1n Mar 14, 2022
be3fea4
Remove test component not children.
T4rk1n Mar 14, 2022
60367ec
Handle components in list.
T4rk1n Apr 14, 2022
253f03a
Add support for component in dictionaries props.
T4rk1n Apr 19, 2022
56bd57a
Lint fix.
T4rk1n Apr 21, 2022
e2cf8dc
Support components in list of dictionary.
T4rk1n Apr 25, 2022
cf7e6b3
Add checklist label support for components.
T4rk1n Apr 25, 2022
f8a2f3e
Add RadioItems label support for components.
T4rk1n Apr 25, 2022
dce32fd
Add dropdown label support for components.
T4rk1n Apr 25, 2022
d4a4998
Merge branch 'dev' into component-as-props
T4rk1n Apr 29, 2022
6e0b8f3
Add collect node, store children_props on generated class, renderer h…
T4rk1n Apr 29, 2022
63f7312
Improve checks.
T4rk1n Apr 29, 2022
fa1bc34
Exclude children from node collect, json childrenProps only if any.
T4rk1n May 2, 2022
7ea2ecb
Fix union rendering.
T4rk1n May 2, 2022
3a26a40
Fix render of union in arrayOf.
T4rk1n May 2, 2022
642fbea
Collect children props in component registry, serve in config.
T4rk1n May 2, 2022
854446c
Extract config to use in crawl layout.
T4rk1n May 2, 2022
8814d44
Merge branch 'dev' into component-as-props
T4rk1n May 3, 2022
62caf4e
Export renderer store.
T4rk1n May 3, 2022
d43e212
Cleanup/Proper handling of nodes.
T4rk1n May 4, 2022
c45f7ac
Store childrenProps on window.
T4rk1n May 5, 2022
8e4b1cf
Get base nodes to return non nested components props only.
T4rk1n May 26, 2022
8d4edfc
Move bracket to end of children prop path.
T4rk1n May 26, 2022
9b92930
Merge branch 'dev' into component-as-props
T4rk1n May 27, 2022
463b4ba
Change labels prop type to node only.
T4rk1n May 27, 2022
30b4b86
Remove elements from test ComponentAsProp
T4rk1n May 27, 2022
c20399f
Move base node iteration to component generator.
T4rk1n Jun 2, 2022
f27bee8
Merge branch 'dev' into component-as-props
T4rk1n Jun 2, 2022
3761b80
Update changelog.
T4rk1n Jun 2, 2022
6e57b0b
Add back deleted test debc026
T4rk1n Jun 2, 2022
ebc309a
Add test legacy components.
T4rk1n Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions @plotly/dash-test-components/src/components/ComponentAsProp.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';


const ComponentAsProp = (props) => {
const { element, id, shapeEl, list_of_shapes } = props;
return (
<div id={id}>
{shapeEl && shapeEl.header}
{element}
{shapeEl && shapeEl.footer}
{list_of_shapes && <ul>{list_of_shapes.map(e => <li key={e.value}>{e.label}</li>)}</ul> }
</div>
)
}

ComponentAsProp.propTypes = {
id: PropTypes.string,
element: PropTypes.node,

shapeEl: PropTypes.shape({
header: PropTypes.node,
footer: PropTypes.node,
}),

list_of_shapes: PropTypes.arrayOf(
PropTypes.exact({
label: PropTypes.node,
value: PropTypes.number,
})
)
}

export default ComponentAsProp;
4 changes: 3 additions & 1 deletion @plotly/dash-test-components/src/index.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import MyPersistedComponent from './components/MyPersistedComponent';
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';

export {
AsyncComponent,
Expand All@@ -15,5 +16,6 @@ export {
MyPersistedComponent,
MyPersistedComponentNested,
StyledComponent,
WidthComponent
WidthComponent,
ComponentAsProp,
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added

- [#2049](https://github.com/plotly/dash/pull/2043) Added `wait_for_class_to_equal` and `wait_for_contains_class` methods to `dash.testing`
- [#1965](https://github.com/plotly/dash/pull/1965) Add component as props.

## [2.4.1] - 2022-05-11

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,11 +104,7 @@ Checklist.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,8 +56,7 @@ Dropdown.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,11 +99,7 @@ RadioItems.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
from dash import Dash, dcc, html


def test_mdcap001_dcc_components_as_props(dash_dcc):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Checklist(
[
{"label": html.H2("H2 label"), "value": "h2"},
{"label": html.A("Link in checklist", href="#"), "value": "a"},
],
id="checklist",
),
dcc.RadioItems(
[
{"label": html.H3("on"), "value": "on"},
{"label": html.P("off"), "value": "off"},
],
id="radio-items",
),
dcc.Dropdown(
[
{"label": html.H4("h4"), "value": "h4"},
{"label": html.H6("h6"), "value": "h6"},
],
id="dropdown",
),
]
)

dash_dcc.start_server(app)

dash_dcc.wait_for_text_to_equal("#checklist h2", "H2 label")
dash_dcc.wait_for_text_to_equal("#checklist a", "Link in checklist")

dash_dcc.wait_for_text_to_equal("#radio-items h3", "on")
dash_dcc.wait_for_text_to_equal("#radio-items p", "off")

dash_dcc.find_element("#dropdown").click()
dash_dcc.wait_for_text_to_equal("#dropdown h4", "h4")
dash_dcc.wait_for_text_to_equal("#dropdown h6", "h6")
5 changes: 2 additions & 3 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import APIController from './APIController.react';
import Loading from './components/core/Loading.react';
import Toolbar from './components/core/Toolbar.react';
import Reloader from './components/core/Reloader.react';
import getConfigFromDOM from './config';
import {setHooks, setConfig} from './actions/index';
import {type, memoizeWith, identity} from 'ramda';

Expand DownExpand Up@@ -35,9 +36,7 @@ class UnconnectedAppContainer extends React.Component {

UNSAFE_componentWillMount() {
const {dispatch} = this.props;
const config = JSON.parse(
document.getElementById('_dash-config').textContent
);
const config = getConfigFromDOM();

// preset common request params in the config
config.fetch = {
Expand Down
1 change: 0 additions & 1 deletion dash/dash-renderer/src/AppProvider.react.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import AppContainer from './AppContainer.react';

const AppProvider = ({hooks}: any) => {
const [{store}] = useState(() => new Store());

return (
<Provider store={store}>
<AppContainer hooks={hooks} />
Expand Down
122 changes: 121 additions & 1 deletion dash/dash-renderer/src/TreeContainer.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,17 +4,23 @@ import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
concat,
dissoc,
equals,
isEmpty,
isNil,
has,
keys,
map,
mergeRight,
pick,
pickBy,
pipe,
propOr,
path as rpath,
pathOr,
type
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
Expand DownExpand Up@@ -67,6 +73,15 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}

function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
has('type', obj) &&
has('namespace', obj) &&
has('props', obj)
);
}

const TreeContainer = memo(props => (
<DashContext.Consumer>
{context => (
Expand DownExpand Up@@ -180,6 +195,32 @@ class BaseTreeContainer extends Component {
);
}

wrapChildrenProp(node, childrenProp) {
if (Array.isArray(node)) {
return node.map((n, i) =>
isDryComponent(n)
? this.createContainer(
this.props,
n,
concat(this.props._dashprivate_path, [
'props',
...childrenProp,
i
])
)
: n
);
}
if (!isDryComponent(node)) {
return node;
}
return this.createContainer(
this.props,
node,
concat(this.props._dashprivate_path, ['props', ...childrenProp])
);
}

getComponent(_dashprivate_layout, children, loading_state, setProps) {
const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} =
this.props;
Expand All@@ -195,7 +236,86 @@ class BaseTreeContainer extends Component {

const element = Registry.resolve(_dashprivate_layout);

const props = dissoc('children', _dashprivate_layout.props);
// Hydrate components props
const childrenProps = pathOr(
[],
[
'children_props',
_dashprivate_layout.namespace,
_dashprivate_layout.type
],
_dashprivate_config
);
const props = pipe(
dissoc('children'),
...childrenProps
.map(childrenProp => {
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
let nodeValue;
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
found = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
frontPath.push(p.replace('[]', ''));
} else {
frontPath.push(p);
}
} else {
backPath.push(p);
}
});

node = rpath(frontPath, _dashprivate_layout.props);
if (node === undefined) {
return;
}
if (!node.length) {
return assocPath(frontPath, node);
}
const firstNode = rpath(backPath, node[0]);
if (!firstNode) {
return assocPath(frontPath, node);
}
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
return assocPath(
backPath,
this.wrapChildrenProp(
rpath(backPath, element),
elementPath
),
element
);
});
path = frontPath;
} else {
node = rpath(path, _dashprivate_layout.props);
if (node === undefined) {
return;
}
nodeValue = this.wrapChildrenProp(node, path);
}
return assocPath(path, nodeValue);
}
const node = _dashprivate_layout.props[childrenProp];
if (node !== undefined) {
return assoc(
childrenProp,
this.wrapChildrenProp(node, [childrenProp])
);
}
})
.filter(e => e !== undefined)
)(_dashprivate_layout.props);

if (type(props.id) === 'Object') {
// Turn object ids (for wildcards) into unique strings.
Expand Down
45 changes: 42 additions & 3 deletions dash/dash-renderer/src/actions/utils.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import {append, concat, has, path, type} from 'ramda';
import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';

/*
* requests_pathname_prefix is the new config parameter introduced in
Expand DownExpand Up@@ -27,11 +27,24 @@ export function urlBase(config) {
const propsChildren = ['props', 'children'];

// crawl a layout object or children array, apply a function on every object
export const crawlLayout = (object, func, currentPath = []) => {
export const crawlLayout = (
object,
func,
currentPath = [],
extraPath = undefined
) => {
if (Array.isArray(object)) {
// children array
object.forEach((child, i) => {
crawlLayout(child, func, append(i, currentPath));
if (extraPath) {
crawlLayout(
rpath(extraPath, child),
func,
concat(currentPath, concat([i], extraPath))
);
} else {
crawlLayout(child, func, append(i, currentPath));
}
});
} else if (type(object) === 'Object') {
func(object, currentPath);
Expand All@@ -41,6 +54,32 @@ export const crawlLayout = (object, func, currentPath = []) => {
const newPath = concat(currentPath, propsChildren);
crawlLayout(children, func, newPath);
}

const childrenProps = pathOr(
[],
[object.namespace, object.type],
window.__dashprivate_childrenProps
);
childrenProps.forEach(childrenProp => {
if (childrenProp.includes('[]')) {
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
);
}
});
}
};

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9eb975a
Add test component as prop.
T4rk1n Mar 11, 2022
3c91fd0
Add component as prop support
T4rk1n Mar 11, 2022
7f1f5eb
Add childrenProps to components json only if there is any.
T4rk1n Mar 14, 2022
be3fea4
Remove test component not children.
T4rk1n Mar 14, 2022
60367ec
Handle components in list.
T4rk1n Apr 14, 2022
253f03a
Add support for component in dictionaries props.
T4rk1n Apr 19, 2022
56bd57a
Lint fix.
T4rk1n Apr 21, 2022
e2cf8dc
Support components in list of dictionary.
T4rk1n Apr 25, 2022
cf7e6b3
Add checklist label support for components.
T4rk1n Apr 25, 2022
f8a2f3e
Add RadioItems label support for components.
T4rk1n Apr 25, 2022
dce32fd
Add dropdown label support for components.
T4rk1n Apr 25, 2022
d4a4998
Merge branch 'dev' into component-as-props
T4rk1n Apr 29, 2022
6e0b8f3
Add collect node, store children_props on generated class, renderer h…
T4rk1n Apr 29, 2022
63f7312
Improve checks.
T4rk1n Apr 29, 2022
fa1bc34
Exclude children from node collect, json childrenProps only if any.
T4rk1n May 2, 2022
7ea2ecb
Fix union rendering.
T4rk1n May 2, 2022
3a26a40
Fix render of union in arrayOf.
T4rk1n May 2, 2022
642fbea
Collect children props in component registry, serve in config.
T4rk1n May 2, 2022
854446c
Extract config to use in crawl layout.
T4rk1n May 2, 2022
8814d44
Merge branch 'dev' into component-as-props
T4rk1n May 3, 2022
62caf4e
Export renderer store.
T4rk1n May 3, 2022
d43e212
Cleanup/Proper handling of nodes.
T4rk1n May 4, 2022
c45f7ac
Store childrenProps on window.
T4rk1n May 5, 2022
8e4b1cf
Get base nodes to return non nested components props only.
T4rk1n May 26, 2022
8d4edfc
Move bracket to end of children prop path.
T4rk1n May 26, 2022
9b92930
Merge branch 'dev' into component-as-props
T4rk1n May 27, 2022
463b4ba
Change labels prop type to node only.
T4rk1n May 27, 2022
30b4b86
Remove elements from test ComponentAsProp
T4rk1n May 27, 2022
c20399f
Move base node iteration to component generator.
T4rk1n Jun 2, 2022
f27bee8
Merge branch 'dev' into component-as-props
T4rk1n Jun 2, 2022
3761b80
Update changelog.
T4rk1n Jun 2, 2022
6e57b0b
Add back deleted test debc026
T4rk1n Jun 2, 2022
ebc309a
Add test legacy components.
T4rk1n Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions @plotly/dash-test-components/src/components/ComponentAsProp.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';


const ComponentAsProp = (props) => {
const { element, id, shapeEl, list_of_shapes } = props;
return (
<div id={id}>
{shapeEl && shapeEl.header}
{element}
{shapeEl && shapeEl.footer}
{list_of_shapes && <ul>{list_of_shapes.map(e => <li key={e.value}>{e.label}</li>)}</ul> }
</div>
)
}

ComponentAsProp.propTypes = {
id: PropTypes.string,
element: PropTypes.node,

shapeEl: PropTypes.shape({
header: PropTypes.node,
footer: PropTypes.node,
}),

list_of_shapes: PropTypes.arrayOf(
PropTypes.exact({
label: PropTypes.node,
value: PropTypes.number,
})
)
}

export default ComponentAsProp;
4 changes: 3 additions & 1 deletion @plotly/dash-test-components/src/index.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import MyPersistedComponent from './components/MyPersistedComponent';
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';

export {
AsyncComponent,
Expand All@@ -15,5 +16,6 @@ export {
MyPersistedComponent,
MyPersistedComponentNested,
StyledComponent,
WidthComponent
WidthComponent,
ComponentAsProp,
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added

- [#2049](https://github.com/plotly/dash/pull/2043) Added `wait_for_class_to_equal` and `wait_for_contains_class` methods to `dash.testing`
- [#1965](https://github.com/plotly/dash/pull/1965) Add component as props.

## [2.4.1] - 2022-05-11

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,11 +104,7 @@ Checklist.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,8 +56,7 @@ Dropdown.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,11 +99,7 @@ RadioItems.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
from dash import Dash, dcc, html


def test_mdcap001_dcc_components_as_props(dash_dcc):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Checklist(
[
{"label": html.H2("H2 label"), "value": "h2"},
{"label": html.A("Link in checklist", href="#"), "value": "a"},
],
id="checklist",
),
dcc.RadioItems(
[
{"label": html.H3("on"), "value": "on"},
{"label": html.P("off"), "value": "off"},
],
id="radio-items",
),
dcc.Dropdown(
[
{"label": html.H4("h4"), "value": "h4"},
{"label": html.H6("h6"), "value": "h6"},
],
id="dropdown",
),
]
)

dash_dcc.start_server(app)

dash_dcc.wait_for_text_to_equal("#checklist h2", "H2 label")
dash_dcc.wait_for_text_to_equal("#checklist a", "Link in checklist")

dash_dcc.wait_for_text_to_equal("#radio-items h3", "on")
dash_dcc.wait_for_text_to_equal("#radio-items p", "off")

dash_dcc.find_element("#dropdown").click()
dash_dcc.wait_for_text_to_equal("#dropdown h4", "h4")
dash_dcc.wait_for_text_to_equal("#dropdown h6", "h6")
5 changes: 2 additions & 3 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import APIController from './APIController.react';
import Loading from './components/core/Loading.react';
import Toolbar from './components/core/Toolbar.react';
import Reloader from './components/core/Reloader.react';
import getConfigFromDOM from './config';
import {setHooks, setConfig} from './actions/index';
import {type, memoizeWith, identity} from 'ramda';

Expand DownExpand Up@@ -35,9 +36,7 @@ class UnconnectedAppContainer extends React.Component {

UNSAFE_componentWillMount() {
const {dispatch} = this.props;
const config = JSON.parse(
document.getElementById('_dash-config').textContent
);
const config = getConfigFromDOM();

// preset common request params in the config
config.fetch = {
Expand Down
1 change: 0 additions & 1 deletion dash/dash-renderer/src/AppProvider.react.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import AppContainer from './AppContainer.react';

const AppProvider = ({hooks}: any) => {
const [{store}] = useState(() => new Store());

return (
<Provider store={store}>
<AppContainer hooks={hooks} />
Expand Down
122 changes: 121 additions & 1 deletion dash/dash-renderer/src/TreeContainer.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,17 +4,23 @@ import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
concat,
dissoc,
equals,
isEmpty,
isNil,
has,
keys,
map,
mergeRight,
pick,
pickBy,
pipe,
propOr,
path as rpath,
pathOr,
type
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
Expand DownExpand Up@@ -67,6 +73,15 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}

function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
has('type', obj) &&
has('namespace', obj) &&
has('props', obj)
);
}

const TreeContainer = memo(props => (
<DashContext.Consumer>
{context => (
Expand DownExpand Up@@ -180,6 +195,32 @@ class BaseTreeContainer extends Component {
);
}

wrapChildrenProp(node, childrenProp) {
if (Array.isArray(node)) {
return node.map((n, i) =>
isDryComponent(n)
? this.createContainer(
this.props,
n,
concat(this.props._dashprivate_path, [
'props',
...childrenProp,
i
])
)
: n
);
}
if (!isDryComponent(node)) {
return node;
}
return this.createContainer(
this.props,
node,
concat(this.props._dashprivate_path, ['props', ...childrenProp])
);
}

getComponent(_dashprivate_layout, children, loading_state, setProps) {
const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} =
this.props;
Expand All@@ -195,7 +236,86 @@ class BaseTreeContainer extends Component {

const element = Registry.resolve(_dashprivate_layout);

const props = dissoc('children', _dashprivate_layout.props);
// Hydrate components props
const childrenProps = pathOr(
[],
[
'children_props',
_dashprivate_layout.namespace,
_dashprivate_layout.type
],
_dashprivate_config
);
const props = pipe(
dissoc('children'),
...childrenProps
.map(childrenProp => {
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
let nodeValue;
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
found = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
frontPath.push(p.replace('[]', ''));
} else {
frontPath.push(p);
}
} else {
backPath.push(p);
}
});

node = rpath(frontPath, _dashprivate_layout.props);
if (node === undefined) {
return;
}
if (!node.length) {
return assocPath(frontPath, node);
}
const firstNode = rpath(backPath, node[0]);
if (!firstNode) {
return assocPath(frontPath, node);
}
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
return assocPath(
backPath,
this.wrapChildrenProp(
rpath(backPath, element),
elementPath
),
element
);
});
path = frontPath;
} else {
node = rpath(path, _dashprivate_layout.props);
if (node === undefined) {
return;
}
nodeValue = this.wrapChildrenProp(node, path);
}
return assocPath(path, nodeValue);
}
const node = _dashprivate_layout.props[childrenProp];
if (node !== undefined) {
return assoc(
childrenProp,
this.wrapChildrenProp(node, [childrenProp])
);
}
})
.filter(e => e !== undefined)
)(_dashprivate_layout.props);

if (type(props.id) === 'Object') {
// Turn object ids (for wildcards) into unique strings.
Expand Down
45 changes: 42 additions & 3 deletions dash/dash-renderer/src/actions/utils.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import {append, concat, has, path, type} from 'ramda';
import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';

/*
* requests_pathname_prefix is the new config parameter introduced in
Expand DownExpand Up@@ -27,11 +27,24 @@ export function urlBase(config) {
const propsChildren = ['props', 'children'];

// crawl a layout object or children array, apply a function on every object
export const crawlLayout = (object, func, currentPath = []) => {
export const crawlLayout = (
object,
func,
currentPath = [],
extraPath = undefined
) => {
if (Array.isArray(object)) {
// children array
object.forEach((child, i) => {
crawlLayout(child, func, append(i, currentPath));
if (extraPath) {
crawlLayout(
rpath(extraPath, child),
func,
concat(currentPath, concat([i], extraPath))
);
} else {
crawlLayout(child, func, append(i, currentPath));
}
});
} else if (type(object) === 'Object') {
func(object, currentPath);
Expand All@@ -41,6 +54,32 @@ export const crawlLayout = (object, func, currentPath = []) => {
const newPath = concat(currentPath, propsChildren);
crawlLayout(children, func, newPath);
}

const childrenProps = pathOr(
[],
[object.namespace, object.type],
window.__dashprivate_childrenProps
);
childrenProps.forEach(childrenProp => {
if (childrenProp.includes('[]')) {
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
);
}
});
}
};

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9eb975a
Add test component as prop.
T4rk1n Mar 11, 2022
3c91fd0
Add component as prop support
T4rk1n Mar 11, 2022
7f1f5eb
Add childrenProps to components json only if there is any.
T4rk1n Mar 14, 2022
be3fea4
Remove test component not children.
T4rk1n Mar 14, 2022
60367ec
Handle components in list.
T4rk1n Apr 14, 2022
253f03a
Add support for component in dictionaries props.
T4rk1n Apr 19, 2022
56bd57a
Lint fix.
T4rk1n Apr 21, 2022
e2cf8dc
Support components in list of dictionary.
T4rk1n Apr 25, 2022
cf7e6b3
Add checklist label support for components.
T4rk1n Apr 25, 2022
f8a2f3e
Add RadioItems label support for components.
T4rk1n Apr 25, 2022
dce32fd
Add dropdown label support for components.
T4rk1n Apr 25, 2022
d4a4998
Merge branch 'dev' into component-as-props
T4rk1n Apr 29, 2022
6e0b8f3
Add collect node, store children_props on generated class, renderer h…
T4rk1n Apr 29, 2022
63f7312
Improve checks.
T4rk1n Apr 29, 2022
fa1bc34
Exclude children from node collect, json childrenProps only if any.
T4rk1n May 2, 2022
7ea2ecb
Fix union rendering.
T4rk1n May 2, 2022
3a26a40
Fix render of union in arrayOf.
T4rk1n May 2, 2022
642fbea
Collect children props in component registry, serve in config.
T4rk1n May 2, 2022
854446c
Extract config to use in crawl layout.
T4rk1n May 2, 2022
8814d44
Merge branch 'dev' into component-as-props
T4rk1n May 3, 2022
62caf4e
Export renderer store.
T4rk1n May 3, 2022
d43e212
Cleanup/Proper handling of nodes.
T4rk1n May 4, 2022
c45f7ac
Store childrenProps on window.
T4rk1n May 5, 2022
8e4b1cf
Get base nodes to return non nested components props only.
T4rk1n May 26, 2022
8d4edfc
Move bracket to end of children prop path.
T4rk1n May 26, 2022
9b92930
Merge branch 'dev' into component-as-props
T4rk1n May 27, 2022
463b4ba
Change labels prop type to node only.
T4rk1n May 27, 2022
30b4b86
Remove elements from test ComponentAsProp
T4rk1n May 27, 2022
c20399f
Move base node iteration to component generator.
T4rk1n Jun 2, 2022
f27bee8
Merge branch 'dev' into component-as-props
T4rk1n Jun 2, 2022
3761b80
Update changelog.
T4rk1n Jun 2, 2022
6e57b0b
Add back deleted test debc026
T4rk1n Jun 2, 2022
ebc309a
Add test legacy components.
T4rk1n Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions @plotly/dash-test-components/src/components/ComponentAsProp.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';


const ComponentAsProp = (props) => {
const { element, id, shapeEl, list_of_shapes } = props;
return (
<div id={id}>
{shapeEl && shapeEl.header}
{element}
{shapeEl && shapeEl.footer}
{list_of_shapes && <ul>{list_of_shapes.map(e => <li key={e.value}>{e.label}</li>)}</ul> }
</div>
)
}

ComponentAsProp.propTypes = {
id: PropTypes.string,
element: PropTypes.node,

shapeEl: PropTypes.shape({
header: PropTypes.node,
footer: PropTypes.node,
}),

list_of_shapes: PropTypes.arrayOf(
PropTypes.exact({
label: PropTypes.node,
value: PropTypes.number,
})
)
}

export default ComponentAsProp;
4 changes: 3 additions & 1 deletion @plotly/dash-test-components/src/index.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import MyPersistedComponent from './components/MyPersistedComponent';
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';

export {
AsyncComponent,
Expand All@@ -15,5 +16,6 @@ export {
MyPersistedComponent,
MyPersistedComponentNested,
StyledComponent,
WidthComponent
WidthComponent,
ComponentAsProp,
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added

- [#2049](https://github.com/plotly/dash/pull/2043) Added `wait_for_class_to_equal` and `wait_for_contains_class` methods to `dash.testing`
- [#1965](https://github.com/plotly/dash/pull/1965) Add component as props.

## [2.4.1] - 2022-05-11

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,11 +104,7 @@ Checklist.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,8 +56,7 @@ Dropdown.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,11 +99,7 @@ RadioItems.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
from dash import Dash, dcc, html


def test_mdcap001_dcc_components_as_props(dash_dcc):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Checklist(
[
{"label": html.H2("H2 label"), "value": "h2"},
{"label": html.A("Link in checklist", href="#"), "value": "a"},
],
id="checklist",
),
dcc.RadioItems(
[
{"label": html.H3("on"), "value": "on"},
{"label": html.P("off"), "value": "off"},
],
id="radio-items",
),
dcc.Dropdown(
[
{"label": html.H4("h4"), "value": "h4"},
{"label": html.H6("h6"), "value": "h6"},
],
id="dropdown",
),
]
)

dash_dcc.start_server(app)

dash_dcc.wait_for_text_to_equal("#checklist h2", "H2 label")
dash_dcc.wait_for_text_to_equal("#checklist a", "Link in checklist")

dash_dcc.wait_for_text_to_equal("#radio-items h3", "on")
dash_dcc.wait_for_text_to_equal("#radio-items p", "off")

dash_dcc.find_element("#dropdown").click()
dash_dcc.wait_for_text_to_equal("#dropdown h4", "h4")
dash_dcc.wait_for_text_to_equal("#dropdown h6", "h6")
5 changes: 2 additions & 3 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import APIController from './APIController.react';
import Loading from './components/core/Loading.react';
import Toolbar from './components/core/Toolbar.react';
import Reloader from './components/core/Reloader.react';
import getConfigFromDOM from './config';
import {setHooks, setConfig} from './actions/index';
import {type, memoizeWith, identity} from 'ramda';

Expand DownExpand Up@@ -35,9 +36,7 @@ class UnconnectedAppContainer extends React.Component {

UNSAFE_componentWillMount() {
const {dispatch} = this.props;
const config = JSON.parse(
document.getElementById('_dash-config').textContent
);
const config = getConfigFromDOM();

// preset common request params in the config
config.fetch = {
Expand Down
1 change: 0 additions & 1 deletion dash/dash-renderer/src/AppProvider.react.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import AppContainer from './AppContainer.react';

const AppProvider = ({hooks}: any) => {
const [{store}] = useState(() => new Store());

return (
<Provider store={store}>
<AppContainer hooks={hooks} />
Expand Down
122 changes: 121 additions & 1 deletion dash/dash-renderer/src/TreeContainer.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,17 +4,23 @@ import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
concat,
dissoc,
equals,
isEmpty,
isNil,
has,
keys,
map,
mergeRight,
pick,
pickBy,
pipe,
propOr,
path as rpath,
pathOr,
type
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
Expand DownExpand Up@@ -67,6 +73,15 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}

function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
has('type', obj) &&
has('namespace', obj) &&
has('props', obj)
);
}

const TreeContainer = memo(props => (
<DashContext.Consumer>
{context => (
Expand DownExpand Up@@ -180,6 +195,32 @@ class BaseTreeContainer extends Component {
);
}

wrapChildrenProp(node, childrenProp) {
if (Array.isArray(node)) {
return node.map((n, i) =>
isDryComponent(n)
? this.createContainer(
this.props,
n,
concat(this.props._dashprivate_path, [
'props',
...childrenProp,
i
])
)
: n
);
}
if (!isDryComponent(node)) {
return node;
}
return this.createContainer(
this.props,
node,
concat(this.props._dashprivate_path, ['props', ...childrenProp])
);
}

getComponent(_dashprivate_layout, children, loading_state, setProps) {
const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} =
this.props;
Expand All@@ -195,7 +236,86 @@ class BaseTreeContainer extends Component {

const element = Registry.resolve(_dashprivate_layout);

const props = dissoc('children', _dashprivate_layout.props);
// Hydrate components props
const childrenProps = pathOr(
[],
[
'children_props',
_dashprivate_layout.namespace,
_dashprivate_layout.type
],
_dashprivate_config
);
const props = pipe(
dissoc('children'),
...childrenProps
.map(childrenProp => {
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
let nodeValue;
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
found = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
frontPath.push(p.replace('[]', ''));
} else {
frontPath.push(p);
}
} else {
backPath.push(p);
}
});

node = rpath(frontPath, _dashprivate_layout.props);
if (node === undefined) {
return;
}
if (!node.length) {
return assocPath(frontPath, node);
}
const firstNode = rpath(backPath, node[0]);
if (!firstNode) {
return assocPath(frontPath, node);
}
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
return assocPath(
backPath,
this.wrapChildrenProp(
rpath(backPath, element),
elementPath
),
element
);
});
path = frontPath;
} else {
node = rpath(path, _dashprivate_layout.props);
if (node === undefined) {
return;
}
nodeValue = this.wrapChildrenProp(node, path);
}
return assocPath(path, nodeValue);
}
const node = _dashprivate_layout.props[childrenProp];
if (node !== undefined) {
return assoc(
childrenProp,
this.wrapChildrenProp(node, [childrenProp])
);
}
})
.filter(e => e !== undefined)
)(_dashprivate_layout.props);

if (type(props.id) === 'Object') {
// Turn object ids (for wildcards) into unique strings.
Expand Down
45 changes: 42 additions & 3 deletions dash/dash-renderer/src/actions/utils.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import {append, concat, has, path, type} from 'ramda';
import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';

/*
* requests_pathname_prefix is the new config parameter introduced in
Expand DownExpand Up@@ -27,11 +27,24 @@ export function urlBase(config) {
const propsChildren = ['props', 'children'];

// crawl a layout object or children array, apply a function on every object
export const crawlLayout = (object, func, currentPath = []) => {
export const crawlLayout = (
object,
func,
currentPath = [],
extraPath = undefined
) => {
if (Array.isArray(object)) {
// children array
object.forEach((child, i) => {
crawlLayout(child, func, append(i, currentPath));
if (extraPath) {
crawlLayout(
rpath(extraPath, child),
func,
concat(currentPath, concat([i], extraPath))
);
} else {
crawlLayout(child, func, append(i, currentPath));
}
});
} else if (type(object) === 'Object') {
func(object, currentPath);
Expand All@@ -41,6 +54,32 @@ export const crawlLayout = (object, func, currentPath = []) => {
const newPath = concat(currentPath, propsChildren);
crawlLayout(children, func, newPath);
}

const childrenProps = pathOr(
[],
[object.namespace, object.type],
window.__dashprivate_childrenProps
);
childrenProps.forEach(childrenProp => {
if (childrenProp.includes('[]')) {
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
);
}
});
}
};

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9eb975a
Add test component as prop.
T4rk1n Mar 11, 2022
3c91fd0
Add component as prop support
T4rk1n Mar 11, 2022
7f1f5eb
Add childrenProps to components json only if there is any.
T4rk1n Mar 14, 2022
be3fea4
Remove test component not children.
T4rk1n Mar 14, 2022
60367ec
Handle components in list.
T4rk1n Apr 14, 2022
253f03a
Add support for component in dictionaries props.
T4rk1n Apr 19, 2022
56bd57a
Lint fix.
T4rk1n Apr 21, 2022
e2cf8dc
Support components in list of dictionary.
T4rk1n Apr 25, 2022
cf7e6b3
Add checklist label support for components.
T4rk1n Apr 25, 2022
f8a2f3e
Add RadioItems label support for components.
T4rk1n Apr 25, 2022
dce32fd
Add dropdown label support for components.
T4rk1n Apr 25, 2022
d4a4998
Merge branch 'dev' into component-as-props
T4rk1n Apr 29, 2022
6e0b8f3
Add collect node, store children_props on generated class, renderer h…
T4rk1n Apr 29, 2022
63f7312
Improve checks.
T4rk1n Apr 29, 2022
fa1bc34
Exclude children from node collect, json childrenProps only if any.
T4rk1n May 2, 2022
7ea2ecb
Fix union rendering.
T4rk1n May 2, 2022
3a26a40
Fix render of union in arrayOf.
T4rk1n May 2, 2022
642fbea
Collect children props in component registry, serve in config.
T4rk1n May 2, 2022
854446c
Extract config to use in crawl layout.
T4rk1n May 2, 2022
8814d44
Merge branch 'dev' into component-as-props
T4rk1n May 3, 2022
62caf4e
Export renderer store.
T4rk1n May 3, 2022
d43e212
Cleanup/Proper handling of nodes.
T4rk1n May 4, 2022
c45f7ac
Store childrenProps on window.
T4rk1n May 5, 2022
8e4b1cf
Get base nodes to return non nested components props only.
T4rk1n May 26, 2022
8d4edfc
Move bracket to end of children prop path.
T4rk1n May 26, 2022
9b92930
Merge branch 'dev' into component-as-props
T4rk1n May 27, 2022
463b4ba
Change labels prop type to node only.
T4rk1n May 27, 2022
30b4b86
Remove elements from test ComponentAsProp
T4rk1n May 27, 2022
c20399f
Move base node iteration to component generator.
T4rk1n Jun 2, 2022
f27bee8
Merge branch 'dev' into component-as-props
T4rk1n Jun 2, 2022
3761b80
Update changelog.
T4rk1n Jun 2, 2022
6e57b0b
Add back deleted test debc026
T4rk1n Jun 2, 2022
ebc309a
Add test legacy components.
T4rk1n Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions @plotly/dash-test-components/src/components/ComponentAsProp.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';


const ComponentAsProp = (props) => {
const { element, id, shapeEl, list_of_shapes } = props;
return (
<div id={id}>
{shapeEl && shapeEl.header}
{element}
{shapeEl && shapeEl.footer}
{list_of_shapes && <ul>{list_of_shapes.map(e => <li key={e.value}>{e.label}</li>)}</ul> }
</div>
)
}

ComponentAsProp.propTypes = {
id: PropTypes.string,
element: PropTypes.node,

shapeEl: PropTypes.shape({
header: PropTypes.node,
footer: PropTypes.node,
}),

list_of_shapes: PropTypes.arrayOf(
PropTypes.exact({
label: PropTypes.node,
value: PropTypes.number,
})
)
}

export default ComponentAsProp;
4 changes: 3 additions & 1 deletion @plotly/dash-test-components/src/index.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import MyPersistedComponent from './components/MyPersistedComponent';
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';

export {
AsyncComponent,
Expand All@@ -15,5 +16,6 @@ export {
MyPersistedComponent,
MyPersistedComponentNested,
StyledComponent,
WidthComponent
WidthComponent,
ComponentAsProp,
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added

- [#2049](https://github.com/plotly/dash/pull/2043) Added `wait_for_class_to_equal` and `wait_for_contains_class` methods to `dash.testing`
- [#1965](https://github.com/plotly/dash/pull/1965) Add component as props.

## [2.4.1] - 2022-05-11

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,11 +104,7 @@ Checklist.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,8 +56,7 @@ Dropdown.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,11 +99,7 @@ RadioItems.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
from dash import Dash, dcc, html


def test_mdcap001_dcc_components_as_props(dash_dcc):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Checklist(
[
{"label": html.H2("H2 label"), "value": "h2"},
{"label": html.A("Link in checklist", href="#"), "value": "a"},
],
id="checklist",
),
dcc.RadioItems(
[
{"label": html.H3("on"), "value": "on"},
{"label": html.P("off"), "value": "off"},
],
id="radio-items",
),
dcc.Dropdown(
[
{"label": html.H4("h4"), "value": "h4"},
{"label": html.H6("h6"), "value": "h6"},
],
id="dropdown",
),
]
)

dash_dcc.start_server(app)

dash_dcc.wait_for_text_to_equal("#checklist h2", "H2 label")
dash_dcc.wait_for_text_to_equal("#checklist a", "Link in checklist")

dash_dcc.wait_for_text_to_equal("#radio-items h3", "on")
dash_dcc.wait_for_text_to_equal("#radio-items p", "off")

dash_dcc.find_element("#dropdown").click()
dash_dcc.wait_for_text_to_equal("#dropdown h4", "h4")
dash_dcc.wait_for_text_to_equal("#dropdown h6", "h6")
5 changes: 2 additions & 3 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import APIController from './APIController.react';
import Loading from './components/core/Loading.react';
import Toolbar from './components/core/Toolbar.react';
import Reloader from './components/core/Reloader.react';
import getConfigFromDOM from './config';
import {setHooks, setConfig} from './actions/index';
import {type, memoizeWith, identity} from 'ramda';

Expand DownExpand Up@@ -35,9 +36,7 @@ class UnconnectedAppContainer extends React.Component {

UNSAFE_componentWillMount() {
const {dispatch} = this.props;
const config = JSON.parse(
document.getElementById('_dash-config').textContent
);
const config = getConfigFromDOM();

// preset common request params in the config
config.fetch = {
Expand Down
1 change: 0 additions & 1 deletion dash/dash-renderer/src/AppProvider.react.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import AppContainer from './AppContainer.react';

const AppProvider = ({hooks}: any) => {
const [{store}] = useState(() => new Store());

return (
<Provider store={store}>
<AppContainer hooks={hooks} />
Expand Down
122 changes: 121 additions & 1 deletion dash/dash-renderer/src/TreeContainer.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,17 +4,23 @@ import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
concat,
dissoc,
equals,
isEmpty,
isNil,
has,
keys,
map,
mergeRight,
pick,
pickBy,
pipe,
propOr,
path as rpath,
pathOr,
type
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
Expand DownExpand Up@@ -67,6 +73,15 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}

function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
has('type', obj) &&
has('namespace', obj) &&
has('props', obj)
);
}

const TreeContainer = memo(props => (
<DashContext.Consumer>
{context => (
Expand DownExpand Up@@ -180,6 +195,32 @@ class BaseTreeContainer extends Component {
);
}

wrapChildrenProp(node, childrenProp) {
if (Array.isArray(node)) {
return node.map((n, i) =>
isDryComponent(n)
? this.createContainer(
this.props,
n,
concat(this.props._dashprivate_path, [
'props',
...childrenProp,
i
])
)
: n
);
}
if (!isDryComponent(node)) {
return node;
}
return this.createContainer(
this.props,
node,
concat(this.props._dashprivate_path, ['props', ...childrenProp])
);
}

getComponent(_dashprivate_layout, children, loading_state, setProps) {
const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} =
this.props;
Expand All@@ -195,7 +236,86 @@ class BaseTreeContainer extends Component {

const element = Registry.resolve(_dashprivate_layout);

const props = dissoc('children', _dashprivate_layout.props);
// Hydrate components props
const childrenProps = pathOr(
[],
[
'children_props',
_dashprivate_layout.namespace,
_dashprivate_layout.type
],
_dashprivate_config
);
const props = pipe(
dissoc('children'),
...childrenProps
.map(childrenProp => {
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
let nodeValue;
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
found = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
frontPath.push(p.replace('[]', ''));
} else {
frontPath.push(p);
}
} else {
backPath.push(p);
}
});

node = rpath(frontPath, _dashprivate_layout.props);
if (node === undefined) {
return;
}
if (!node.length) {
return assocPath(frontPath, node);
}
const firstNode = rpath(backPath, node[0]);
if (!firstNode) {
return assocPath(frontPath, node);
}
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
return assocPath(
backPath,
this.wrapChildrenProp(
rpath(backPath, element),
elementPath
),
element
);
});
path = frontPath;
} else {
node = rpath(path, _dashprivate_layout.props);
if (node === undefined) {
return;
}
nodeValue = this.wrapChildrenProp(node, path);
}
return assocPath(path, nodeValue);
}
const node = _dashprivate_layout.props[childrenProp];
if (node !== undefined) {
return assoc(
childrenProp,
this.wrapChildrenProp(node, [childrenProp])
);
}
})
.filter(e => e !== undefined)
)(_dashprivate_layout.props);

if (type(props.id) === 'Object') {
// Turn object ids (for wildcards) into unique strings.
Expand Down
45 changes: 42 additions & 3 deletions dash/dash-renderer/src/actions/utils.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import {append, concat, has, path, type} from 'ramda';
import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';

/*
* requests_pathname_prefix is the new config parameter introduced in
Expand DownExpand Up@@ -27,11 +27,24 @@ export function urlBase(config) {
const propsChildren = ['props', 'children'];

// crawl a layout object or children array, apply a function on every object
export const crawlLayout = (object, func, currentPath = []) => {
export const crawlLayout = (
object,
func,
currentPath = [],
extraPath = undefined
) => {
if (Array.isArray(object)) {
// children array
object.forEach((child, i) => {
crawlLayout(child, func, append(i, currentPath));
if (extraPath) {
crawlLayout(
rpath(extraPath, child),
func,
concat(currentPath, concat([i], extraPath))
);
} else {
crawlLayout(child, func, append(i, currentPath));
}
});
} else if (type(object) === 'Object') {
func(object, currentPath);
Expand All@@ -41,6 +54,32 @@ export const crawlLayout = (object, func, currentPath = []) => {
const newPath = concat(currentPath, propsChildren);
crawlLayout(children, func, newPath);
}

const childrenProps = pathOr(
[],
[object.namespace, object.type],
window.__dashprivate_childrenProps
);
childrenProps.forEach(childrenProp => {
if (childrenProp.includes('[]')) {
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
);
}
});
}
};

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9eb975a
Add test component as prop.
T4rk1n Mar 11, 2022
3c91fd0
Add component as prop support
T4rk1n Mar 11, 2022
7f1f5eb
Add childrenProps to components json only if there is any.
T4rk1n Mar 14, 2022
be3fea4
Remove test component not children.
T4rk1n Mar 14, 2022
60367ec
Handle components in list.
T4rk1n Apr 14, 2022
253f03a
Add support for component in dictionaries props.
T4rk1n Apr 19, 2022
56bd57a
Lint fix.
T4rk1n Apr 21, 2022
e2cf8dc
Support components in list of dictionary.
T4rk1n Apr 25, 2022
cf7e6b3
Add checklist label support for components.
T4rk1n Apr 25, 2022
f8a2f3e
Add RadioItems label support for components.
T4rk1n Apr 25, 2022
dce32fd
Add dropdown label support for components.
T4rk1n Apr 25, 2022
d4a4998
Merge branch 'dev' into component-as-props
T4rk1n Apr 29, 2022
6e0b8f3
Add collect node, store children_props on generated class, renderer h…
T4rk1n Apr 29, 2022
63f7312
Improve checks.
T4rk1n Apr 29, 2022
fa1bc34
Exclude children from node collect, json childrenProps only if any.
T4rk1n May 2, 2022
7ea2ecb
Fix union rendering.
T4rk1n May 2, 2022
3a26a40
Fix render of union in arrayOf.
T4rk1n May 2, 2022
642fbea
Collect children props in component registry, serve in config.
T4rk1n May 2, 2022
854446c
Extract config to use in crawl layout.
T4rk1n May 2, 2022
8814d44
Merge branch 'dev' into component-as-props
T4rk1n May 3, 2022
62caf4e
Export renderer store.
T4rk1n May 3, 2022
d43e212
Cleanup/Proper handling of nodes.
T4rk1n May 4, 2022
c45f7ac
Store childrenProps on window.
T4rk1n May 5, 2022
8e4b1cf
Get base nodes to return non nested components props only.
T4rk1n May 26, 2022
8d4edfc
Move bracket to end of children prop path.
T4rk1n May 26, 2022
9b92930
Merge branch 'dev' into component-as-props
T4rk1n May 27, 2022
463b4ba
Change labels prop type to node only.
T4rk1n May 27, 2022
30b4b86
Remove elements from test ComponentAsProp
T4rk1n May 27, 2022
c20399f
Move base node iteration to component generator.
T4rk1n Jun 2, 2022
f27bee8
Merge branch 'dev' into component-as-props
T4rk1n Jun 2, 2022
3761b80
Update changelog.
T4rk1n Jun 2, 2022
6e57b0b
Add back deleted test debc026
T4rk1n Jun 2, 2022
ebc309a
Add test legacy components.
T4rk1n Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions @plotly/dash-test-components/src/components/ComponentAsProp.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';


const ComponentAsProp = (props) => {
const { element, id, shapeEl, list_of_shapes } = props;
return (
<div id={id}>
{shapeEl && shapeEl.header}
{element}
{shapeEl && shapeEl.footer}
{list_of_shapes && <ul>{list_of_shapes.map(e => <li key={e.value}>{e.label}</li>)}</ul> }
</div>
)
}

ComponentAsProp.propTypes = {
id: PropTypes.string,
element: PropTypes.node,

shapeEl: PropTypes.shape({
header: PropTypes.node,
footer: PropTypes.node,
}),

list_of_shapes: PropTypes.arrayOf(
PropTypes.exact({
label: PropTypes.node,
value: PropTypes.number,
})
)
}

export default ComponentAsProp;
4 changes: 3 additions & 1 deletion @plotly/dash-test-components/src/index.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import MyPersistedComponent from './components/MyPersistedComponent';
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';

export {
AsyncComponent,
Expand All@@ -15,5 +16,6 @@ export {
MyPersistedComponent,
MyPersistedComponentNested,
StyledComponent,
WidthComponent
WidthComponent,
ComponentAsProp,
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added

- [#2049](https://github.com/plotly/dash/pull/2043) Added `wait_for_class_to_equal` and `wait_for_contains_class` methods to `dash.testing`
- [#1965](https://github.com/plotly/dash/pull/1965) Add component as props.

## [2.4.1] - 2022-05-11

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,11 +104,7 @@ Checklist.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,8 +56,7 @@ Dropdown.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,11 +99,7 @@ RadioItems.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
from dash import Dash, dcc, html


def test_mdcap001_dcc_components_as_props(dash_dcc):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Checklist(
[
{"label": html.H2("H2 label"), "value": "h2"},
{"label": html.A("Link in checklist", href="#"), "value": "a"},
],
id="checklist",
),
dcc.RadioItems(
[
{"label": html.H3("on"), "value": "on"},
{"label": html.P("off"), "value": "off"},
],
id="radio-items",
),
dcc.Dropdown(
[
{"label": html.H4("h4"), "value": "h4"},
{"label": html.H6("h6"), "value": "h6"},
],
id="dropdown",
),
]
)

dash_dcc.start_server(app)

dash_dcc.wait_for_text_to_equal("#checklist h2", "H2 label")
dash_dcc.wait_for_text_to_equal("#checklist a", "Link in checklist")

dash_dcc.wait_for_text_to_equal("#radio-items h3", "on")
dash_dcc.wait_for_text_to_equal("#radio-items p", "off")

dash_dcc.find_element("#dropdown").click()
dash_dcc.wait_for_text_to_equal("#dropdown h4", "h4")
dash_dcc.wait_for_text_to_equal("#dropdown h6", "h6")
5 changes: 2 additions & 3 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import APIController from './APIController.react';
import Loading from './components/core/Loading.react';
import Toolbar from './components/core/Toolbar.react';
import Reloader from './components/core/Reloader.react';
import getConfigFromDOM from './config';
import {setHooks, setConfig} from './actions/index';
import {type, memoizeWith, identity} from 'ramda';

Expand DownExpand Up@@ -35,9 +36,7 @@ class UnconnectedAppContainer extends React.Component {

UNSAFE_componentWillMount() {
const {dispatch} = this.props;
const config = JSON.parse(
document.getElementById('_dash-config').textContent
);
const config = getConfigFromDOM();

// preset common request params in the config
config.fetch = {
Expand Down
1 change: 0 additions & 1 deletion dash/dash-renderer/src/AppProvider.react.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import AppContainer from './AppContainer.react';

const AppProvider = ({hooks}: any) => {
const [{store}] = useState(() => new Store());

return (
<Provider store={store}>
<AppContainer hooks={hooks} />
Expand Down
122 changes: 121 additions & 1 deletion dash/dash-renderer/src/TreeContainer.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,17 +4,23 @@ import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
concat,
dissoc,
equals,
isEmpty,
isNil,
has,
keys,
map,
mergeRight,
pick,
pickBy,
pipe,
propOr,
path as rpath,
pathOr,
type
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
Expand DownExpand Up@@ -67,6 +73,15 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}

function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
has('type', obj) &&
has('namespace', obj) &&
has('props', obj)
);
}

const TreeContainer = memo(props => (
<DashContext.Consumer>
{context => (
Expand DownExpand Up@@ -180,6 +195,32 @@ class BaseTreeContainer extends Component {
);
}

wrapChildrenProp(node, childrenProp) {
if (Array.isArray(node)) {
return node.map((n, i) =>
isDryComponent(n)
? this.createContainer(
this.props,
n,
concat(this.props._dashprivate_path, [
'props',
...childrenProp,
i
])
)
: n
);
}
if (!isDryComponent(node)) {
return node;
}
return this.createContainer(
this.props,
node,
concat(this.props._dashprivate_path, ['props', ...childrenProp])
);
}

getComponent(_dashprivate_layout, children, loading_state, setProps) {
const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} =
this.props;
Expand All@@ -195,7 +236,86 @@ class BaseTreeContainer extends Component {

const element = Registry.resolve(_dashprivate_layout);

const props = dissoc('children', _dashprivate_layout.props);
// Hydrate components props
const childrenProps = pathOr(
[],
[
'children_props',
_dashprivate_layout.namespace,
_dashprivate_layout.type
],
_dashprivate_config
);
const props = pipe(
dissoc('children'),
...childrenProps
.map(childrenProp => {
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
let nodeValue;
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
found = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
frontPath.push(p.replace('[]', ''));
} else {
frontPath.push(p);
}
} else {
backPath.push(p);
}
});

node = rpath(frontPath, _dashprivate_layout.props);
if (node === undefined) {
return;
}
if (!node.length) {
return assocPath(frontPath, node);
}
const firstNode = rpath(backPath, node[0]);
if (!firstNode) {
return assocPath(frontPath, node);
}
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
return assocPath(
backPath,
this.wrapChildrenProp(
rpath(backPath, element),
elementPath
),
element
);
});
path = frontPath;
} else {
node = rpath(path, _dashprivate_layout.props);
if (node === undefined) {
return;
}
nodeValue = this.wrapChildrenProp(node, path);
}
return assocPath(path, nodeValue);
}
const node = _dashprivate_layout.props[childrenProp];
if (node !== undefined) {
return assoc(
childrenProp,
this.wrapChildrenProp(node, [childrenProp])
);
}
})
.filter(e => e !== undefined)
)(_dashprivate_layout.props);

if (type(props.id) === 'Object') {
// Turn object ids (for wildcards) into unique strings.
Expand Down
45 changes: 42 additions & 3 deletions dash/dash-renderer/src/actions/utils.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import {append, concat, has, path, type} from 'ramda';
import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';

/*
* requests_pathname_prefix is the new config parameter introduced in
Expand DownExpand Up@@ -27,11 +27,24 @@ export function urlBase(config) {
const propsChildren = ['props', 'children'];

// crawl a layout object or children array, apply a function on every object
export const crawlLayout = (object, func, currentPath = []) => {
export const crawlLayout = (
object,
func,
currentPath = [],
extraPath = undefined
) => {
if (Array.isArray(object)) {
// children array
object.forEach((child, i) => {
crawlLayout(child, func, append(i, currentPath));
if (extraPath) {
crawlLayout(
rpath(extraPath, child),
func,
concat(currentPath, concat([i], extraPath))
);
} else {
crawlLayout(child, func, append(i, currentPath));
}
});
} else if (type(object) === 'Object') {
func(object, currentPath);
Expand All@@ -41,6 +54,32 @@ export const crawlLayout = (object, func, currentPath = []) => {
const newPath = concat(currentPath, propsChildren);
crawlLayout(children, func, newPath);
}

const childrenProps = pathOr(
[],
[object.namespace, object.type],
window.__dashprivate_childrenProps
);
childrenProps.forEach(childrenProp => {
if (childrenProp.includes('[]')) {
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
);
}
});
}
};

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9eb975a
Add test component as prop.
T4rk1n Mar 11, 2022
3c91fd0
Add component as prop support
T4rk1n Mar 11, 2022
7f1f5eb
Add childrenProps to components json only if there is any.
T4rk1n Mar 14, 2022
be3fea4
Remove test component not children.
T4rk1n Mar 14, 2022
60367ec
Handle components in list.
T4rk1n Apr 14, 2022
253f03a
Add support for component in dictionaries props.
T4rk1n Apr 19, 2022
56bd57a
Lint fix.
T4rk1n Apr 21, 2022
e2cf8dc
Support components in list of dictionary.
T4rk1n Apr 25, 2022
cf7e6b3
Add checklist label support for components.
T4rk1n Apr 25, 2022
f8a2f3e
Add RadioItems label support for components.
T4rk1n Apr 25, 2022
dce32fd
Add dropdown label support for components.
T4rk1n Apr 25, 2022
d4a4998
Merge branch 'dev' into component-as-props
T4rk1n Apr 29, 2022
6e0b8f3
Add collect node, store children_props on generated class, renderer h…
T4rk1n Apr 29, 2022
63f7312
Improve checks.
T4rk1n Apr 29, 2022
fa1bc34
Exclude children from node collect, json childrenProps only if any.
T4rk1n May 2, 2022
7ea2ecb
Fix union rendering.
T4rk1n May 2, 2022
3a26a40
Fix render of union in arrayOf.
T4rk1n May 2, 2022
642fbea
Collect children props in component registry, serve in config.
T4rk1n May 2, 2022
854446c
Extract config to use in crawl layout.
T4rk1n May 2, 2022
8814d44
Merge branch 'dev' into component-as-props
T4rk1n May 3, 2022
62caf4e
Export renderer store.
T4rk1n May 3, 2022
d43e212
Cleanup/Proper handling of nodes.
T4rk1n May 4, 2022
c45f7ac
Store childrenProps on window.
T4rk1n May 5, 2022
8e4b1cf
Get base nodes to return non nested components props only.
T4rk1n May 26, 2022
8d4edfc
Move bracket to end of children prop path.
T4rk1n May 26, 2022
9b92930
Merge branch 'dev' into component-as-props
T4rk1n May 27, 2022
463b4ba
Change labels prop type to node only.
T4rk1n May 27, 2022
30b4b86
Remove elements from test ComponentAsProp
T4rk1n May 27, 2022
c20399f
Move base node iteration to component generator.
T4rk1n Jun 2, 2022
f27bee8
Merge branch 'dev' into component-as-props
T4rk1n Jun 2, 2022
3761b80
Update changelog.
T4rk1n Jun 2, 2022
6e57b0b
Add back deleted test debc026
T4rk1n Jun 2, 2022
ebc309a
Add test legacy components.
T4rk1n Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions @plotly/dash-test-components/src/components/ComponentAsProp.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';


const ComponentAsProp = (props) => {
const { element, id, shapeEl, list_of_shapes } = props;
return (
<div id={id}>
{shapeEl && shapeEl.header}
{element}
{shapeEl && shapeEl.footer}
{list_of_shapes && <ul>{list_of_shapes.map(e => <li key={e.value}>{e.label}</li>)}</ul> }
</div>
)
}

ComponentAsProp.propTypes = {
id: PropTypes.string,
element: PropTypes.node,

shapeEl: PropTypes.shape({
header: PropTypes.node,
footer: PropTypes.node,
}),

list_of_shapes: PropTypes.arrayOf(
PropTypes.exact({
label: PropTypes.node,
value: PropTypes.number,
})
)
}

export default ComponentAsProp;
4 changes: 3 additions & 1 deletion @plotly/dash-test-components/src/index.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import MyPersistedComponent from './components/MyPersistedComponent';
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';

export {
AsyncComponent,
Expand All@@ -15,5 +16,6 @@ export {
MyPersistedComponent,
MyPersistedComponentNested,
StyledComponent,
WidthComponent
WidthComponent,
ComponentAsProp,
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added

- [#2049](https://github.com/plotly/dash/pull/2043) Added `wait_for_class_to_equal` and `wait_for_contains_class` methods to `dash.testing`
- [#1965](https://github.com/plotly/dash/pull/1965) Add component as props.

## [2.4.1] - 2022-05-11

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,11 +104,7 @@ Checklist.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,8 +56,7 @@ Dropdown.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,11 +99,7 @@ RadioItems.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
from dash import Dash, dcc, html


def test_mdcap001_dcc_components_as_props(dash_dcc):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Checklist(
[
{"label": html.H2("H2 label"), "value": "h2"},
{"label": html.A("Link in checklist", href="#"), "value": "a"},
],
id="checklist",
),
dcc.RadioItems(
[
{"label": html.H3("on"), "value": "on"},
{"label": html.P("off"), "value": "off"},
],
id="radio-items",
),
dcc.Dropdown(
[
{"label": html.H4("h4"), "value": "h4"},
{"label": html.H6("h6"), "value": "h6"},
],
id="dropdown",
),
]
)

dash_dcc.start_server(app)

dash_dcc.wait_for_text_to_equal("#checklist h2", "H2 label")
dash_dcc.wait_for_text_to_equal("#checklist a", "Link in checklist")

dash_dcc.wait_for_text_to_equal("#radio-items h3", "on")
dash_dcc.wait_for_text_to_equal("#radio-items p", "off")

dash_dcc.find_element("#dropdown").click()
dash_dcc.wait_for_text_to_equal("#dropdown h4", "h4")
dash_dcc.wait_for_text_to_equal("#dropdown h6", "h6")
5 changes: 2 additions & 3 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import APIController from './APIController.react';
import Loading from './components/core/Loading.react';
import Toolbar from './components/core/Toolbar.react';
import Reloader from './components/core/Reloader.react';
import getConfigFromDOM from './config';
import {setHooks, setConfig} from './actions/index';
import {type, memoizeWith, identity} from 'ramda';

Expand DownExpand Up@@ -35,9 +36,7 @@ class UnconnectedAppContainer extends React.Component {

UNSAFE_componentWillMount() {
const {dispatch} = this.props;
const config = JSON.parse(
document.getElementById('_dash-config').textContent
);
const config = getConfigFromDOM();

// preset common request params in the config
config.fetch = {
Expand Down
1 change: 0 additions & 1 deletion dash/dash-renderer/src/AppProvider.react.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import AppContainer from './AppContainer.react';

const AppProvider = ({hooks}: any) => {
const [{store}] = useState(() => new Store());

return (
<Provider store={store}>
<AppContainer hooks={hooks} />
Expand Down
122 changes: 121 additions & 1 deletion dash/dash-renderer/src/TreeContainer.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,17 +4,23 @@ import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
concat,
dissoc,
equals,
isEmpty,
isNil,
has,
keys,
map,
mergeRight,
pick,
pickBy,
pipe,
propOr,
path as rpath,
pathOr,
type
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
Expand DownExpand Up@@ -67,6 +73,15 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}

function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
has('type', obj) &&
has('namespace', obj) &&
has('props', obj)
);
}

const TreeContainer = memo(props => (
<DashContext.Consumer>
{context => (
Expand DownExpand Up@@ -180,6 +195,32 @@ class BaseTreeContainer extends Component {
);
}

wrapChildrenProp(node, childrenProp) {
if (Array.isArray(node)) {
return node.map((n, i) =>
isDryComponent(n)
? this.createContainer(
this.props,
n,
concat(this.props._dashprivate_path, [
'props',
...childrenProp,
i
])
)
: n
);
}
if (!isDryComponent(node)) {
return node;
}
return this.createContainer(
this.props,
node,
concat(this.props._dashprivate_path, ['props', ...childrenProp])
);
}

getComponent(_dashprivate_layout, children, loading_state, setProps) {
const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} =
this.props;
Expand All@@ -195,7 +236,86 @@ class BaseTreeContainer extends Component {

const element = Registry.resolve(_dashprivate_layout);

const props = dissoc('children', _dashprivate_layout.props);
// Hydrate components props
const childrenProps = pathOr(
[],
[
'children_props',
_dashprivate_layout.namespace,
_dashprivate_layout.type
],
_dashprivate_config
);
const props = pipe(
dissoc('children'),
...childrenProps
.map(childrenProp => {
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
let nodeValue;
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
found = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
frontPath.push(p.replace('[]', ''));
} else {
frontPath.push(p);
}
} else {
backPath.push(p);
}
});

node = rpath(frontPath, _dashprivate_layout.props);
if (node === undefined) {
return;
}
if (!node.length) {
return assocPath(frontPath, node);
}
const firstNode = rpath(backPath, node[0]);
if (!firstNode) {
return assocPath(frontPath, node);
}
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
return assocPath(
backPath,
this.wrapChildrenProp(
rpath(backPath, element),
elementPath
),
element
);
});
path = frontPath;
} else {
node = rpath(path, _dashprivate_layout.props);
if (node === undefined) {
return;
}
nodeValue = this.wrapChildrenProp(node, path);
}
return assocPath(path, nodeValue);
}
const node = _dashprivate_layout.props[childrenProp];
if (node !== undefined) {
return assoc(
childrenProp,
this.wrapChildrenProp(node, [childrenProp])
);
}
})
.filter(e => e !== undefined)
)(_dashprivate_layout.props);

if (type(props.id) === 'Object') {
// Turn object ids (for wildcards) into unique strings.
Expand Down
45 changes: 42 additions & 3 deletions dash/dash-renderer/src/actions/utils.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import {append, concat, has, path, type} from 'ramda';
import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';

/*
* requests_pathname_prefix is the new config parameter introduced in
Expand DownExpand Up@@ -27,11 +27,24 @@ export function urlBase(config) {
const propsChildren = ['props', 'children'];

// crawl a layout object or children array, apply a function on every object
export const crawlLayout = (object, func, currentPath = []) => {
export const crawlLayout = (
object,
func,
currentPath = [],
extraPath = undefined
) => {
if (Array.isArray(object)) {
// children array
object.forEach((child, i) => {
crawlLayout(child, func, append(i, currentPath));
if (extraPath) {
crawlLayout(
rpath(extraPath, child),
func,
concat(currentPath, concat([i], extraPath))
);
} else {
crawlLayout(child, func, append(i, currentPath));
}
});
} else if (type(object) === 'Object') {
func(object, currentPath);
Expand All@@ -41,6 +54,32 @@ export const crawlLayout = (object, func, currentPath = []) => {
const newPath = concat(currentPath, propsChildren);
crawlLayout(children, func, newPath);
}

const childrenProps = pathOr(
[],
[object.namespace, object.type],
window.__dashprivate_childrenProps
);
childrenProps.forEach(childrenProp => {
if (childrenProp.includes('[]')) {
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
);
}
});
}
};

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9eb975a
Add test component as prop.
T4rk1n Mar 11, 2022
3c91fd0
Add component as prop support
T4rk1n Mar 11, 2022
7f1f5eb
Add childrenProps to components json only if there is any.
T4rk1n Mar 14, 2022
be3fea4
Remove test component not children.
T4rk1n Mar 14, 2022
60367ec
Handle components in list.
T4rk1n Apr 14, 2022
253f03a
Add support for component in dictionaries props.
T4rk1n Apr 19, 2022
56bd57a
Lint fix.
T4rk1n Apr 21, 2022
e2cf8dc
Support components in list of dictionary.
T4rk1n Apr 25, 2022
cf7e6b3
Add checklist label support for components.
T4rk1n Apr 25, 2022
f8a2f3e
Add RadioItems label support for components.
T4rk1n Apr 25, 2022
dce32fd
Add dropdown label support for components.
T4rk1n Apr 25, 2022
d4a4998
Merge branch 'dev' into component-as-props
T4rk1n Apr 29, 2022
6e0b8f3
Add collect node, store children_props on generated class, renderer h…
T4rk1n Apr 29, 2022
63f7312
Improve checks.
T4rk1n Apr 29, 2022
fa1bc34
Exclude children from node collect, json childrenProps only if any.
T4rk1n May 2, 2022
7ea2ecb
Fix union rendering.
T4rk1n May 2, 2022
3a26a40
Fix render of union in arrayOf.
T4rk1n May 2, 2022
642fbea
Collect children props in component registry, serve in config.
T4rk1n May 2, 2022
854446c
Extract config to use in crawl layout.
T4rk1n May 2, 2022
8814d44
Merge branch 'dev' into component-as-props
T4rk1n May 3, 2022
62caf4e
Export renderer store.
T4rk1n May 3, 2022
d43e212
Cleanup/Proper handling of nodes.
T4rk1n May 4, 2022
c45f7ac
Store childrenProps on window.
T4rk1n May 5, 2022
8e4b1cf
Get base nodes to return non nested components props only.
T4rk1n May 26, 2022
8d4edfc
Move bracket to end of children prop path.
T4rk1n May 26, 2022
9b92930
Merge branch 'dev' into component-as-props
T4rk1n May 27, 2022
463b4ba
Change labels prop type to node only.
T4rk1n May 27, 2022
30b4b86
Remove elements from test ComponentAsProp
T4rk1n May 27, 2022
c20399f
Move base node iteration to component generator.
T4rk1n Jun 2, 2022
f27bee8
Merge branch 'dev' into component-as-props
T4rk1n Jun 2, 2022
3761b80
Update changelog.
T4rk1n Jun 2, 2022
6e57b0b
Add back deleted test debc026
T4rk1n Jun 2, 2022
ebc309a
Add test legacy components.
T4rk1n Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions @plotly/dash-test-components/src/components/ComponentAsProp.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';


const ComponentAsProp = (props) => {
const { element, id, shapeEl, list_of_shapes } = props;
return (
<div id={id}>
{shapeEl && shapeEl.header}
{element}
{shapeEl && shapeEl.footer}
{list_of_shapes && <ul>{list_of_shapes.map(e => <li key={e.value}>{e.label}</li>)}</ul> }
</div>
)
}

ComponentAsProp.propTypes = {
id: PropTypes.string,
element: PropTypes.node,

shapeEl: PropTypes.shape({
header: PropTypes.node,
footer: PropTypes.node,
}),

list_of_shapes: PropTypes.arrayOf(
PropTypes.exact({
label: PropTypes.node,
value: PropTypes.number,
})
)
}

export default ComponentAsProp;
4 changes: 3 additions & 1 deletion @plotly/dash-test-components/src/index.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import MyPersistedComponent from './components/MyPersistedComponent';
import MyPersistedComponentNested from './components/MyPersistedComponentNested';
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';

export {
AsyncComponent,
Expand All@@ -15,5 +16,6 @@ export {
MyPersistedComponent,
MyPersistedComponentNested,
StyledComponent,
WidthComponent
WidthComponent,
ComponentAsProp,
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added

- [#2049](https://github.com/plotly/dash/pull/2043) Added `wait_for_class_to_equal` and `wait_for_contains_class` methods to `dash.testing`
- [#1965](https://github.com/plotly/dash/pull/1965) Add component as props.

## [2.4.1] - 2022-05-11

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,11 +104,7 @@ Checklist.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,8 +56,7 @@ Dropdown.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,11 +99,7 @@ RadioItems.propTypes = {
/**
* The option's label
*/
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
label: PropTypes.node.isRequired,

/**
* The value of the option. This value
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
from dash import Dash, dcc, html


def test_mdcap001_dcc_components_as_props(dash_dcc):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Checklist(
[
{"label": html.H2("H2 label"), "value": "h2"},
{"label": html.A("Link in checklist", href="#"), "value": "a"},
],
id="checklist",
),
dcc.RadioItems(
[
{"label": html.H3("on"), "value": "on"},
{"label": html.P("off"), "value": "off"},
],
id="radio-items",
),
dcc.Dropdown(
[
{"label": html.H4("h4"), "value": "h4"},
{"label": html.H6("h6"), "value": "h6"},
],
id="dropdown",
),
]
)

dash_dcc.start_server(app)

dash_dcc.wait_for_text_to_equal("#checklist h2", "H2 label")
dash_dcc.wait_for_text_to_equal("#checklist a", "Link in checklist")

dash_dcc.wait_for_text_to_equal("#radio-items h3", "on")
dash_dcc.wait_for_text_to_equal("#radio-items p", "off")

dash_dcc.find_element("#dropdown").click()
dash_dcc.wait_for_text_to_equal("#dropdown h4", "h4")
dash_dcc.wait_for_text_to_equal("#dropdown h6", "h6")
5 changes: 2 additions & 3 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import APIController from './APIController.react';
import Loading from './components/core/Loading.react';
import Toolbar from './components/core/Toolbar.react';
import Reloader from './components/core/Reloader.react';
import getConfigFromDOM from './config';
import {setHooks, setConfig} from './actions/index';
import {type, memoizeWith, identity} from 'ramda';

Expand DownExpand Up@@ -35,9 +36,7 @@ class UnconnectedAppContainer extends React.Component {

UNSAFE_componentWillMount() {
const {dispatch} = this.props;
const config = JSON.parse(
document.getElementById('_dash-config').textContent
);
const config = getConfigFromDOM();

// preset common request params in the config
config.fetch = {
Expand Down
1 change: 0 additions & 1 deletion dash/dash-renderer/src/AppProvider.react.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import AppContainer from './AppContainer.react';

const AppProvider = ({hooks}: any) => {
const [{store}] = useState(() => new Store());

return (
<Provider store={store}>
<AppContainer hooks={hooks} />
Expand Down
122 changes: 121 additions & 1 deletion dash/dash-renderer/src/TreeContainer.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,17 +4,23 @@ import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
concat,
dissoc,
equals,
isEmpty,
isNil,
has,
keys,
map,
mergeRight,
pick,
pickBy,
pipe,
propOr,
path as rpath,
pathOr,
type
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
Expand DownExpand Up@@ -67,6 +73,15 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}

function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
has('type', obj) &&
has('namespace', obj) &&
has('props', obj)
);
}

const TreeContainer = memo(props => (
<DashContext.Consumer>
{context => (
Expand DownExpand Up@@ -180,6 +195,32 @@ class BaseTreeContainer extends Component {
);
}

wrapChildrenProp(node, childrenProp) {
if (Array.isArray(node)) {
return node.map((n, i) =>
isDryComponent(n)
? this.createContainer(
this.props,
n,
concat(this.props._dashprivate_path, [
'props',
...childrenProp,
i
])
)
: n
);
}
if (!isDryComponent(node)) {
return node;
}
return this.createContainer(
this.props,
node,
concat(this.props._dashprivate_path, ['props', ...childrenProp])
);
}

getComponent(_dashprivate_layout, children, loading_state, setProps) {
const {_dashprivate_config, _dashprivate_dispatch, _dashprivate_error} =
this.props;
Expand All@@ -195,7 +236,86 @@ class BaseTreeContainer extends Component {

const element = Registry.resolve(_dashprivate_layout);

const props = dissoc('children', _dashprivate_layout.props);
// Hydrate components props
const childrenProps = pathOr(
[],
[
'children_props',
_dashprivate_layout.namespace,
_dashprivate_layout.type
],
_dashprivate_config
);
const props = pipe(
dissoc('children'),
...childrenProps
.map(childrenProp => {
if (childrenProp.includes('.')) {
let path = childrenProp.split('.');
let node;
let nodeValue;
if (childrenProp.includes('[]')) {
let frontPath = [],
backPath = [],
found = false;
path.forEach(p => {
if (!found) {
if (p.includes('[]')) {
found = true;
frontPath.push(p.replace('[]', ''));
} else {
frontPath.push(p);
}
} else {
backPath.push(p);
}
});

node = rpath(frontPath, _dashprivate_layout.props);
if (node === undefined) {
return;
}
if (!node.length) {
return assocPath(frontPath, node);
}
const firstNode = rpath(backPath, node[0]);
if (!firstNode) {
return assocPath(frontPath, node);
}
nodeValue = node.map((element, i) => {
const elementPath = concat(
frontPath,
concat([i], backPath)
);
return assocPath(
backPath,
this.wrapChildrenProp(
rpath(backPath, element),
elementPath
),
element
);
});
path = frontPath;
} else {
node = rpath(path, _dashprivate_layout.props);
if (node === undefined) {
return;
}
nodeValue = this.wrapChildrenProp(node, path);
}
return assocPath(path, nodeValue);
}
const node = _dashprivate_layout.props[childrenProp];
if (node !== undefined) {
return assoc(
childrenProp,
this.wrapChildrenProp(node, [childrenProp])
);
}
})
.filter(e => e !== undefined)
)(_dashprivate_layout.props);

if (type(props.id) === 'Object') {
// Turn object ids (for wildcards) into unique strings.
Expand Down
45 changes: 42 additions & 3 deletions dash/dash-renderer/src/actions/utils.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import {append, concat, has, path, type} from 'ramda';
import {append, concat, has, path, pathOr, type, path as rpath} from 'ramda';

/*
* requests_pathname_prefix is the new config parameter introduced in
Expand DownExpand Up@@ -27,11 +27,24 @@ export function urlBase(config) {
const propsChildren = ['props', 'children'];

// crawl a layout object or children array, apply a function on every object
export const crawlLayout = (object, func, currentPath = []) => {
export const crawlLayout = (
object,
func,
currentPath = [],
extraPath = undefined
) => {
if (Array.isArray(object)) {
// children array
object.forEach((child, i) => {
crawlLayout(child, func, append(i, currentPath));
if (extraPath) {
crawlLayout(
rpath(extraPath, child),
func,
concat(currentPath, concat([i], extraPath))
);
} else {
crawlLayout(child, func, append(i, currentPath));
}
});
} else if (type(object) === 'Object') {
func(object, currentPath);
Expand All@@ -41,6 +54,32 @@ export const crawlLayout = (object, func, currentPath = []) => {
const newPath = concat(currentPath, propsChildren);
crawlLayout(children, func, newPath);
}

const childrenProps = pathOr(
[],
[object.namespace, object.type],
window.__dashprivate_childrenProps
);
childrenProps.forEach(childrenProp => {
if (childrenProp.includes('[]')) {
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
);
}
});
}
};

Expand Down
Loading