Skip to content

Commit f22973d

Browse files
Merge pull request #367 from plotly/standalone_dash-renderer
Standalone dash renderer (for custom hooks) - March 1
2 parents 2a4c63e + 1c59379 commit f22973d

File tree

2 files changed

+215
-8
lines changed

2 files changed

+215
-8
lines changed

dash/dash.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from . import _watch
3636
from . import _configs
3737

38-
3938
_default_index = '''<!DOCTYPE html>
4039
<html>
4140
<head>
@@ -49,6 +48,7 @@
4948
<footer>
5049
{%config%}
5150
{%scripts%}
51+
{%renderer%}
5252
</footer>
5353
</body>
5454
</html>'''
@@ -64,10 +64,12 @@
6464
_re_index_entry = re.compile(r'{%app_entry%}')
6565
_re_index_config = re.compile(r'{%config%}')
6666
_re_index_scripts = re.compile(r'{%scripts%}')
67+
_re_renderer_scripts = re.compile(r'{%renderer%}')
6768

6869
_re_index_entry_id = re.compile(r'id="react-entry-point"')
6970
_re_index_config_id = re.compile(r'id="_dash-config"')
7071
_re_index_scripts_id = re.compile(r'src=".*dash[-_]renderer.*"')
72+
_re_renderer_scripts_id = re.compile(r'id="_dash-renderer')
7173

7274

7375
# pylint: disable=too-many-instance-attributes
@@ -271,6 +273,9 @@ def _add_url(self, name, view_func, methods=('GET',)):
271273
# e.g. for adding authentication with flask_login
272274
self.routes.append(name)
273275

276+
# default renderer string
277+
self.renderer = 'var renderer = new DashRenderer();'
278+
274279
@property
275280
def layout(self):
276281
return self._layout
@@ -464,6 +469,13 @@ def _generate_config_html(self):
464469
'</script>'
465470
).format(json.dumps(self._config()))
466471

472+
def _generate_renderer(self):
473+
return (
474+
'<script id="_dash-renderer" type="application/javascript">'
475+
'{}'
476+
'</script>'
477+
).format(self.renderer)
478+
467479
def _generate_meta_html(self):
468480
has_ie_compat = any(
469481
x.get('http-equiv', '') == 'X-UA-Compatible'
@@ -527,6 +539,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
527539
css = self._generate_css_dist_html()
528540
config = self._generate_config_html()
529541
metas = self._generate_meta_html()
542+
renderer = self._generate_renderer()
530543
title = getattr(self, 'title', 'Dash')
531544

532545
if self._favicon:
@@ -547,12 +560,14 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
547560

548561
index = self.interpolate_index(
549562
metas=metas, title=title, css=css, config=config,
550-
scripts=scripts, app_entry=_app_entry, favicon=favicon)
563+
scripts=scripts, app_entry=_app_entry, favicon=favicon,
564+
renderer=renderer)
551565

552566
checks = (
553567
(_re_index_entry_id.search(index), '#react-entry-point'),
554568
(_re_index_config_id.search(index), '#_dash-configs'),
555569
(_re_index_scripts_id.search(index), 'dash-renderer'),
570+
(_re_renderer_scripts_id.search(index), 'new DashRenderer'),
556571
)
557572
missing = [missing for check, missing in checks if not check]
558573

@@ -569,7 +584,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
569584

570585
def interpolate_index(self,
571586
metas='', title='', css='', config='',
572-
scripts='', app_entry='', favicon=''):
587+
scripts='', app_entry='', favicon='', renderer=''):
573588
"""
574589
Called to create the initial HTML string that is loaded on page.
575590
Override this method to provide you own custom HTML.
@@ -589,19 +604,22 @@ def interpolate_index(self, **kwargs):
589604
{app_entry}
590605
{config}
591606
{scripts}
607+
{renderer}
592608
<div id="custom-footer">My custom footer</div>
593609
</body>
594610
</html>
595611
'''.format(
596612
app_entry=kwargs.get('app_entry'),
597613
config=kwargs.get('config'),
598-
scripts=kwargs.get('scripts'))
614+
scripts=kwargs.get('scripts'),
615+
renderer=kwargs.get('renderer'))
599616
600617
:param metas: Collected & formatted meta tags.
601618
:param title: The title of the app.
602619
:param css: Collected & formatted css dependencies as <link> tags.
603620
:param config: Configs needed by dash-renderer.
604621
:param scripts: Collected & formatted scripts tags.
622+
:param renderer: A script tag that instantiates the DashRenderer.
605623
:param app_entry: Where the app will render.
606624
:param favicon: A favicon <link> tag if found in assets folder.
607625
:return: The interpolated HTML string for the index.
@@ -613,6 +631,7 @@ def interpolate_index(self, **kwargs):
613631
config=config,
614632
scripts=scripts,
615633
favicon=favicon,
634+
renderer=renderer,
616635
app_entry=app_entry)
617636

618637
def dependencies(self):

tests/test_integration.py

+192-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import dash_core_components as dcc
1010
import dash_flow_example
1111

12+
from selenium.webdriver.common.action_chains import ActionChains
13+
from selenium.webdriver.common.keys import Keys
14+
1215
import dash
1316

1417
from dash.dependencies import Input, Output
@@ -59,7 +62,15 @@ def update_output(value):
5962
self.percy_snapshot(name='simple-callback-1')
6063

6164
input1 = self.wait_for_element_by_id('input')
62-
input1.clear()
65+
66+
chain = (ActionChains(self.driver)
67+
.click(input1)
68+
.send_keys(Keys.HOME)
69+
.key_down(Keys.SHIFT)
70+
.send_keys(Keys.END)
71+
.key_up(Keys.SHIFT)
72+
.send_keys(Keys.DELETE))
73+
chain.perform()
6374

6475
input1.send_keys('hello world')
6576

@@ -69,7 +80,8 @@ def update_output(value):
6980
self.assertEqual(
7081
call_count.value,
7182
# an initial call to retrieve the first value
72-
1 +
83+
# and one for clearing the input
84+
2 +
7385
# one for each hello world character
7486
len('hello world')
7587
)
@@ -111,7 +123,14 @@ def update_text(data):
111123
self.percy_snapshot(name='wildcard-callback-1')
112124

113125
input1 = self.wait_for_element_by_id('input')
114-
input1.clear()
126+
chain = (ActionChains(self.driver)
127+
.click(input1)
128+
.send_keys(Keys.HOME)
129+
.key_down(Keys.SHIFT)
130+
.send_keys(Keys.END)
131+
.key_up(Keys.SHIFT)
132+
.send_keys(Keys.DELETE))
133+
chain.perform()
115134

116135
input1.send_keys('hello world')
117136

@@ -121,7 +140,8 @@ def update_text(data):
121140
self.assertEqual(
122141
input_call_count.value,
123142
# an initial call
124-
1 +
143+
# and a call for clearing the input
144+
2 +
125145
# one for each hello world character
126146
len('hello world')
127147
)
@@ -326,6 +346,7 @@ def test_index_customization(self):
326346
<footer>
327347
{%config%}
328348
{%scripts%}
349+
{%renderer%}
329350
</footer>
330351
<div id="custom-footer">My custom footer</div>
331352
<script>
@@ -378,6 +399,7 @@ def test_assets(self):
378399
<footer>
379400
{%config%}
380401
{%scripts%}
402+
{%renderer%}
381403
</footer>
382404
</body>
383405
</html>
@@ -493,6 +515,7 @@ def test_external_files_init(self):
493515
<footer>
494516
{%config%}
495517
{%scripts%}
518+
{%renderer%}
496519
</footer>
497520
</body>
498521
</html>
@@ -532,6 +555,171 @@ def create_layout():
532555
self.startServer(app)
533556
time.sleep(0.5)
534557

558+
def test_with_custom_renderer(self):
559+
app = dash.Dash(__name__)
560+
561+
app.index_string = '''
562+
<!DOCTYPE html>
563+
<html>
564+
<head>
565+
{%metas%}
566+
<title>{%title%}</title>
567+
{%favicon%}
568+
{%css%}
569+
</head>
570+
<body>
571+
<div>Testing custom DashRenderer</div>
572+
{%app_entry%}
573+
<footer>
574+
{%config%}
575+
{%scripts%}
576+
<script id="_dash-renderer" type="application/javascript">
577+
console.log('firing up a custom renderer!')
578+
const renderer = new DashRenderer({
579+
request_pre: () => {
580+
var output = document.getElementById('output-pre')
581+
if(output) {
582+
output.innerHTML = 'request_pre changed this text!';
583+
}
584+
},
585+
request_post: () => {
586+
var output = document.getElementById('output-post')
587+
if(output) {
588+
output.innerHTML = 'request_post changed this text!';
589+
}
590+
}
591+
})
592+
</script>
593+
</footer>
594+
<div>With request hooks</div>
595+
</body>
596+
</html>
597+
'''
598+
599+
app.layout = html.Div([
600+
dcc.Input(
601+
id='input',
602+
value='initial value'
603+
),
604+
html.Div(
605+
html.Div([
606+
html.Div(id='output-1'),
607+
html.Div(id='output-pre'),
608+
html.Div(id='output-post')
609+
])
610+
)
611+
])
612+
613+
@app.callback(Output('output-1', 'children'), [Input('input', 'value')])
614+
def update_output(value):
615+
return value
616+
617+
self.startServer(app)
618+
619+
input1 = self.wait_for_element_by_id('input')
620+
chain = (ActionChains(self.driver)
621+
.click(input1)
622+
.send_keys(Keys.HOME)
623+
.key_down(Keys.SHIFT)
624+
.send_keys(Keys.END)
625+
.key_up(Keys.SHIFT)
626+
.send_keys(Keys.DELETE))
627+
chain.perform()
628+
629+
input1.send_keys('fire request hooks')
630+
631+
self.wait_for_text_to_equal('#output-1', 'fire request hooks')
632+
self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!')
633+
self.wait_for_text_to_equal('#output-post', 'request_post changed this text!')
634+
635+
self.percy_snapshot(name='request-hooks')
636+
637+
def test_with_custom_renderer_interpolated(self):
638+
639+
renderer = '''
640+
<script id="_dash-renderer" type="application/javascript">
641+
console.log('firing up a custom renderer!')
642+
const renderer = new DashRenderer({
643+
request_pre: () => {
644+
var output = document.getElementById('output-pre')
645+
if(output) {
646+
output.innerHTML = 'request_pre changed this text!';
647+
}
648+
},
649+
request_post: () => {
650+
var output = document.getElementById('output-post')
651+
if(output) {
652+
output.innerHTML = 'request_post changed this text!';
653+
}
654+
}
655+
})
656+
</script>
657+
'''
658+
class CustomDash(dash.Dash):
659+
660+
def interpolate_index(self, **kwargs):
661+
return '''
662+
<!DOCTYPE html>
663+
<html>
664+
<head>
665+
<title>My App</title>
666+
</head>
667+
<body>
668+
669+
<div id="custom-header">My custom header</div>
670+
{app_entry}
671+
{config}
672+
{scripts}
673+
{renderer}
674+
<div id="custom-footer">My custom footer</div>
675+
</body>
676+
</html>
677+
'''.format(
678+
app_entry=kwargs['app_entry'],
679+
config=kwargs['config'],
680+
scripts=kwargs['scripts'],
681+
renderer=renderer)
682+
683+
app = CustomDash()
684+
685+
app.layout = html.Div([
686+
dcc.Input(
687+
id='input',
688+
value='initial value'
689+
),
690+
html.Div(
691+
html.Div([
692+
html.Div(id='output-1'),
693+
html.Div(id='output-pre'),
694+
html.Div(id='output-post')
695+
])
696+
)
697+
])
698+
699+
@app.callback(Output('output-1', 'children'), [Input('input', 'value')])
700+
def update_output(value):
701+
return value
702+
703+
self.startServer(app)
704+
705+
input1 = self.wait_for_element_by_id('input')
706+
chain = (ActionChains(self.driver)
707+
.click(input1)
708+
.send_keys(Keys.HOME)
709+
.key_down(Keys.SHIFT)
710+
.send_keys(Keys.END)
711+
.key_up(Keys.SHIFT)
712+
.send_keys(Keys.DELETE))
713+
chain.perform()
714+
715+
input1.send_keys('fire request hooks')
716+
717+
self.wait_for_text_to_equal('#output-1', 'fire request hooks')
718+
self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!')
719+
self.wait_for_text_to_equal('#output-post', 'request_post changed this text!')
720+
721+
self.percy_snapshot(name='request-hooks interpolated')
722+
535723
def test_late_component_register(self):
536724
app = dash.Dash()
537725

0 commit comments

Comments
 (0)