Skip to content

Commit 5d9f578

Browse files
jjaraalmMarc-Andre-Rivet
authored andcommitted
Inline clientside callbacks (#967)
1 parent 48d8e70 commit 5d9f578

File tree

4 files changed

+118
-16
lines changed

4 files changed

+118
-16
lines changed

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## Unreleased
6+
### Added
7+
- [#967](https://github.com/plotly/dash/pull/967) Adds support for defining
8+
clientside JavaScript callbacks via inline strings.
9+
510
## [1.6.1] - 2019-11-14
611
### Fixed
712
- [#1006](https://github.com/plotly/dash/pull/1006) Fix IE11 / ES5 compatibility and validation issues

Diff for: dash/dash.py

+67-14
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ def __init__(
307307
# list of dependencies
308308
self.callback_map = {}
309309

310+
# list of inline scripts
311+
self._inline_scripts = []
312+
310313
# index_string has special setter so can't go in config
311314
self._index_string = ""
312315
self.index_string = index_string
@@ -636,6 +639,10 @@ def _generate_scripts_html(self):
636639
if isinstance(src, dict)
637640
else '<script src="{}"></script>'.format(src)
638641
for src in srcs
642+
] +
643+
[
644+
'<script>{}</script>'.format(src)
645+
for src in self._inline_scripts
639646
]
640647
)
641648

@@ -1194,13 +1201,14 @@ def clientside_callback(
11941201
(JavaScript) function instead of a Python function.
11951202
11961203
Unlike `@app.calllback`, `clientside_callback` is not a decorator:
1197-
it takes a
1204+
it takes either a
11981205
`dash.dependencies.ClientsideFunction(namespace, function_name)`
11991206
argument that describes which JavaScript function to call
12001207
(Dash will look for the JavaScript function at
1201-
`window[namespace][function_name]`).
1208+
`window.dash_clientside[namespace][function_name]`), or it may take
1209+
a string argument that contains the clientside function source.
12021210
1203-
For example:
1211+
For example, when using a `dash.dependencies.ClientsideFunction`:
12041212
```
12051213
app.clientside_callback(
12061214
ClientsideFunction('my_clientside_library', 'my_function'),
@@ -1211,16 +1219,17 @@ def clientside_callback(
12111219
```
12121220
12131221
With this signature, Dash's front-end will call
1214-
`window.my_clientside_library.my_function` with the current
1215-
values of the `value` properties of the components
1216-
`my-input` and `another-input` whenever those values change.
1217-
1218-
Include a JavaScript file by including it your `assets/` folder.
1219-
The file can be named anything but you'll need to assign the
1220-
function's namespace to the `window`. For example, this file might
1221-
look like:
1222+
`window.dash_clientside.my_clientside_library.my_function` with the
1223+
current values of the `value` properties of the components `my-input`
1224+
and `another-input` whenever those values change.
1225+
1226+
Include a JavaScript file by including it your `assets/` folder. The
1227+
file can be named anything but you'll need to assign the function's
1228+
namespace to the `window.dash_clientside` namespace. For example,
1229+
this file might look:
12221230
```
1223-
window.my_clientside_library = {
1231+
window.dash_clientside = window.dash_clientside || {};
1232+
window.dash_clientside.my_clientside_library = {
12241233
my_function: function(input_value_1, input_value_2) {
12251234
return (
12261235
parseFloat(input_value_1, 10) +
@@ -1229,10 +1238,54 @@ def clientside_callback(
12291238
}
12301239
}
12311240
```
1241+
1242+
Alternatively, you can pass the JavaScript source directly to
1243+
`clientside_callback`. In this case, the same example would look like:
1244+
```
1245+
app.clientside_callback(
1246+
'''
1247+
function(input_value_1, input_value_2) {
1248+
return (
1249+
parseFloat(input_value_1, 10) +
1250+
parseFloat(input_value_2, 10)
1251+
);
1252+
}
1253+
''',
1254+
Output('my-div' 'children'),
1255+
[Input('my-input', 'value'),
1256+
Input('another-input', 'value')]
1257+
)
1258+
```
12321259
"""
12331260
self._validate_callback(output, inputs, state)
12341261
callback_id = _create_callback_id(output)
12351262

1263+
# If JS source is explicitly given, create a namespace and function
1264+
# name, then inject the code.
1265+
if isinstance(clientside_function, str):
1266+
1267+
out0 = output
1268+
if isinstance(output, (list, tuple)):
1269+
out0 = output[0]
1270+
1271+
namespace = '_dashprivate_{}'.format(out0.component_id)
1272+
function_name = '{}'.format(out0.component_property)
1273+
1274+
self._inline_scripts.append(
1275+
"""
1276+
var clientside = window.dash_clientside = window.dash_clientside || {{}};
1277+
var ns = clientside["{0}"] = clientside["{0}"] || {{}};
1278+
ns["{1}"] = {2};
1279+
""".format(namespace.replace('"', '\\"'),
1280+
function_name.replace('"', '\\"'),
1281+
clientside_function)
1282+
)
1283+
1284+
# Callback is stored in an external asset.
1285+
else:
1286+
namespace = clientside_function.namespace
1287+
function_name = clientside_function.function_name
1288+
12361289
self.callback_map[callback_id] = {
12371290
"inputs": [
12381291
{"id": c.component_id, "property": c.component_property}
@@ -1243,8 +1296,8 @@ def clientside_callback(
12431296
for c in state
12441297
],
12451298
"clientside_function": {
1246-
"namespace": clientside_function.namespace,
1247-
"function_name": clientside_function.function_name,
1299+
"namespace": namespace,
1300+
"function_name": function_name,
12481301
},
12491302
}
12501303

Diff for: dash/dependencies.py

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ class ClientsideFunction:
3636
# pylint: disable=too-few-public-methods
3737
def __init__(self, namespace=None, function_name=None):
3838

39+
if namespace.startswith('_dashprivate_'):
40+
raise ValueError("Namespaces cannot start with '_dashprivate_'.")
41+
3942
if namespace in ['PreventUpdate', 'no_update']:
4043
raise ValueError('"{}" is a forbidden namespace in'
4144
' dash_clientside.'.format(namespace))

Diff for: tests/integration/clientside/test_clientside.py

+43-2
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ def test_clsd005_clientside_fails_when_returning_a_promise(dash_duo):
262262
dash_duo.wait_for_text_to_equal("#side-effect", "side effect")
263263
dash_duo.wait_for_text_to_equal("#output", "output")
264264

265-
266265
def test_clsd006_PreventUpdate(dash_duo):
267266
app = Dash(__name__, assets_folder="assets")
268267

@@ -307,7 +306,7 @@ def test_clsd006_PreventUpdate(dash_duo):
307306
dash_duo.wait_for_text_to_equal("#third", '3')
308307

309308

310-
def test_clsd006_no_update(dash_duo):
309+
def test_clsd007_no_update(dash_duo):
311310
app = Dash(__name__, assets_folder="assets")
312311

313312
app.layout = html.Div(
@@ -344,3 +343,45 @@ def test_clsd006_no_update(dash_duo):
344343
dash_duo.wait_for_text_to_equal("#first", '111')
345344
dash_duo.wait_for_text_to_equal("#second", '3')
346345
dash_duo.wait_for_text_to_equal("#third", '4')
346+
347+
def test_clsd008_clientside_inline_source(dash_duo):
348+
app = Dash(__name__, assets_folder="assets")
349+
350+
app.layout = html.Div(
351+
[
352+
dcc.Input(id="input"),
353+
html.Div(id="output-clientside"),
354+
html.Div(id="output-serverside"),
355+
]
356+
)
357+
358+
@app.callback(
359+
Output("output-serverside", "children"), [Input("input", "value")]
360+
)
361+
def update_output(value):
362+
return 'Server says "{}"'.format(value)
363+
364+
app.clientside_callback(
365+
"""
366+
function (value) {
367+
return 'Client says "' + value + '"';
368+
}
369+
""",
370+
Output("output-clientside", "children"),
371+
[Input("input", "value")],
372+
)
373+
374+
dash_duo.start_server(app)
375+
376+
dash_duo.wait_for_text_to_equal("#output-serverside", 'Server says "None"')
377+
dash_duo.wait_for_text_to_equal(
378+
"#output-clientside", 'Client says "undefined"'
379+
)
380+
381+
dash_duo.find_element("#input").send_keys("hello world")
382+
dash_duo.wait_for_text_to_equal(
383+
"#output-serverside", 'Server says "hello world"'
384+
)
385+
dash_duo.wait_for_text_to_equal(
386+
"#output-clientside", 'Client says "hello world"'
387+
)

0 commit comments

Comments
 (0)