Skip to content

Commit 1bb7a74

Browse files
authored
Merge pull request #2471 from plotly/fix-2467
Fix allow_duplicate output with clientside callback.
2 parents fde5033 + 68af684 commit 1bb7a74

File tree

4 files changed

+46
-12
lines changed

4 files changed

+46
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5-
## [UNRELEASED
5+
## [UNRELEASED]
66

77
## Fixed
88

99
- [#2479](https://github.com/plotly/dash/pull/2479) Fix `KeyError` "Callback function not found for output [...], , perhaps you forgot to prepend the '@'?" issue when using duplicate callbacks targeting the same output. This issue would occur when the app is restarted or when running with multiple `gunicorn` workers.
10+
- [#2471](https://github.com/plotly/dash/pull/2471) Fix `allow_duplicate` output with clientside callback, fix [#2467](https://github.com/plotly/dash/issues/2467)
1011

1112
## [2.9.1] - 2023-03-17
1213

dash/_callback.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import collections
2+
import uuid
23
from functools import wraps
34

45
import flask
@@ -530,18 +531,14 @@ def register_clientside_callback(
530531
# If JS source is explicitly given, create a namespace and function
531532
# name, then inject the code.
532533
if isinstance(clientside_function, str):
533-
534-
out0 = output
535-
if isinstance(output, (list, tuple)):
536-
out0 = output[0]
537-
538-
namespace = f"_dashprivate_{out0.component_id}"
539-
function_name = out0.component_property
534+
namespace = "_dashprivate_clientside_funcs"
535+
# Just make sure every function has a different name if not provided.
536+
function_name = uuid.uuid4().hex
540537

541538
inline_scripts.append(
542539
_inline_clientside_template.format(
543-
namespace=namespace.replace('"', '\\"'),
544-
function_name=function_name.replace('"', '\\"'),
540+
namespace=namespace,
541+
function_name=function_name,
545542
clientside_function=clientside_function,
546543
)
547544
)

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ const getVals = (input: any) =>
215215
const zipIfArray = (a: any, b: any) =>
216216
Array.isArray(a) ? zip(a, b) : [[a, b]];
217217

218+
function cleanOutputProp(property: string) {
219+
return property.split('@')[0];
220+
}
221+
218222
async function handleClientside(
219223
dispatch: any,
220224
clientside_function: any,
@@ -275,7 +279,7 @@ async function handleClientside(
275279
const idStr = stringifyId(id);
276280
const dataForId = (result[idStr] = result[idStr] || {});
277281
if (retij !== dc.no_update) {
278-
dataForId[property] = retij;
282+
dataForId[cleanOutputProp(property)] = retij;
279283
}
280284
});
281285
});
@@ -704,7 +708,7 @@ export function executeCallback(
704708
// Layout may have changed.
705709
const currentLayout = getState().layout;
706710
flatten(outputs).forEach((out: any) => {
707-
const propName = out.property.split('@')[0];
711+
const propName = cleanOutputProp(out.property);
708712
const outputPath = getPath(paths, out.id);
709713
const previousValue = path(
710714
outputPath.concat(['props', propName]),

tests/integration/test_patch.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,3 +420,35 @@ def on_click(_):
420420

421421
dash_duo_mp.wait_for_element("#click2").click()
422422
dash_duo_mp.wait_for_text_to_equal("#output", "click 2")
423+
424+
425+
def test_pch005_clientside_duplicate(dash_duo):
426+
app = Dash(__name__)
427+
428+
app.layout = html.Div(
429+
[
430+
html.Button("Click 1", id="click1"),
431+
html.Button("Click 2", id="click2"),
432+
html.Div("initial", id="output"),
433+
]
434+
)
435+
436+
app.clientside_callback(
437+
"function() { return 'click1';}",
438+
Output("output", "children", allow_duplicate=True),
439+
Input("click1", "n_clicks"),
440+
prevent_initial_call=True,
441+
)
442+
app.clientside_callback(
443+
"function() { return 'click2';}",
444+
Output("output", "children", allow_duplicate=True),
445+
Input("click2", "n_clicks"),
446+
prevent_initial_call=True,
447+
)
448+
dash_duo.start_server(app)
449+
450+
dash_duo.find_element("#click1").click()
451+
dash_duo.wait_for_text_to_equal("#output", "click1")
452+
453+
dash_duo.find_element("#click2").click()
454+
dash_duo.wait_for_text_to_equal("#output", "click2")

0 commit comments

Comments
 (0)