Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline clientside callbacks #967

Merged
merged 12 commits into from
Nov 15, 2019
87 changes: 71 additions & 16 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,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
Expand Down Expand Up @@ -629,6 +632,10 @@ def _generate_scripts_html(self):
if isinstance(src, dict)
else '<script src="{}"></script>'.format(src)
for src in srcs
] +
[
'<script>{}</script>'.format(src)
for src in self._inline_scripts
]
)

Expand Down Expand Up @@ -1161,13 +1168,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! 🔬

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'),
Expand All @@ -1178,28 +1186,75 @@ 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 like:
```
window.dash_clientside = Object.assign(window.dash_clientside || {},
{
my_function: function(input_value_1, input_value_2) {
return (
parseFloat(input_value_1, 10) +
parseFloat(input_value_2, 10)
);
}
}
);
```
window.my_clientside_library = {
my_function: function(input_value_1, input_value_2) {

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(
"""
if (!window.dash_clientside) {{
window.dash_clientside = {{}};
}}
window.dash_clientside["{0}"] = Object.assign(
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved
window.dash_clientside["{0}"] || {{}}, {{"{1}": {2}}}
);
""".format(namespace, function_name, 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}
Expand All @@ -1210,8 +1265,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,
},
}

Expand Down
3 changes: 3 additions & 0 deletions dash/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class State(DashDependency): # pylint: disable=too-few-public-methods
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_'.")

self.namespace = namespace
self.function_name = function_name

Expand Down
42 changes: 42 additions & 0 deletions tests/integration/clientside/test_clientside.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,45 @@ def test_clsd005_clientside_fails_when_returning_a_promise(dash_duo):
dash_duo.wait_for_text_to_equal("#input", "hello")
dash_duo.wait_for_text_to_equal("#side-effect", "side effect")
dash_duo.wait_for_text_to_equal("#output", "output")

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"'
)