Skip to content

Commit

Permalink
Merge pull request #367 from plotly/standalone_dash-renderer
Browse files Browse the repository at this point in the history
Standalone dash renderer (for custom hooks) - March 1
  • Loading branch information
valentijnnieman committed Feb 28, 2019
2 parents 2a4c63e + 1c59379 commit f22973d
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 8 deletions.
27 changes: 23 additions & 4 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
from . import _watch
from . import _configs


_default_index = '''<!DOCTYPE html>
<html>
<head>
Expand All @@ -49,6 +48,7 @@
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
</body>
</html>'''
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -464,6 +469,13 @@ def _generate_config_html(self):
'</script>'
).format(json.dumps(self._config()))

def _generate_renderer(self):
return (
'<script id="_dash-renderer" type="application/javascript">'
'{}'
'</script>'
).format(self.renderer)

def _generate_meta_html(self):
has_ie_compat = any(
x.get('http-equiv', '') == 'X-UA-Compatible'
Expand Down Expand Up @@ -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:
Expand All @@ -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]

Expand All @@ -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.
Expand All @@ -589,19 +604,22 @@ def interpolate_index(self, **kwargs):
{app_entry}
{config}
{scripts}
{renderer}
<div id="custom-footer">My custom footer</div>
</body>
</html>
'''.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 <link> 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 <link> tag if found in assets folder.
:return: The interpolated HTML string for the index.
Expand All @@ -613,6 +631,7 @@ def interpolate_index(self, **kwargs):
config=config,
scripts=scripts,
favicon=favicon,
renderer=renderer,
app_entry=app_entry)

def dependencies(self):
Expand Down
196 changes: 192 additions & 4 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')

Expand All @@ -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')
)
Expand Down Expand Up @@ -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')

Expand All @@ -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')
)
Expand Down Expand Up @@ -326,6 +346,7 @@ def test_index_customization(self):
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
<div id="custom-footer">My custom footer</div>
<script>
Expand Down Expand Up @@ -378,6 +399,7 @@ def test_assets(self):
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
</body>
</html>
Expand Down Expand Up @@ -493,6 +515,7 @@ def test_external_files_init(self):
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
</body>
</html>
Expand Down Expand Up @@ -532,6 +555,171 @@ def create_layout():
self.startServer(app)
time.sleep(0.5)

def test_with_custom_renderer(self):
app = dash.Dash(__name__)

app.index_string = '''
<!DOCTYPE html>
<html>
<head>
{%metas%}
<title>{%title%}</title>
{%favicon%}
{%css%}
</head>
<body>
<div>Testing custom DashRenderer</div>
{%app_entry%}
<footer>
{%config%}
{%scripts%}
<script id="_dash-renderer" type="application/javascript">
console.log('firing up a custom renderer!')
const renderer = new DashRenderer({
request_pre: () => {
var output = document.getElementById('output-pre')
if(output) {
output.innerHTML = 'request_pre changed this text!';
}
},
request_post: () => {
var output = document.getElementById('output-post')
if(output) {
output.innerHTML = 'request_post changed this text!';
}
}
})
</script>
</footer>
<div>With request hooks</div>
</body>
</html>
'''

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 = '''
<script id="_dash-renderer" type="application/javascript">
console.log('firing up a custom renderer!')
const renderer = new DashRenderer({
request_pre: () => {
var output = document.getElementById('output-pre')
if(output) {
output.innerHTML = 'request_pre changed this text!';
}
},
request_post: () => {
var output = document.getElementById('output-post')
if(output) {
output.innerHTML = 'request_post changed this text!';
}
}
})
</script>
'''
class CustomDash(dash.Dash):

def interpolate_index(self, **kwargs):
return '''
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="custom-header">My custom header</div>
{app_entry}
{config}
{scripts}
{renderer}
<div id="custom-footer">My custom footer</div>
</body>
</html>
'''.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()

Expand Down

0 comments on commit f22973d

Please sign in to comment.