Skip to content

Commit 7202d53

Browse files
authored
Merge pull request #2009 from MrTeale/clientside-callbacks-promise-handling
Clientside Callback Promise Handling
2 parents f6b51a8 + 6cd85bb commit 7202d53

File tree

4 files changed

+176
-41
lines changed

4 files changed

+176
-41
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- [#2009](https://github.com/plotly/dash/pull/2009) Add support for Promises within Client-side callbacks as requested in [#1364](https://github.com/plotly/dash/pull/1364).
10+
711
### Fixed
812

913
- [#2015](https://github.com/plotly/dash/pull/2015) Fix bug [#1854](https://github.com/plotly/dash/issues/1854) in which the combination of row_selectable="single or multi" and filter_action="native" caused the JS error.

dash/dash-renderer/src/actions/callbacks.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ const getVals = (input: any) =>
200200
const zipIfArray = (a: any, b: any) =>
201201
Array.isArray(a) ? zip(a, b) : [[a, b]];
202202

203-
function handleClientside(
203+
async function handleClientside(
204204
dispatch: any,
205205
clientside_function: any,
206206
config: any,
@@ -246,14 +246,12 @@ function handleClientside(
246246
dc.callback_context.states_list = state;
247247
dc.callback_context.states = stateDict;
248248

249-
const returnValue = dc[namespace][function_name](...args);
249+
let returnValue = dc[namespace][function_name](...args);
250+
251+
delete dc.callback_context;
250252

251253
if (typeof returnValue?.then === 'function') {
252-
throw new Error(
253-
'The clientside function returned a Promise. ' +
254-
'Promises are not supported in Dash clientside ' +
255-
'right now, but may be in the future.'
256-
);
254+
returnValue = await returnValue;
257255
}
258256

259257
zipIfArray(outputs, returnValue).forEach(([outi, reti]) => {
@@ -504,15 +502,13 @@ export function executeCallback(
504502

505503
if (clientside_function) {
506504
try {
507-
return {
508-
data: handleClientside(
509-
dispatch,
510-
clientside_function,
511-
config,
512-
payload
513-
),
505+
const data = await handleClientside(
506+
dispatch,
507+
clientside_function,
508+
config,
514509
payload
515-
};
510+
);
511+
return {data, payload};
516512
} catch (error: any) {
517513
return {error, payload};
518514
}

tests/integration/clientside/assets/clientside.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,27 @@ window.dash_clientside.clientside = {
9898
}
9999
window.callCount += 1;
100100
return inputValue.toString();
101-
}
101+
},
102+
103+
chained_promise: function (inputValue) {
104+
return new Promise(function (resolve) {
105+
setTimeout(function () {
106+
resolve(inputValue + "-chained");
107+
}, 100);
108+
});
109+
},
110+
111+
delayed_promise: function (inputValue) {
112+
return new Promise(function (resolve) {
113+
window.callbackDone = function (deferredValue) {
114+
resolve("clientside-" + inputValue + "-" + deferredValue);
115+
};
116+
});
117+
},
118+
119+
non_delayed_promise: function (inputValue) {
120+
return new Promise(function (resolve) {
121+
resolve("clientside-" + inputValue);
122+
});
123+
},
102124
};

tests/integration/clientside/test_clientside.py

Lines changed: 138 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: UTF-8 -*-
2-
from multiprocessing import Value
2+
from multiprocessing import Value, Lock
33

44
from dash import Dash, Input, Output, State, ClientsideFunction, ALL, html, dcc
55
from selenium.webdriver.common.keys import Keys
@@ -223,30 +223,6 @@ def test_clsd004_clientside_multiple_outputs(dash_duo):
223223
dash_duo.wait_for_text_to_equal(selector, expected)
224224

225225

226-
def test_clsd005_clientside_fails_when_returning_a_promise(dash_duo):
227-
app = Dash(__name__, assets_folder="assets")
228-
229-
app.layout = html.Div(
230-
[
231-
html.Div(id="input", children="hello"),
232-
html.Div(id="side-effect"),
233-
html.Div(id="output", children="output"),
234-
]
235-
)
236-
237-
app.clientside_callback(
238-
ClientsideFunction("clientside", "side_effect_and_return_a_promise"),
239-
Output("output", "children"),
240-
[Input("input", "children")],
241-
)
242-
243-
dash_duo.start_server(app)
244-
245-
dash_duo.wait_for_text_to_equal("#input", "hello")
246-
dash_duo.wait_for_text_to_equal("#side-effect", "side effect")
247-
dash_duo.wait_for_text_to_equal("#output", "output")
248-
249-
250226
def test_clsd006_PreventUpdate(dash_duo):
251227
app = Dash(__name__, assets_folder="assets")
252228

@@ -716,3 +692,140 @@ def test_clsd014_input_output_callback(dash_duo):
716692
assert call_count == 2, "initial + changed once"
717693

718694
assert dash_duo.get_logs() == []
695+
696+
697+
def test_clsd015_clientside_chained_callbacks_returning_promise(dash_duo):
698+
app = Dash(__name__, assets_folder="assets")
699+
700+
app.layout = html.Div(
701+
[
702+
html.Div(id="input", children=["initial"]),
703+
html.Div(id="div-1"),
704+
html.Div(id="div-2"),
705+
]
706+
)
707+
708+
app.clientside_callback(
709+
ClientsideFunction(namespace="clientside", function_name="chained_promise"),
710+
Output("div-1", "children"),
711+
Input("input", "children"),
712+
)
713+
714+
@app.callback(Output("div-2", "children"), Input("div-1", "children"))
715+
def callback(value):
716+
return value + "-twice"
717+
718+
dash_duo.start_server(app)
719+
720+
dash_duo.wait_for_text_to_equal("#div-1", "initial-chained")
721+
dash_duo.wait_for_text_to_equal("#div-2", "initial-chained-twice")
722+
723+
724+
def test_clsd016_serverside_clientside_shared_input_with_promise(dash_duo):
725+
app = Dash(__name__, assets_folder="assets")
726+
727+
app.layout = html.Div(
728+
[
729+
html.Div(id="input", children=["initial"]),
730+
html.Div(id="clientside-div"),
731+
html.Div(id="serverside-div"),
732+
]
733+
)
734+
735+
app.clientside_callback(
736+
ClientsideFunction(namespace="clientside", function_name="delayed_promise"),
737+
Output("clientside-div", "children"),
738+
Input("input", "children"),
739+
)
740+
741+
@app.callback(Output("serverside-div", "children"), Input("input", "children"))
742+
def callback(value):
743+
return "serverside-" + value[0]
744+
745+
dash_duo.start_server(app)
746+
747+
dash_duo.wait_for_text_to_equal("#serverside-div", "serverside-initial")
748+
dash_duo.driver.execute_script("window.callbackDone('deferred')")
749+
dash_duo.wait_for_text_to_equal("#clientside-div", "clientside-initial-deferred")
750+
751+
752+
def test_clsd017_clientside_serverside_shared_input_with_promise(dash_duo):
753+
lock = Lock()
754+
lock.acquire()
755+
756+
app = Dash(__name__, assets_folder="assets")
757+
758+
app.layout = html.Div(
759+
[
760+
html.Div(id="input", children=["initial"]),
761+
html.Div(id="clientside-div"),
762+
html.Div(id="serverside-div"),
763+
]
764+
)
765+
766+
app.clientside_callback(
767+
ClientsideFunction(namespace="clientside", function_name="non_delayed_promise"),
768+
Output("clientside-div", "children"),
769+
Input("input", "children"),
770+
)
771+
772+
@app.callback(Output("serverside-div", "children"), Input("input", "children"))
773+
def callback(value):
774+
with lock:
775+
return "serverside-" + value[0] + "-deferred"
776+
777+
dash_duo.start_server(app)
778+
779+
dash_duo.wait_for_text_to_equal("#clientside-div", "clientside-initial")
780+
lock.release()
781+
dash_duo.wait_for_text_to_equal("#serverside-div", "serverside-initial-deferred")
782+
783+
784+
def test_clsd018_clientside_inline_async_function(dash_duo):
785+
app = Dash(__name__)
786+
787+
app.layout = html.Div(
788+
[
789+
html.Div(id="input", children=["initial"]),
790+
html.Div(id="output-div"),
791+
]
792+
)
793+
794+
app.clientside_callback(
795+
"""
796+
async function(input) {
797+
return input + "-inline";
798+
}
799+
""",
800+
Output("output-div", "children"),
801+
Input("input", "children"),
802+
)
803+
804+
dash_duo.start_server(app)
805+
dash_duo.wait_for_text_to_equal("#output-div", "initial-inline")
806+
807+
808+
def test_clsd019_clientside_inline_promise(dash_duo):
809+
app = Dash(__name__)
810+
811+
app.layout = html.Div(
812+
[
813+
html.Div(id="input", children=["initial"]),
814+
html.Div(id="output-div"),
815+
]
816+
)
817+
818+
app.clientside_callback(
819+
"""
820+
function(inputValue) {
821+
return new Promise(function (resolve) {
822+
resolve(inputValue + "-inline");
823+
});
824+
}
825+
""",
826+
Output("output-div", "children"),
827+
Input("input", "children"),
828+
)
829+
830+
dash_duo.start_server(app)
831+
dash_duo.wait_for_text_to_equal("#output-div", "initial-inline")

0 commit comments

Comments
 (0)