diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2ad26060..a5c159c887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Fixed - [#2756](https://github.com/plotly/dash/pull/2756) Prevent false dangerous link warning. Fixes [#2743](https://github.com/plotly/dash/issues/2743) +- [#2752](https://github.com/plotly/dash/pull/2752) Fixed issue with Windows build, for first time build on Windows, the dev needs to use `npm run first-build` ## Changed @@ -14,6 +15,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#2735](https://github.com/plotly/dash/pull/2735) Configure CI for Python 3.8 and 3.12, drop support for Python 3.6 and Python 3.7 [#2736](https://github.com/plotly/dash/issues/2736) ## Added +- [#2758](https://github.com/plotly/dash/pull/2758) + - exposing `setProps` to `dash_clientside.clientSide_setProps` to allow for JS code to interact directly with the dash eco-system - [#2730](https://github.com/plotly/dash/pull/2721) Load script files with `.mjs` ending as js modules ## [2.15.0] - 2024-01-31 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74bd379a9a..067c3cdaa4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,7 @@ $ npm ci # $ npm run build # runs `renderer build` and `npm build` in dcc, html, table # build and install components used in tests +# on windows, the developer will need to use `npm run first-build` this performs additional first steps # # Alternatively one could run part of the build process e.g. $ dash-update-components "dash-core-components" diff --git a/dash/dash-renderer/renderer-test.sh b/dash/dash-renderer/renderer-test.sh new file mode 100644 index 0000000000..a0ffb37bf4 --- /dev/null +++ b/dash/dash-renderer/renderer-test.sh @@ -0,0 +1,6 @@ +#!/bin/bash +file=./build/dash_renderer.min.js +if [ ! -f "$file" ]; then + echo "dash-renderer did not build correctly" + exit 1 +fi diff --git a/dash/dash-renderer/src/index.js b/dash/dash-renderer/src/index.js index 7eccf61f7b..3a2bbc7add 100644 --- a/dash/dash-renderer/src/index.js +++ b/dash/dash-renderer/src/index.js @@ -1,4 +1,5 @@ import {DashRenderer} from './DashRenderer'; +import './utils/clientsideFunctions'; // make DashRenderer globally available window.DashRenderer = DashRenderer; diff --git a/dash/dash-renderer/src/store.ts b/dash/dash-renderer/src/store.ts index 09bf21be8b..261bdbe891 100644 --- a/dash/dash-renderer/src/store.ts +++ b/dash/dash-renderer/src/store.ts @@ -56,6 +56,11 @@ export default class RendererStore { private createAppStore = (reducer: any, middleware: any) => { this.__store = createStore(reducer, middleware); this.storeObserver.setStore(this.__store); + const ds = ((window as any).dash_stores = + (window as any).dash_stores || []); + if (!ds.includes(this.__store)) { + ds.push(this.__store); + } this.setObservers(); }; diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts new file mode 100644 index 0000000000..1f33383205 --- /dev/null +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -0,0 +1,23 @@ +import {updateProps, notifyObservers} from '../actions/index'; +import {getPath} from '../actions/paths'; + +const set_props = (id: string | object, props: {[k: string]: any}) => { + const ds = ((window as any).dash_stores = + (window as any).dash_stores || []); + for (let y = 0; y < ds.length; y++) { + const {dispatch, getState} = ds[y]; + const {paths} = getState(); + const componentPath = getPath(paths, id); + dispatch( + updateProps({ + props, + itempath: componentPath + }) + ); + dispatch(notifyObservers({id, props})); + } +}; + +const dc = ((window as any).dash_clientside = + (window as any).dash_clientside || {}); +dc['set_props'] = set_props; diff --git a/package.json b/package.json index a6caee53a3..bead30129d 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "private::initialize.renderer": "cd dash/dash-renderer && npm ci", "private::cibuild.components": "python dash/development/update_components.py 'all' --ci True", "private::build.components": "python dash/development/update_components.py 'all'", - "private::cibuild.renderer": "cd dash/dash-renderer && renderer build", - "private::build.renderer": "cd dash/dash-renderer && renderer build", + "private::cibuild.renderer": "cd dash/dash-renderer && rimraf build/dash_renderer.min.js && renderer build && sh renderer-test.sh", + "private::build.renderer": "cd dash/dash-renderer && rimraf build/dash_renderer.min.js && renderer build && sh renderer-test.sh", "private::build.jupyterlab": "cd @plotly/dash-jupyterlab && jlpm install && jlpm build:pack", "private::lint.black": "black dash tests --exclude metadata_test.py --check", "private::lint.flake8": "flake8 --exclude=metadata_test.py dash tests", diff --git a/tests/integration/clientside/test_clientside_functions.py b/tests/integration/clientside/test_clientside_functions.py new file mode 100644 index 0000000000..c00062835e --- /dev/null +++ b/tests/integration/clientside/test_clientside_functions.py @@ -0,0 +1,62 @@ +from dash import Dash, html, Input, Output, no_update, State +import json +from multiprocessing import Value + + +def test_sp001_clientside_setprops(dash_duo): + + call_count = Value("i", 0) + + app = Dash(__name__) + + ids = [ + {"id": {"index": "1", "type": "test"}, "children": ["rawr"]}, + {"id": "two", "children": "this is a test"}, + {"id": "three", "children": "i see trees of green"}, + ] + + app.layout = html.Div( + [ + *[html.Div(id=x["id"]) for x in ids], + html.Div(id="four"), + html.Button(id="setup", children="test setprops"), + ] + ) + + app.clientside_callback( + """ + () => { + """ + + json.dumps(ids) + + """.forEach(({id, ...props}) => window.dash_clientside.set_props(id, props)) + return window.dash_clientside.no_update + } + """, + Output("setup", "id"), + Input("setup", "n_clicks"), + prevent_initial_call=True, + ) + + for x in ids: + + @app.callback( + Output(x["id"], "id", allow_duplicate=True), + Output("four", "children", allow_duplicate=True), + Input(x["id"], "children"), + State(x["id"], "id"), + prevent_initial_call=True, + ) + def prinout(c, id): + call_count.value += 1 + for y in ids: + if y["id"] == id: + assert y["children"] == c + return no_update, call_count.value + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#setup", "test setprops") + dash_duo.find_element("#setup").click() + dash_duo.wait_for_text_to_equal("#two", "this is a test") + dash_duo.wait_for_text_to_equal("#three", "i see trees of green") + dash_duo.wait_for_text_to_equal("#four", "3")