Skip to content

Store props from callback #1900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dash/dash-renderer/src/TreeContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from 'ramda';
import {notifyObservers, updateProps} from './actions';
import isSimpleComponent from './isSimpleComponent';
import {recordUiEdit} from './persistence';
import {recordEdit} from './persistence';
import ComponentErrorBoundary from './components/error/ComponentErrorBoundary.react';
import checkPropTypes from './checkPropTypes';
import {getWatchedKeys, stringifyId} from './actions/dependencies';
Expand Down Expand Up @@ -136,7 +136,7 @@ class BaseTreeContainer extends Component {

// setProps here is triggered by the UI - record these changes
// for persistence
recordUiEdit(_dashprivate_layout, newProps, _dashprivate_dispatch);
recordEdit(_dashprivate_layout, newProps, _dashprivate_dispatch);

// Only dispatch changes to Dash if a watched prop changed
if (watchedKeys.length) {
Expand Down
5 changes: 4 additions & 1 deletion dash/dash-renderer/src/observers/executedCallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {ICallback, IStoredCallback} from '../types/callbacks';
import {updateProps, setPaths, handleAsyncError} from '../actions';
import {getPath, computePaths} from '../actions/paths';

import {applyPersistence, prunePersistence} from '../persistence';
import {applyPersistence, prunePersistence, recordEdit} from '../persistence';
import {IStoreObserverDefinition} from '../StoreObserver';

const observer: IStoreObserverDefinition<IStoreState> = {
Expand Down Expand Up @@ -65,6 +65,9 @@ const observer: IStoreObserverDefinition<IStoreState> = {
// those components have props to update to persist user edits.
const {props} = applyPersistence({props: updatedProps}, dispatch);

// Save props to storage if persistance is active.
recordEdit(path(itempath, layout), updatedProps, dispatch);

dispatch(
updateProps({
itempath,
Expand Down
41 changes: 17 additions & 24 deletions dash/dash-renderer/src/persistence.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ const getProps = layout => {
};
};

export function recordUiEdit(layout, newProps, dispatch) {
export function recordEdit(layout, newProps, dispatch) {
const {
canPersist,
id,
Expand All @@ -316,7 +316,18 @@ export function recordUiEdit(layout, newProps, dispatch) {
persisted_props,
persistence_type
} = getProps(layout);
if (!canPersist || !persistence) {

const getFinal = (propName, prevVal) =>
propName in newProps ? newProps[propName] : prevVal;
const finalPersistence = getFinal('persistence', persistence);
const finalPersistenceType = getFinal('persistence_type', persistence_type);

if (
!canPersist ||
!finalPersistence ||
finalPersistence !== persistence ||
finalPersistenceType !== persistence_type
) {
return;
}

Expand All @@ -336,6 +347,10 @@ export function recordUiEdit(layout, newProps, dispatch) {
if (originalVal !== newVal) {
if (storage.hasItem(valsKey)) {
originalVal = storage.getItem(valsKey)[1];
if (newVal === originalVal) {
storage.removeItem(valsKey);
return;
}
}
const vals =
originalVal === undefined
Expand Down Expand Up @@ -517,28 +532,6 @@ export function prunePersistence(layout, newProps, dispatch) {
filter(notInNewProps, finalPersistedProps)
);
}

// now the main point - clear any edit of a prop that changed
// note that this is independent of the new prop value.
const transforms = element.persistenceTransforms || {};
for (const propName in newProps) {
const propTransforms = transforms[propName];
if (propTransforms) {
for (const propPart in propTransforms) {
finalStorage.removeItem(
getValsKey(
id,
`${propName}.${propPart}`,
finalPersistence
)
);
}
} else {
finalStorage.removeItem(
getValsKey(id, propName, finalPersistence)
);
}
}
}
return persistenceChanged ? mergeRight(newProps, update) : newProps;
}
16 changes: 8 additions & 8 deletions dash/dash-renderer/tests/persistence.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {expect} from 'chai';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {recordUiEdit, stores, storePrefix} from '../src/persistence';
import {recordEdit, stores, storePrefix} from '../src/persistence';
const longString = pow => {
let s = 's';
for (let i = 0; i < pow; i++) {
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('storage fallbacks and equivalence', () => {
const layout = layoutA(storeType);

it(`empty ${storeName} works`, () => {
recordUiEdit(layout, {p1: propVal}, _dispatch);
recordEdit(layout, {p1: propVal}, _dispatch);
expect(dispatchCalls).to.deep.equal([]);
expect(store.getItem(`${storePrefix}a.p1.true`)).to.equal(
`[${propStr}]`
Expand All @@ -109,7 +109,7 @@ describe('storage fallbacks and equivalence', () => {
it(`${storeName} full from persistence works with warnings`, () => {
fillStorage(store, `${storePrefix}x.x`);

recordUiEdit(layout, {p1: propVal}, _dispatch);
recordEdit(layout, {p1: propVal}, _dispatch);
expect(dispatchCalls).to.deep.equal([
`${storeName} init first try failed; clearing and retrying`,
`${storeName} init set/get succeeded after clearing!`
Expand All @@ -125,7 +125,7 @@ describe('storage fallbacks and equivalence', () => {
it(`${storeName} full from other stuff falls back on memory`, () => {
fillStorage(store, 'not_ours');

recordUiEdit(layout, {p1: propVal}, _dispatch);
recordEdit(layout, {p1: propVal}, _dispatch);
expect(dispatchCalls).to.deep.deep.equal([
`${storeName} init first try failed; clearing and retrying`,
`${storeName} init still failed, falling back to memory`
Expand All @@ -139,11 +139,11 @@ describe('storage fallbacks and equivalence', () => {
// Maybe not ideal long-term behavior, but this is what happens

// initialize and ensure the store is happy
recordUiEdit(layout, {p1: propVal}, _dispatch);
recordEdit(layout, {p1: propVal}, _dispatch);
expect(dispatchCalls).to.deep.deep.equal([]);

// now flood it.
recordUiEdit(layout, {p1: longString(26)}, _dispatch);
recordEdit(layout, {p1: longString(26)}, _dispatch);
expect(dispatchCalls).to.deep.equal([
`a.p1.true failed to save in ${storeName}. Persisted props may be lost.`
]);
Expand All @@ -156,7 +156,7 @@ describe('storage fallbacks and equivalence', () => {

it(`${storeType} primitives in/out match`, () => {
// ensure storage is instantiated
recordUiEdit(layout, {p1: propVal}, _dispatch);
recordEdit(layout, {p1: propVal}, _dispatch);
const store = stores[storeType];
[
0,
Expand All @@ -177,7 +177,7 @@ describe('storage fallbacks and equivalence', () => {
});

it(`${storeType} arrays and objects in/out are clones`, () => {
recordUiEdit(layout, {p1: propVal}, _dispatch);
recordEdit(layout, {p1: propVal}, _dispatch);
const store = stores[storeType];

[[1, 2, 3], {a: 1, b: 2}].forEach(val => {
Expand Down
61 changes: 61 additions & 0 deletions tests/integration/renderer/test_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,3 +558,64 @@ def update_container2(n_clicks):
# 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")


def test_rdps014_save_callback_persistence(dash_duo):
app = dash.Dash(__name__)

def make_input(persistence):
return dcc.Input(id="persisted", value="a", persistence=persistence)

app.layout = html.Div(
[
dcc.Input(id="persistence-val", value=""),
dcc.Input(id="persistence-key", value=""),
html.Div(make_input(""), id="persisted-container"),
html.Div(id="out"),
]
)

# this is not a good way to set persistence, as it doesn't allow you to
# get the right initial value. Much better is to update the whole component
# as we do in the previous test case... but it shouldn't break this way.
@app.callback(
Output("persisted", "persistence"), [Input("persistence-key", "value")]
)
def set_persistence(val):
return val

@app.callback(Output("persisted", "value"), [Input("persistence-val", "value")])
def set_value(val):
return val

@app.callback(Output("out", "children"), [Input("persisted", "value")])
def set_out(val):
return val

dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#out", "")

dash_duo.find_element("#persistence-key").send_keys("a")
time.sleep(0.2)
assert not dash_duo.get_logs()
dash_duo.wait_for_text_to_equal("#out", "")
dash_duo.find_element("#persistence-val").send_keys("alpaca")
dash_duo.wait_for_text_to_equal("#out", "alpaca")

dash_duo.find_element("#persistence-key").send_keys("b")
dash_duo.wait_for_text_to_equal("#out", "")
dash_duo.clear_input("#persistence-val")
dash_duo.find_element("#persistence-val").send_keys("artichoke")
dash_duo.wait_for_text_to_equal("#out", "artichoke")

# no persistence, still goes back to original value
dash_duo.clear_input("#persistence-key")
dash_duo.wait_for_text_to_equal("#out", "")

# apricot and artichoke saved
dash_duo.find_element("#persistence-key").send_keys("a")
dash_duo.wait_for_text_to_equal("#out", "alpaca")
dash_duo.find_element("#persistence-key").send_keys("b")
assert not dash_duo.get_logs()
dash_duo.wait_for_text_to_equal("#out", "artichoke")