diff --git a/CHANGELOG.md b/CHANGELOG.md index 79fa8a2150..405158ab14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - [#1237](https://github.com/plotly/dash/pull/1237) Closes [#920](https://github.com/plotly/dash/issues/920): Converts hot reload fetch failures into a server status indicator showing whether the latest fetch succeeded or failed. Callback fetch failures still appear as errors but have a clearer message. +### Fixed +- [#1248](https://github.com/plotly/dash/pull/1248) Fixes [#1245](https://github.com/plotly/dash/issues/1245), so you can use prop persistence with components that have dict IDs, ie for pattern-matching callbacks. + ## [1.12.0] - 2020-05-05 ### Added - [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior. diff --git a/dash-renderer/src/persistence.js b/dash-renderer/src/persistence.js index 36b8688a3f..4940d0ff89 100644 --- a/dash-renderer/src/persistence.js +++ b/dash-renderer/src/persistence.js @@ -69,6 +69,7 @@ import { import {createAction} from 'redux-actions'; import Registry from './registry'; +import {stringifyId} from './actions/dependencies'; export const storePrefix = '_dash_persistence.'; @@ -270,7 +271,7 @@ const getTransform = (element, propName, propPart) => : noopTransform; const getValsKey = (id, persistedProp, persistence) => - `${id}.${persistedProp}.${JSON.stringify(persistence)}`; + `${stringifyId(id)}.${persistedProp}.${JSON.stringify(persistence)}`; const getProps = layout => { const {props, type, namespace} = layout; diff --git a/tests/integration/devtools/test_hot_reload.py b/tests/integration/devtools/test_hot_reload.py index 0b0a0fae28..5fdaa50865 100644 --- a/tests/integration/devtools/test_hot_reload.py +++ b/tests/integration/devtools/test_hot_reload.py @@ -16,10 +16,10 @@ def test_dvhr001_hot_reload(dash_duo): app = dash.Dash(__name__, assets_folder="hr_assets") - app.layout = html.Div([ - html.H3("Hot reload", id="text"), - html.Button("Click", id="btn") - ], id="hot-reload-content") + app.layout = html.Div( + [html.H3("Hot reload", id="text"), html.Button("Click", id="btn")], + id="hot-reload-content", + ) @app.callback(Output("text", "children"), [Input("btn", "n_clicks")]) def new_text(n): diff --git a/tests/integration/renderer/test_persistence.py b/tests/integration/renderer/test_persistence.py index 2fc5672be8..49d91d6f24 100644 --- a/tests/integration/renderer/test_persistence.py +++ b/tests/integration/renderer/test_persistence.py @@ -5,7 +5,7 @@ from selenium.webdriver.common.keys import Keys import dash -from dash.dependencies import Input, Output +from dash.dependencies import Input, Output, State, MATCH import dash_core_components as dcc import dash_html_components as html @@ -451,3 +451,78 @@ def set_out(val): dash_duo.find_element("#persistence-val").send_keys("2") assert not dash_duo.get_logs() dash_duo.wait_for_text_to_equal("#out", "artichoke") + + +def test_rdps012_pattern_matching(dash_duo): + # copy of rdps010 but with dict IDs, + # plus a button to change the dict ID so the persistence should reset + def make_input(persistence, n): + return dcc.Input( + id={"i": n, "id": "persisted"}, + className="persisted", + value="a", + persistence=persistence, + ) + + app = dash.Dash(__name__) + app.layout = html.Div( + [html.Button("click", id="btn", n_clicks=0), html.Div(id="content")] + ) + + @app.callback(Output("content", "children"), [Input("btn", "n_clicks")]) + def content(n): + return [ + dcc.Input( + id={"i": n, "id": "persistence-val"}, + value="", + className="persistence-val", + ), + html.Div(make_input("", n), id={"i": n, "id": "persisted-container"}), + html.Div(id={"i": n, "id": "out"}, className="out"), + ] + + @app.callback( + Output({"i": MATCH, "id": "persisted-container"}, "children"), + [Input({"i": MATCH, "id": "persistence-val"}, "value")], + [State("btn", "n_clicks")], + ) + def set_persistence(val, n): + return make_input(val, n) + + @app.callback( + Output({"i": MATCH, "id": "out"}, "children"), + [Input({"i": MATCH, "id": "persisted"}, "value")], + ) + def set_out(val): + return val + + dash_duo.start_server(app) + + for _ in range(3): + dash_duo.wait_for_text_to_equal(".out", "a") + dash_duo.find_element(".persisted").send_keys("lpaca") + dash_duo.wait_for_text_to_equal(".out", "alpaca") + + dash_duo.find_element(".persistence-val").send_keys("s") + dash_duo.wait_for_text_to_equal(".out", "a") + dash_duo.find_element(".persisted").send_keys("nchovies") + dash_duo.wait_for_text_to_equal(".out", "anchovies") + + dash_duo.find_element(".persistence-val").send_keys("2") + dash_duo.wait_for_text_to_equal(".out", "a") + dash_duo.find_element(".persisted").send_keys( + Keys.BACK_SPACE + ) # persist falsy value + dash_duo.wait_for_text_to_equal(".out", "") + + # alpaca not saved with falsy persistence + dash_duo.clear_input(".persistence-val") + dash_duo.wait_for_text_to_equal(".out", "a") + + # anchovies and aardvark saved + dash_duo.find_element(".persistence-val").send_keys("s") + dash_duo.wait_for_text_to_equal(".out", "anchovies") + dash_duo.find_element(".persistence-val").send_keys("2") + dash_duo.wait_for_text_to_equal(".out", "") + + dash_duo.find_element("#btn").click()