diff --git a/dash/dash.py b/dash/dash.py index 0f04362689..48bce0984c 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -35,7 +35,6 @@ from . import _watch from . import _configs - _default_index = ''' @@ -49,6 +48,7 @@ ''' @@ -64,10 +64,12 @@ _re_index_entry = re.compile(r'{%app_entry%}') _re_index_config = re.compile(r'{%config%}') _re_index_scripts = re.compile(r'{%scripts%}') +_re_renderer_scripts = re.compile(r'{%renderer%}') _re_index_entry_id = re.compile(r'id="react-entry-point"') _re_index_config_id = re.compile(r'id="_dash-config"') _re_index_scripts_id = re.compile(r'src=".*dash[-_]renderer.*"') +_re_renderer_scripts_id = re.compile(r'id="_dash-renderer') # pylint: disable=too-many-instance-attributes @@ -271,6 +273,9 @@ def _add_url(self, name, view_func, methods=('GET',)): # e.g. for adding authentication with flask_login self.routes.append(name) + # default renderer string + self.renderer = 'var renderer = new DashRenderer();' + @property def layout(self): return self._layout @@ -464,6 +469,13 @@ def _generate_config_html(self): '' ).format(json.dumps(self._config())) + def _generate_renderer(self): + return ( + '' + ).format(self.renderer) + def _generate_meta_html(self): has_ie_compat = any( x.get('http-equiv', '') == 'X-UA-Compatible' @@ -527,6 +539,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument css = self._generate_css_dist_html() config = self._generate_config_html() metas = self._generate_meta_html() + renderer = self._generate_renderer() title = getattr(self, 'title', 'Dash') if self._favicon: @@ -547,12 +560,14 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument index = self.interpolate_index( metas=metas, title=title, css=css, config=config, - scripts=scripts, app_entry=_app_entry, favicon=favicon) + scripts=scripts, app_entry=_app_entry, favicon=favicon, + renderer=renderer) checks = ( (_re_index_entry_id.search(index), '#react-entry-point'), (_re_index_config_id.search(index), '#_dash-configs'), (_re_index_scripts_id.search(index), 'dash-renderer'), + (_re_renderer_scripts_id.search(index), 'new DashRenderer'), ) missing = [missing for check, missing in checks if not check] @@ -569,7 +584,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument def interpolate_index(self, metas='', title='', css='', config='', - scripts='', app_entry='', favicon=''): + scripts='', app_entry='', favicon='', renderer=''): """ Called to create the initial HTML string that is loaded on page. Override this method to provide you own custom HTML. @@ -589,19 +604,22 @@ def interpolate_index(self, **kwargs): {app_entry} {config} {scripts} + {renderer} '''.format( app_entry=kwargs.get('app_entry'), config=kwargs.get('config'), - scripts=kwargs.get('scripts')) + scripts=kwargs.get('scripts'), + renderer=kwargs.get('renderer')) :param metas: Collected & formatted meta tags. :param title: The title of the app. :param css: Collected & formatted css dependencies as tags. :param config: Configs needed by dash-renderer. :param scripts: Collected & formatted scripts tags. + :param renderer: A script tag that instantiates the DashRenderer. :param app_entry: Where the app will render. :param favicon: A favicon tag if found in assets folder. :return: The interpolated HTML string for the index. @@ -613,6 +631,7 @@ def interpolate_index(self, **kwargs): config=config, scripts=scripts, favicon=favicon, + renderer=renderer, app_entry=app_entry) def dependencies(self): diff --git a/tests/test_integration.py b/tests/test_integration.py index 33fc72e69b..6fada65c2a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -9,6 +9,9 @@ import dash_core_components as dcc import dash_flow_example +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys + import dash from dash.dependencies import Input, Output @@ -59,7 +62,15 @@ def update_output(value): self.percy_snapshot(name='simple-callback-1') input1 = self.wait_for_element_by_id('input') - input1.clear() + + chain = (ActionChains(self.driver) + .click(input1) + .send_keys(Keys.HOME) + .key_down(Keys.SHIFT) + .send_keys(Keys.END) + .key_up(Keys.SHIFT) + .send_keys(Keys.DELETE)) + chain.perform() input1.send_keys('hello world') @@ -69,7 +80,8 @@ def update_output(value): self.assertEqual( call_count.value, # an initial call to retrieve the first value - 1 + + # and one for clearing the input + 2 + # one for each hello world character len('hello world') ) @@ -111,7 +123,14 @@ def update_text(data): self.percy_snapshot(name='wildcard-callback-1') input1 = self.wait_for_element_by_id('input') - input1.clear() + chain = (ActionChains(self.driver) + .click(input1) + .send_keys(Keys.HOME) + .key_down(Keys.SHIFT) + .send_keys(Keys.END) + .key_up(Keys.SHIFT) + .send_keys(Keys.DELETE)) + chain.perform() input1.send_keys('hello world') @@ -121,7 +140,8 @@ def update_text(data): self.assertEqual( input_call_count.value, # an initial call - 1 + + # and a call for clearing the input + 2 + # one for each hello world character len('hello world') ) @@ -326,6 +346,7 @@ def test_index_customization(self): + +
With request hooks
+ + + ''' + + app.layout = html.Div([ + dcc.Input( + id='input', + value='initial value' + ), + html.Div( + html.Div([ + html.Div(id='output-1'), + html.Div(id='output-pre'), + html.Div(id='output-post') + ]) + ) + ]) + + @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + def update_output(value): + return value + + self.startServer(app) + + input1 = self.wait_for_element_by_id('input') + chain = (ActionChains(self.driver) + .click(input1) + .send_keys(Keys.HOME) + .key_down(Keys.SHIFT) + .send_keys(Keys.END) + .key_up(Keys.SHIFT) + .send_keys(Keys.DELETE)) + chain.perform() + + input1.send_keys('fire request hooks') + + self.wait_for_text_to_equal('#output-1', 'fire request hooks') + self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!') + self.wait_for_text_to_equal('#output-post', 'request_post changed this text!') + + self.percy_snapshot(name='request-hooks') + + def test_with_custom_renderer_interpolated(self): + + renderer = ''' + + ''' + class CustomDash(dash.Dash): + + def interpolate_index(self, **kwargs): + return ''' + + + + My App + + + +
My custom header
+ {app_entry} + {config} + {scripts} + {renderer} + + + + '''.format( + app_entry=kwargs['app_entry'], + config=kwargs['config'], + scripts=kwargs['scripts'], + renderer=renderer) + + app = CustomDash() + + app.layout = html.Div([ + dcc.Input( + id='input', + value='initial value' + ), + html.Div( + html.Div([ + html.Div(id='output-1'), + html.Div(id='output-pre'), + html.Div(id='output-post') + ]) + ) + ]) + + @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + def update_output(value): + return value + + self.startServer(app) + + input1 = self.wait_for_element_by_id('input') + chain = (ActionChains(self.driver) + .click(input1) + .send_keys(Keys.HOME) + .key_down(Keys.SHIFT) + .send_keys(Keys.END) + .key_up(Keys.SHIFT) + .send_keys(Keys.DELETE)) + chain.perform() + + input1.send_keys('fire request hooks') + + self.wait_for_text_to_equal('#output-1', 'fire request hooks') + self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!') + self.wait_for_text_to_equal('#output-post', 'request_post changed this text!') + + self.percy_snapshot(name='request-hooks interpolated') + def test_late_component_register(self): app = dash.Dash()