diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7109f4c26..29d739b323 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
+## Unreleased
+### Added
+- [#967](https://github.com/plotly/dash/pull/967) Adds support for defining
+clientside JavaScript callbacks via inline strings.
+
## [1.6.1] - 2019-11-14
### Fixed
- [#1006](https://github.com/plotly/dash/pull/1006) Fix IE11 / ES5 compatibility and validation issues
diff --git a/dash/dash.py b/dash/dash.py
index 2f2c98e4d4..d509ea987c 100644
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -307,6 +307,9 @@ def __init__(
# list of dependencies
self.callback_map = {}
+ # list of inline scripts
+ self._inline_scripts = []
+
# index_string has special setter so can't go in config
self._index_string = ""
self.index_string = index_string
@@ -636,6 +639,10 @@ def _generate_scripts_html(self):
if isinstance(src, dict)
else ''.format(src)
for src in srcs
+ ] +
+ [
+ ''.format(src)
+ for src in self._inline_scripts
]
)
@@ -1194,13 +1201,14 @@ def clientside_callback(
(JavaScript) function instead of a Python function.
Unlike `@app.calllback`, `clientside_callback` is not a decorator:
- it takes a
+ it takes either a
`dash.dependencies.ClientsideFunction(namespace, function_name)`
argument that describes which JavaScript function to call
(Dash will look for the JavaScript function at
- `window[namespace][function_name]`).
+ `window.dash_clientside[namespace][function_name]`), or it may take
+ a string argument that contains the clientside function source.
- For example:
+ For example, when using a `dash.dependencies.ClientsideFunction`:
```
app.clientside_callback(
ClientsideFunction('my_clientside_library', 'my_function'),
@@ -1211,16 +1219,17 @@ def clientside_callback(
```
With this signature, Dash's front-end will call
- `window.my_clientside_library.my_function` with the current
- values of the `value` properties of the components
- `my-input` and `another-input` whenever those values change.
-
- Include a JavaScript file by including it your `assets/` folder.
- The file can be named anything but you'll need to assign the
- function's namespace to the `window`. For example, this file might
- look like:
+ `window.dash_clientside.my_clientside_library.my_function` with the
+ current values of the `value` properties of the components `my-input`
+ and `another-input` whenever those values change.
+
+ Include a JavaScript file by including it your `assets/` folder. The
+ file can be named anything but you'll need to assign the function's
+ namespace to the `window.dash_clientside` namespace. For example,
+ this file might look:
```
- window.my_clientside_library = {
+ window.dash_clientside = window.dash_clientside || {};
+ window.dash_clientside.my_clientside_library = {
my_function: function(input_value_1, input_value_2) {
return (
parseFloat(input_value_1, 10) +
@@ -1229,10 +1238,54 @@ def clientside_callback(
}
}
```
+
+ Alternatively, you can pass the JavaScript source directly to
+ `clientside_callback`. In this case, the same example would look like:
+ ```
+ app.clientside_callback(
+ '''
+ function(input_value_1, input_value_2) {
+ return (
+ parseFloat(input_value_1, 10) +
+ parseFloat(input_value_2, 10)
+ );
+ }
+ ''',
+ Output('my-div' 'children'),
+ [Input('my-input', 'value'),
+ Input('another-input', 'value')]
+ )
+ ```
"""
self._validate_callback(output, inputs, state)
callback_id = _create_callback_id(output)
+ # If JS source is explicitly given, create a namespace and function
+ # name, then inject the code.
+ if isinstance(clientside_function, str):
+
+ out0 = output
+ if isinstance(output, (list, tuple)):
+ out0 = output[0]
+
+ namespace = '_dashprivate_{}'.format(out0.component_id)
+ function_name = '{}'.format(out0.component_property)
+
+ self._inline_scripts.append(
+ """
+ var clientside = window.dash_clientside = window.dash_clientside || {{}};
+ var ns = clientside["{0}"] = clientside["{0}"] || {{}};
+ ns["{1}"] = {2};
+ """.format(namespace.replace('"', '\\"'),
+ function_name.replace('"', '\\"'),
+ clientside_function)
+ )
+
+ # Callback is stored in an external asset.
+ else:
+ namespace = clientside_function.namespace
+ function_name = clientside_function.function_name
+
self.callback_map[callback_id] = {
"inputs": [
{"id": c.component_id, "property": c.component_property}
@@ -1243,8 +1296,8 @@ def clientside_callback(
for c in state
],
"clientside_function": {
- "namespace": clientside_function.namespace,
- "function_name": clientside_function.function_name,
+ "namespace": namespace,
+ "function_name": function_name,
},
}
diff --git a/dash/dependencies.py b/dash/dependencies.py
index 314cf7db75..3d9b583c1a 100644
--- a/dash/dependencies.py
+++ b/dash/dependencies.py
@@ -36,6 +36,9 @@ class ClientsideFunction:
# pylint: disable=too-few-public-methods
def __init__(self, namespace=None, function_name=None):
+ if namespace.startswith('_dashprivate_'):
+ raise ValueError("Namespaces cannot start with '_dashprivate_'.")
+
if namespace in ['PreventUpdate', 'no_update']:
raise ValueError('"{}" is a forbidden namespace in'
' dash_clientside.'.format(namespace))
diff --git a/tests/integration/clientside/test_clientside.py b/tests/integration/clientside/test_clientside.py
index 83d8057ecf..d610d18be9 100644
--- a/tests/integration/clientside/test_clientside.py
+++ b/tests/integration/clientside/test_clientside.py
@@ -262,7 +262,6 @@ def test_clsd005_clientside_fails_when_returning_a_promise(dash_duo):
dash_duo.wait_for_text_to_equal("#side-effect", "side effect")
dash_duo.wait_for_text_to_equal("#output", "output")
-
def test_clsd006_PreventUpdate(dash_duo):
app = Dash(__name__, assets_folder="assets")
@@ -307,7 +306,7 @@ def test_clsd006_PreventUpdate(dash_duo):
dash_duo.wait_for_text_to_equal("#third", '3')
-def test_clsd006_no_update(dash_duo):
+def test_clsd007_no_update(dash_duo):
app = Dash(__name__, assets_folder="assets")
app.layout = html.Div(
@@ -344,3 +343,45 @@ def test_clsd006_no_update(dash_duo):
dash_duo.wait_for_text_to_equal("#first", '111')
dash_duo.wait_for_text_to_equal("#second", '3')
dash_duo.wait_for_text_to_equal("#third", '4')
+
+def test_clsd008_clientside_inline_source(dash_duo):
+ app = Dash(__name__, assets_folder="assets")
+
+ app.layout = html.Div(
+ [
+ dcc.Input(id="input"),
+ html.Div(id="output-clientside"),
+ html.Div(id="output-serverside"),
+ ]
+ )
+
+ @app.callback(
+ Output("output-serverside", "children"), [Input("input", "value")]
+ )
+ def update_output(value):
+ return 'Server says "{}"'.format(value)
+
+ app.clientside_callback(
+ """
+ function (value) {
+ return 'Client says "' + value + '"';
+ }
+ """,
+ Output("output-clientside", "children"),
+ [Input("input", "value")],
+ )
+
+ dash_duo.start_server(app)
+
+ dash_duo.wait_for_text_to_equal("#output-serverside", 'Server says "None"')
+ dash_duo.wait_for_text_to_equal(
+ "#output-clientside", 'Client says "undefined"'
+ )
+
+ dash_duo.find_element("#input").send_keys("hello world")
+ dash_duo.wait_for_text_to_equal(
+ "#output-serverside", 'Server says "hello world"'
+ )
+ dash_duo.wait_for_text_to_equal(
+ "#output-clientside", 'Client says "hello world"'
+ )