diff --git a/CHANGELOG.md b/CHANGELOG.md index e9903cff3c..5e37852bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). -## [UNRELEASED +## [UNRELEASED] ## Fixed - [#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. +- [#2471](https://github.com/plotly/dash/pull/2471) Fix `allow_duplicate` output with clientside callback, fix [#2467](https://github.com/plotly/dash/issues/2467) ## [2.9.1] - 2023-03-17 diff --git a/dash/_callback.py b/dash/_callback.py index 7a21729fc2..666a3dccef 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -1,4 +1,5 @@ import collections +import uuid from functools import wraps import flask @@ -530,18 +531,14 @@ def register_clientside_callback( # 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 = f"_dashprivate_{out0.component_id}" - function_name = out0.component_property + namespace = "_dashprivate_clientside_funcs" + # Just make sure every function has a different name if not provided. + function_name = uuid.uuid4().hex inline_scripts.append( _inline_clientside_template.format( - namespace=namespace.replace('"', '\\"'), - function_name=function_name.replace('"', '\\"'), + namespace=namespace, + function_name=function_name, clientside_function=clientside_function, ) ) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 9064aad175..33426c7238 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -215,6 +215,10 @@ const getVals = (input: any) => const zipIfArray = (a: any, b: any) => Array.isArray(a) ? zip(a, b) : [[a, b]]; +function cleanOutputProp(property: string) { + return property.split('@')[0]; +} + async function handleClientside( dispatch: any, clientside_function: any, @@ -275,7 +279,7 @@ async function handleClientside( const idStr = stringifyId(id); const dataForId = (result[idStr] = result[idStr] || {}); if (retij !== dc.no_update) { - dataForId[property] = retij; + dataForId[cleanOutputProp(property)] = retij; } }); }); @@ -704,7 +708,7 @@ export function executeCallback( // Layout may have changed. const currentLayout = getState().layout; flatten(outputs).forEach((out: any) => { - const propName = out.property.split('@')[0]; + const propName = cleanOutputProp(out.property); const outputPath = getPath(paths, out.id); const previousValue = path( outputPath.concat(['props', propName]), diff --git a/tests/integration/test_patch.py b/tests/integration/test_patch.py index 485aaea5fe..2562464b39 100644 --- a/tests/integration/test_patch.py +++ b/tests/integration/test_patch.py @@ -420,3 +420,35 @@ def on_click(_): dash_duo_mp.wait_for_element("#click2").click() dash_duo_mp.wait_for_text_to_equal("#output", "click 2") + + +def test_pch005_clientside_duplicate(dash_duo): + app = Dash(__name__) + + app.layout = html.Div( + [ + html.Button("Click 1", id="click1"), + html.Button("Click 2", id="click2"), + html.Div("initial", id="output"), + ] + ) + + app.clientside_callback( + "function() { return 'click1';}", + Output("output", "children", allow_duplicate=True), + Input("click1", "n_clicks"), + prevent_initial_call=True, + ) + app.clientside_callback( + "function() { return 'click2';}", + Output("output", "children", allow_duplicate=True), + Input("click2", "n_clicks"), + prevent_initial_call=True, + ) + dash_duo.start_server(app) + + dash_duo.find_element("#click1").click() + dash_duo.wait_for_text_to_equal("#output", "click1") + + dash_duo.find_element("#click2").click() + dash_duo.wait_for_text_to_equal("#output", "click2")