diff --git a/@plotly/dash-test-components/src/components/MyPersistedComponent.js b/@plotly/dash-test-components/src/components/MyPersistedComponent.js
new file mode 100644
index 0000000000..d64a0bb33e
--- /dev/null
+++ b/@plotly/dash-test-components/src/components/MyPersistedComponent.js
@@ -0,0 +1,166 @@
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+
+const isEquivalent = (v1, v2) => v1 === v2 || (isNaN(v1) && isNaN(v2));
+
+const omit = (key, obj) => {
+ const { [key]: omitted, ...rest } = obj;
+ return rest;
+ }
+
+/**
+ * Adapted dcc input component for persistence tests.
+ *
+ * Note that unnecessary props have been removed.
+ */
+export default class MyPersistedComponent extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.input = React.createRef();
+ this.onChange = this.onChange.bind(this);
+ this.onEvent = this.onEvent.bind(this);
+ this.onKeyPress = this.onKeyPress.bind(this);
+ this.setInputValue = this.setInputValue.bind(this);
+ this.setPropValue = this.setPropValue.bind(this);
+ }
+
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const {value} = this.input.current;
+ this.setInputValue(
+ value,
+ nextProps.value
+ );
+ this.setState({value: nextProps.value});
+ }
+
+ componentDidMount() {
+ const {value} = this.input.current;
+ this.setInputValue(
+ value,
+ this.props.value
+ );
+ }
+
+ UNSAFE_componentWillMount() {
+ this.setState({value: this.props.value})
+ }
+
+ render() {
+ const valprops = {value: this.state.value}
+ return (
+
+ );
+ }
+
+ setInputValue(base, value) {
+ base = NaN;
+
+ if (!isEquivalent(base, value)) {
+ this.input.current.value = value
+ }
+ }
+
+ setPropValue(base, value) {
+ if (!isEquivalent(base, value)) {
+ this.props.setProps({value});
+ }
+ }
+
+ onEvent() {
+ const {value} = this.input.current;
+ this.props.setProps({value})
+ }
+
+ onKeyPress(e) {
+ return this.onEvent();
+ }
+
+ onChange() {
+ this.onEvent()
+ }
+}
+
+MyPersistedComponent.defaultProps = {
+ persisted_props: ['value'],
+ persistence_type: 'local',
+};
+
+MyPersistedComponent.propTypes = {
+ /**
+ * The ID of this component, used to identify dash components
+ * in callbacks. The ID needs to be unique across all of the
+ * components in an app.
+ */
+ id: PropTypes.string,
+
+ /**
+ * The value of the input
+ */
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+
+ /**
+ * The name of the control, which is submitted with the form data.
+ */
+ name: PropTypes.string,
+
+ /**
+ * Dash-assigned callback that gets fired when the value changes.
+ */
+ setProps: PropTypes.func,
+
+
+ /**
+ * Used to allow user interactions in this component to be persisted when
+ * the component - or the page - is refreshed. If `persisted` is truthy and
+ * hasn't changed from its previous value, a `value` that the user has
+ * changed while using the app will keep that change, as long as
+ * the new `value` also matches what was given originally.
+ * Used in conjunction with `persistence_type`.
+ */
+ persistence: PropTypes.oneOfType([
+ PropTypes.bool,
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+
+ /**
+ * Properties whose user interactions will persist after refreshing the
+ * component or the page. Since only `value` is allowed this prop can
+ * normally be ignored.
+ */
+ persisted_props: PropTypes.arrayOf(PropTypes.oneOf(['value'])),
+
+ /**
+ * Where persisted user changes will be stored:
+ * memory: only kept in memory, reset on page refresh.
+ * local: window.localStorage, data is kept after the browser quit.
+ * session: window.sessionStorage, data is cleared once the browser quit.
+ */
+ persistence_type: PropTypes.oneOf(['local', 'session', 'memory']),
+};
+
+MyPersistedComponent.persistenceTransforms = {
+ value: {
+
+ extract: propValue => {
+ if (!(propValue === null || propValue === undefined)) {
+ return propValue.toUpperCase();
+ }
+ return propValue;
+ },
+ apply: storedValue => storedValue,
+
+ },
+ };
\ No newline at end of file
diff --git a/@plotly/dash-test-components/src/components/MyPersistedComponentNested.js b/@plotly/dash-test-components/src/components/MyPersistedComponentNested.js
new file mode 100644
index 0000000000..c18d5a1bad
--- /dev/null
+++ b/@plotly/dash-test-components/src/components/MyPersistedComponentNested.js
@@ -0,0 +1,169 @@
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+
+const isEquivalent = (v1, v2) => v1 === v2 || (isNaN(v1) && isNaN(v2));
+
+const omit = (key, obj) => {
+ const { [key]: omitted, ...rest } = obj;
+ return rest;
+ }
+
+/**
+ * Adapted dcc input component for persistence tests.
+ *
+ * Note that unnecessary props have been removed.
+ */
+export default class MyPersistedComponentNested extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.input = React.createRef();
+ this.onChange = this.onChange.bind(this);
+ this.onEvent = this.onEvent.bind(this);
+ this.onKeyPress = this.onKeyPress.bind(this);
+ this.setInputValue = this.setInputValue.bind(this);
+ this.setPropValue = this.setPropValue.bind(this);
+ }
+
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const {value} = this.input.current;
+ this.setInputValue(
+ value,
+ nextProps.value
+ );
+ this.setState({value: nextProps.value});
+ }
+
+ componentDidMount() {
+ const {value} = this.input.current;
+ this.setInputValue(
+ value,
+ this.props.value
+ );
+ }
+
+ UNSAFE_componentWillMount() {
+ this.setState({value: this.props.value})
+ }
+
+ render() {
+ const valprops = {value: this.state.value}
+ return (
+
+ );
+ }
+
+ setInputValue(base, value) {
+ base = NaN;
+
+ if (!isEquivalent(base, value)) {
+ this.input.current.value = value
+ }
+ }
+
+ setPropValue(base, value) {
+ if (!isEquivalent(base, value)) {
+ this.props.setProps({value});
+ }
+ }
+
+ onEvent() {
+ const {value} = this.input.current;
+ this.props.setProps({value})
+ }
+
+ onKeyPress(e) {
+ return this.onEvent();
+ }
+
+ onChange() {
+ this.onEvent()
+ }
+}
+
+MyPersistedComponentNested.defaultProps = {
+ persisted_props: ['value.nested_value'],
+ persistence_type: 'local',
+};
+
+MyPersistedComponentNested.propTypes = {
+ /**
+ * The ID of this component, used to identify dash components
+ * in callbacks. The ID needs to be unique across all of the
+ * components in an app.
+ */
+ id: PropTypes.string,
+
+ /**
+ * The value of the input
+ */
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+
+ /**
+ * The name of the control, which is submitted with the form data.
+ */
+ name: PropTypes.string,
+
+ /**
+ * Dash-assigned callback that gets fired when the value changes.
+ */
+ setProps: PropTypes.func,
+
+
+ /**
+ * Used to allow user interactions in this component to be persisted when
+ * the component - or the page - is refreshed. If `persisted` is truthy and
+ * hasn't changed from its previous value, a `value` that the user has
+ * changed while using the app will keep that change, as long as
+ * the new `value` also matches what was given originally.
+ * Used in conjunction with `persistence_type`.
+ */
+ persistence: PropTypes.oneOfType([
+ PropTypes.bool,
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+
+ /**
+ * Properties whose user interactions will persist after refreshing the
+ * component or the page. Since only `value` is allowed this prop can
+ * normally be ignored.
+ */
+ persisted_props: PropTypes.arrayOf(PropTypes.oneOf(['value.nested_value'])),
+
+ /**
+ * Where persisted user changes will be stored:
+ * memory: only kept in memory, reset on page refresh.
+ * local: window.localStorage, data is kept after the browser quit.
+ * session: window.sessionStorage, data is cleared once the browser quit.
+ */
+ persistence_type: PropTypes.oneOf(['local', 'session', 'memory']),
+};
+
+MyPersistedComponentNested.persistenceTransforms = {
+ value: {
+
+ nested_value: {
+
+ extract: propValue => {
+ if (!(propValue === null || propValue === undefined)) {
+ return propValue.toUpperCase();
+ }
+ return propValue;
+ },
+ apply: storedValue => storedValue,
+
+ }
+ },
+};
diff --git a/@plotly/dash-test-components/src/index.js b/@plotly/dash-test-components/src/index.js
index 24f7254ffe..f9752fc6a2 100644
--- a/@plotly/dash-test-components/src/index.js
+++ b/@plotly/dash-test-components/src/index.js
@@ -1,5 +1,8 @@
import StyledComponent from './components/StyledComponent';
+import MyPersistedComponent from './components/MyPersistedComponent';
+import MyPersistedComponentNested from './components/MyPersistedComponentNested';
+
export {
- StyledComponent,
+ StyledComponent, MyPersistedComponent, MyPersistedComponentNested
};
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5a41709c6..3be5312c0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
+## [UNRELEASED]
+### Changed
+- [#1376](https://github.com/plotly/dash/pull/1376) Extends the `getTransform` logic in the renderer to handle `persistenceTransforms` for both nested and non-nested persisted props. This was used to to fix [dcc#700](https://github.com/plotly/dash-core-components/issues/700) in conjunction with [dcc#854](https://github.com/plotly/dash-core-components/pull/854) by using persistenceTransforms to strip the time part of the datetime so that datepickers can persist when defined in callbacks.
+
## [1.16.0] - 2020-09-03
### Added
- [#1371](https://github.com/plotly/dash/pull/1371) You can now get [CSP `script-src` hashes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) of all added inline scripts by calling `app.csp_hashes()` (both Dash internal inline scripts, and those added with `app.clientside_callback`) .
diff --git a/dash-renderer/src/persistence.js b/dash-renderer/src/persistence.js
index 41222a6589..b7bc2719ba 100644
--- a/dash-renderer/src/persistence.js
+++ b/dash-renderer/src/persistence.js
@@ -265,10 +265,18 @@ const noopTransform = {
apply: (storedValue, _propValue) => storedValue
};
-const getTransform = (element, propName, propPart) =>
- propPart
- ? element.persistenceTransforms[propName][propPart]
- : noopTransform;
+const getTransform = (element, propName, propPart) => {
+ if (
+ element.persistenceTransforms &&
+ element.persistenceTransforms[propName]
+ ) {
+ if (propPart) {
+ return element.persistenceTransforms[propName][propPart];
+ }
+ return element.persistenceTransforms[propName];
+ }
+ return noopTransform;
+};
const getValsKey = (id, persistedProp, persistence) =>
`${stringifyId(id)}.${persistedProp}.${JSON.stringify(persistence)}`;
diff --git a/tests/integration/renderer/test_persistence.py b/tests/integration/renderer/test_persistence.py
index 49d91d6f24..e0879a1e46 100644
--- a/tests/integration/renderer/test_persistence.py
+++ b/tests/integration/renderer/test_persistence.py
@@ -11,6 +11,9 @@
import dash_html_components as html
import dash_table as dt
+from dash_test_components import MyPersistedComponent
+from dash_test_components import MyPersistedComponentNested
+
@pytest.fixture(autouse=True)
def clear_storage(dash_duo):
@@ -526,3 +529,36 @@ def set_out(val):
dash_duo.wait_for_text_to_equal(".out", "")
dash_duo.find_element("#btn").click()
+
+
+def test_rdps013_persisted_props_nested(dash_duo):
+ # testing persistenceTransforms with generated test components
+ # with persisted prop and persisted nested prop
+ app = dash.Dash(__name__)
+
+ app.layout = html.Div(
+ [
+ html.Button("click me", id="btn"),
+ html.Div(id="container1"),
+ html.Div(id="container2"),
+ ]
+ )
+
+ @app.callback(Output("container1", "children"), [Input("btn", "n_clicks")])
+ def update_container(n_clicks):
+ return MyPersistedComponent(id="component-propName", persistence=True)
+
+ @app.callback(Output("container2", "children"), [Input("btn", "n_clicks")])
+ def update_container(n_clicks):
+ return MyPersistedComponentNested(id="component-propPart", persistence=True)
+
+ dash_duo.start_server(app)
+
+ # send lower case strings to test components
+ dash_duo.find_element("#component-propName").send_keys("alpaca")
+ dash_duo.find_element("#component-propPart").send_keys("artichoke")
+ dash_duo.find_element("#btn").click()
+
+ # persistenceTransforms should return upper case strings
+ dash_duo.wait_for_text_to_equal("#component-propName", "ALPACA")
+ dash_duo.wait_for_text_to_equal("#component-propPart", "ARTICHOKE")