Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const Dropdown = props => {
search_value,
style,
value,
searchable,
} = props;
const [optionsCheck, setOptionsCheck] = useState(null);
const persistentOptions = useRef(null);
Expand Down Expand Up @@ -157,7 +158,7 @@ const Dropdown = props => {
options={sanitizedOptions}
value={value}
onChange={onChange}
onInputChange={onInputChange}
onInputChange={searchable ? onInputChange : undefined}
backspaceRemoves={clearable}
deleteRemoves={clearable}
inputProps={{autoComplete: 'off'}}
Expand Down
7 changes: 4 additions & 3 deletions dash/dash-renderer/src/reducers/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import layout from './layout';
import paths from './paths';
import callbackJobs from './callbackJobs';
import loading from './loading';
import {stringifyPath} from '../wrapper/wrapping';

export const apiRequests = [
'dependenciesRequest',
Expand All @@ -36,9 +37,9 @@ function layoutHashes(state = {}, action) {
) {
// Let us compare the paths sums to get updates without triggering
// render on the parent containers.
const jsonPath = JSON.stringify(action.payload.itempath);
const prev = pathOr(0, [jsonPath], state);
return assoc(jsonPath, prev + 1, state);
const strPath = stringifyPath(action.payload.itempath);
const prev = pathOr(0, [strPath], state);
return assoc(strPath, prev + 1, state);
}
return state;
}
Expand Down
89 changes: 47 additions & 42 deletions dash/dash-renderer/src/wrapper/DashWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {DashConfig} from '../config';
import {notifyObservers, onError, updateProps} from '../actions';
import {getWatchedKeys, stringifyId} from '../actions/dependencies';
import {recordUiEdit} from '../persistence';
import {createElement, isDryComponent} from './wrapping';
import {createElement, getComponentLayout, isDryComponent} from './wrapping';
import Registry from '../registry';
import isSimpleComponent from '../isSimpleComponent';
import {
Expand Down Expand Up @@ -62,56 +62,61 @@ function DashWrapper({
const setProps = (newProps: UpdatePropsPayload) => {
const {id} = componentProps;
const {_dash_error, ...restProps} = newProps;
const oldProps = componentProps;
const changedProps = pickBy(
(val, key) => !equals(val, oldProps[key]),
restProps
);
if (_dash_error) {
dispatch(
onError({
type: 'frontEnd',
error: _dash_error
})

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
dispatch((dispatch, getState) => {
const currentState = getState();
const {graphs} = currentState;

const {props: oldProps} = getComponentLayout(
componentPath,
currentState
);
}
if (!isEmpty(changedProps)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
dispatch((dispatch, getState) => {
const {graphs} = getState();
// Identify the modified props that are required for callbacks
const watchedKeys = getWatchedKeys(
id,
keys(changedProps),
graphs
const changedProps = pickBy(
(val, key) => !equals(val, oldProps[key]),
restProps
);
if (_dash_error) {
dispatch(
onError({
type: 'frontEnd',
error: _dash_error
})
);
}

batch(() => {
// setProps here is triggered by the UI - record these changes
// for persistence
recordUiEdit(component, newProps, dispatch);
if (isEmpty(changedProps)) {
return;
}

// Only dispatch changes to Dash if a watched prop changed
if (watchedKeys.length) {
dispatch(
notifyObservers({
id,
props: pick(watchedKeys, changedProps)
})
);
}
// Identify the modified props that are required for callbacks
const watchedKeys = getWatchedKeys(id, keys(changedProps), graphs);

batch(() => {
// setProps here is triggered by the UI - record these changes
// for persistence
recordUiEdit(component, newProps, dispatch);

// Always update this component's props
// Only dispatch changes to Dash if a watched prop changed
if (watchedKeys.length) {
dispatch(
updateProps({
props: changedProps,
itempath: componentPath
notifyObservers({
id,
props: pick(watchedKeys, changedProps)
})
);
});
}

// Always update this component's props
dispatch(
updateProps({
props: changedProps,
itempath: componentPath
})
);
});
}
});
};

const createContainer = useCallback(
Expand Down
12 changes: 5 additions & 7 deletions dash/dash-renderer/src/wrapper/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import {path} from 'ramda';

import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component';
import {getComponentLayout, stringifyPath} from './wrapping';

type SelectDashProps = [DashComponent, BaseDashProps, number];

export const selectDashProps =
(componentPath: DashLayoutPath) =>
(state: any): SelectDashProps => {
const c = path(componentPath, state.layout) as DashComponent;
const c = getComponentLayout(componentPath, state);
// Layout hashes records the number of times a path has been updated.
// sum with the parents hash (match without the last ']') to get the real hash
// Then it can be easily compared without having to compare the props.
let jsonPath = JSON.stringify(componentPath);
jsonPath = jsonPath.substring(0, jsonPath.length - 1);
const strPath = stringifyPath(componentPath);

const h = Object.entries(state.layoutHashes).reduce(
(acc, [path, pathHash]) =>
jsonPath.startsWith(path.substring(0, path.length - 1))
(acc, [updatedPath, pathHash]) =>
strPath.startsWith(updatedPath)
? (pathHash as number) + acc
: acc,
0
Expand Down
14 changes: 13 additions & 1 deletion dash/dash-renderer/src/wrapper/wrapping.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import {mergeRight, type, has} from 'ramda';
import {mergeRight, path, type, has, join} from 'ramda';
import {DashComponent, DashLayoutPath} from '../types/component';

export function createElement(
element: any,
Expand Down Expand Up @@ -49,3 +50,14 @@ export function validateComponent(componentDefinition: any) {
);
}
}

export function stringifyPath(layoutPath: DashLayoutPath) {
return join(',', layoutPath);
}

export function getComponentLayout(
componentPath: DashLayoutPath,
state: any
): DashComponent {
return path(componentPath, state.layout) as DashComponent;
}
2 changes: 2 additions & 0 deletions tests/integration/renderer/test_persistence.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from multiprocessing import Value
import flaky
import pytest
import time

Expand Down Expand Up @@ -206,6 +207,7 @@ def toggle_table(n):
check_table_names(dash_duo, ["a", "b"])


@flaky.flaky(max_runs=3)
def test_rdps005_persisted_props(dash_duo):
app = Dash(__name__)
app.layout = html.Div(
Expand Down
34 changes: 34 additions & 0 deletions tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,37 @@ def my_route_f():
response = requests.post(url)
assert response.status_code == 200
assert response.text == "hello"


def test_inin031_initial_value_set_back(dash_duo):
# Test for regression on the initial value to be able to
# set back to initial after changing again.
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Dropdown(
id="dropdown",
options=["Toronto", "Montréal", "Vancouver"],
value="Toronto",
searchable=False,
),
html.Div(id="output"),
]
)

@app.callback(Output("output", "children"), [Input("dropdown", "value")])
def callback(value):
return f"You have selected {value}"

dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#output", "You have selected Toronto")

dash_duo.select_dcc_dropdown("#dropdown", "Vancouver")
dash_duo.wait_for_text_to_equal("#output", "You have selected Vancouver")

dash_duo.select_dcc_dropdown("#dropdown", "Toronto")
dash_duo.wait_for_text_to_equal("#output", "You have selected Toronto")

assert dash_duo.get_logs() == []