diff --git a/dev-requirements.txt b/dev-requirements.txt
index 454fad2..d89b342 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,7 +1,8 @@
dash_core_components==0.33.0
dash_html_components==0.11.0rc5
-dash==0.28.0
+dash==0.28.5
percy
selenium
mock
six
+tests/requirements/component_props-0.0.1.tar.gz
diff --git a/src/TreeContainer.js b/src/TreeContainer.js
index b3b44f2..a94e8e4 100644
--- a/src/TreeContainer.js
+++ b/src/TreeContainer.js
@@ -6,81 +6,88 @@ import PropTypes from 'prop-types';
import Registry from './registry';
import NotifyObservers from './components/core/NotifyObservers.react';
-export default class TreeContainer extends Component {
- shouldComponentUpdate(nextProps) {
- return nextProps.layout !== this.props.layout;
- }
-
- render() {
- return render(this.props.layout);
+function isComponent(c) {
+ switch (R.type(c)) {
+ case 'Array':
+ return R.any(isComponent)(c);
+ case 'Object':
+ return (
+ c.hasOwnProperty('namespace') &&
+ c.hasOwnProperty('props') &&
+ c.hasOwnProperty('type')
+ );
+ default:
+ return false;
}
}
-TreeContainer.propTypes = {
- layout: PropTypes.object,
-};
-
-function render(component) {
- if (
- R.contains(R.type(component), ['String', 'Number', 'Null', 'Boolean'])
- ) {
- return component;
- }
+function hydrateProps(props) {
+ const replace = {};
+ Object.entries(props)
+ .filter(([_, v]) => isComponent(v))
+ .forEach(([k, v]) => {
+ if (R.type(v) === 'Array') {
+ // TODO add key ?.
+ replace[k] = v.map(c => {
+ if (!isComponent(c)) {
+ return c;
+ }
+ const newProps = hydrateProps(c.props);
+ return hydrateComponent(
+ c.type,
+ c.namespace,
+ newProps
+ );
+ });
+ } else {
+ const newProps = hydrateProps(v.props);
+ replace[k] = hydrateComponent(
+ v.type,
+ v.namespace,
+ newProps
+ );
+ }
+ });
+ return R.merge(props, replace);
+}
- // Create list of child elements
- let children;
+function hydrateComponent(
+ component_name,
+ namespace,
+ props,
+) {
+ const element = Registry.resolve(component_name, namespace);
- const componentProps = R.propOr({}, 'props', component);
+ const component = React.createElement(
+ element,
+ props
+ );
- if (
- !R.has('props', component) ||
- !R.has('children', component.props) ||
- typeof component.props.children === 'undefined'
- ) {
- // No children
- children = [];
- } else if (
- R.contains(R.type(component.props.children), [
- 'String',
- 'Number',
- 'Null',
- 'Boolean',
- ])
- ) {
- children = [component.props.children];
- } else {
- // One or multiple objects
- // Recursively render the tree
- // TODO - I think we should pass in `key` here.
- children = (Array.isArray(componentProps.children)
- ? componentProps.children
- : [componentProps.children]
- ).map(render);
- }
+ // eslint-disable-next-line
+ return {component};
+}
- if (!component.type) {
- /* eslint-disable no-console */
- console.error(R.type(component), component);
- /* eslint-enable no-console */
- throw new Error('component.type is undefined');
- }
- if (!component.namespace) {
- /* eslint-disable no-console */
- console.error(R.type(component), component);
- /* eslint-enable no-console */
- throw new Error('component.namespace is undefined');
+export default class TreeContainer extends Component {
+ shouldComponentUpdate(nextProps) {
+ return nextProps.layout !== this.props.layout;
}
- const element = Registry.resolve(component.type, component.namespace);
- const parent = React.createElement(
- element,
- R.omit(['children'], component.props),
- ...children
- );
+ render() {
+ const { layout } = this.props;
+ if (
+ R.contains(R.type(layout), ['String', 'Number', 'Null', 'Boolean'])
+ ) {
+ return layout;
+ }
- return {parent};
+ return hydrateComponent(
+ layout.type,
+ layout.namespace,
+ hydrateProps(layout.props)
+ );
+ }
}
-render.propTypes = {
- children: PropTypes.object,
+TreeContainer.propTypes = {
+ layout: PropTypes.object,
};
diff --git a/tests/requirements/component_props-0.0.1.tar.gz b/tests/requirements/component_props-0.0.1.tar.gz
new file mode 100644
index 0000000..a7f6c57
Binary files /dev/null and b/tests/requirements/component_props-0.0.1.tar.gz differ
diff --git a/tests/test_render.py b/tests/test_render.py
index dc1c68e..f21ca47 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -4,6 +4,11 @@
from dash.development.base_component import Component
import dash_html_components as html
import dash_core_components as dcc
+from dash.exceptions import PreventUpdate
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.wait import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
from .IntegrationTests import IntegrationTests
from .utils import assert_clean_console, wait_for
from multiprocessing import Value
@@ -12,6 +17,9 @@
import itertools
import json
import unittest
+import component_props
+
+TIMEOUT = 20
class Tests(IntegrationTests):
@@ -19,31 +27,15 @@ def setUp(self):
pass
def wait_for_element_by_css_selector(self, selector):
- start_time = time.time()
- exception = Exception('Time ran out, {} not found'.format(selector))
- while time.time() < start_time + 20:
- try:
- return self.driver.find_element_by_css_selector(selector)
- except Exception as e:
- exception = e
- pass
- time.sleep(0.25)
- raise exception
+ return WebDriverWait(self.driver, TIMEOUT).until(
+ EC.presence_of_element_located((By.CSS_SELECTOR, selector))
+ )
def wait_for_text_to_equal(self, selector, assertion_text):
- start_time = time.time()
- exception = Exception('Time ran out, {} on {} not found'.format(
- assertion_text, selector))
- while time.time() < start_time + 20:
- el = self.wait_for_element_by_css_selector(selector)
- try:
- return self.assertEqual(el.text, assertion_text)
- except Exception as e:
- exception = e
- pass
- time.sleep(0.25)
-
- raise exception
+ return WebDriverWait(self.driver, TIMEOUT).until(
+ EC.text_to_be_present_in_element((By.CSS_SELECTOR, selector),
+ assertion_text)
+ )
def request_queue_assertions(
self, check_rejected=True, expected_length=None):
@@ -1866,3 +1858,40 @@ def update_output(*args):
self.assertTrue(timestamp_2.value > timestamp_1.value)
self.assertEqual(call_count.value, 4)
self.percy_snapshot('button-2 click again')
+
+ def test_component_as_props(self):
+ app = dash.Dash(__name__)
+ app.layout = html.Div([
+ html.Button('button', id='mod-btn'),
+ component_props.ComponentProps(
+ id='c-props',
+ component_props=html.Div('Component as prop'),
+ comp_array=[
+ html.Div([html.Div('1'), html.Div('2'), 3]),
+ html.Div('4')
+ ]
+ ),
+ html.Div(id='output')
+ ])
+
+ @app.callback(Output('c-props', 'component_props'),
+ [Input('mod-btn', 'n_clicks')])
+ def on_clicks(n_clicks):
+ if n_clicks is None:
+ raise PreventUpdate
+
+ return html.H2('New component from a callback')
+
+ self.startServer(app)
+
+ self.wait_for_text_to_equal('#c-props > div:nth-child(1)', 'Component as prop')
+ for i in range(1, 3):
+ self.wait_for_text_to_equal(
+ '#c-props > div:nth-child(2) > div:nth-child(1) > div:nth-child({})'.format(i),
+ str(i)
+ )
+ self.wait_for_text_to_equal('#c-props > div:nth-child(2) > div:nth-child(1)', '3')
+ self.wait_for_text_to_equal('#c-props > div:nth-child(2) > div:nth-child(2)', '4')
+
+ self.wait_for_element_by_css_selector('#mod-btn').click()
+ self.wait_for_text_to_equal('#c-props > h2', 'New component from a callback')