Skip to content

Commit f6b51a8

Browse files
authored
Merge pull request #1970 from plotly/1868-dropdown-remove-option
Fix Dropdown multi option removed update value.
2 parents 4b03e5a + ed5942e commit f6b51a8

File tree

9 files changed

+224
-122
lines changed

9 files changed

+224
-122
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ This project adheres to [Semantic Versioning](https://semver.org/).
2020
- Upgrade `black` to v22.3.0 for Python 3.7+ - if you use `dash[ci]` and you call `black`, this may alter your code formatting slightly, including more consistently breaking Python 2 compatibility.
2121
- Many other mainly JS dependency upgrades to the internals of Dash renderer and components. These may patch bugs or improve performance.
2222

23+
### Fixed
24+
25+
- [#1970](https://github.com/plotly/dash/pull/1970) dcc.Dropdown Refactor fixes:
26+
- Fix bug [#1868](https://github.com/plotly/dash/issues/1868) value does not update when selected option removed from options.
27+
- Fix bug [#1908](https://github.com/plotly/dash/issues/1908) Selected options not showing when the value contains a comma.
28+
2329
## [2.3.1] - 2022-03-29
2430

2531
### Fixed
Lines changed: 101 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {isNil, pluck, omit, type} from 'ramda';
2-
import React, {Component} from 'react';
1+
import {isNil, pluck, without, pick} from 'ramda';
2+
import React, {useState, useCallback, useEffect, useMemo} from 'react';
33
import ReactDropdown from 'react-virtualized-select';
44
import createFilterOptions from 'react-select-fast-filter-options';
55
import '../components/css/[email protected]';
@@ -21,90 +21,111 @@ const TOKENIZER = {
2121
},
2222
};
2323

24-
const DELIMITER = ',';
24+
const RDProps = [
25+
'multi',
26+
'clearable',
27+
'searchable',
28+
'search_value',
29+
'placeholder',
30+
'disabled',
31+
'optionHeight',
32+
'style',
33+
'className',
34+
];
2535

26-
export default class Dropdown extends Component {
27-
constructor(props) {
28-
super(props);
29-
this.state = {
30-
filterOptions: createFilterOptions({
31-
options: sanitizeOptions(props.options),
36+
const Dropdown = props => {
37+
const {
38+
id,
39+
clearable,
40+
multi,
41+
options,
42+
setProps,
43+
style,
44+
loading_state,
45+
value,
46+
} = props;
47+
const [optionsCheck, setOptionsCheck] = useState(null);
48+
const [sanitizedOptions, filterOptions] = useMemo(() => {
49+
const sanitized = sanitizeOptions(options);
50+
return [
51+
sanitized,
52+
createFilterOptions({
53+
options: sanitized,
3254
tokenizer: TOKENIZER,
3355
}),
34-
};
35-
}
56+
];
57+
}, [options]);
3658

37-
UNSAFE_componentWillReceiveProps(newProps) {
38-
if (newProps.options !== this.props.options) {
39-
this.setState({
40-
filterOptions: createFilterOptions({
41-
options: sanitizeOptions(newProps.options),
42-
tokenizer: TOKENIZER,
43-
}),
44-
});
45-
}
46-
}
59+
const onChange = useCallback(
60+
selectedOption => {
61+
if (multi) {
62+
let value;
63+
if (isNil(selectedOption)) {
64+
value = [];
65+
} else {
66+
value = pluck('value', selectedOption);
67+
}
68+
setProps({value});
69+
} else {
70+
let value;
71+
if (isNil(selectedOption)) {
72+
value = null;
73+
} else {
74+
value = selectedOption.value;
75+
}
76+
setProps({value});
77+
}
78+
},
79+
[multi]
80+
);
4781

48-
render() {
49-
const {
50-
id,
51-
clearable,
52-
multi,
53-
options,
54-
setProps,
55-
style,
56-
loading_state,
57-
value,
58-
} = this.props;
59-
const {filterOptions} = this.state;
60-
let selectedValue;
61-
if (type(value) === 'Array') {
62-
selectedValue = value.join(DELIMITER);
63-
} else {
64-
selectedValue = value;
65-
}
66-
return (
67-
<div
68-
id={id}
69-
className="dash-dropdown"
70-
style={style}
71-
data-dash-is-loading={
72-
(loading_state && loading_state.is_loading) || undefined
82+
const onInputChange = useCallback(
83+
search_value => setProps({search_value}),
84+
[]
85+
);
86+
87+
useEffect(() => {
88+
if (optionsCheck !== sanitizedOptions && !isNil(value)) {
89+
const values = sanitizedOptions.map(option => option.value);
90+
if (multi && Array.isArray(value)) {
91+
const invalids = value.filter(v => !values.includes(v));
92+
if (invalids.length) {
93+
setProps({value: without(invalids, value)});
7394
}
74-
>
75-
<ReactDropdown
76-
filterOptions={filterOptions}
77-
options={sanitizeOptions(options)}
78-
value={selectedValue}
79-
onChange={selectedOption => {
80-
if (multi) {
81-
let value;
82-
if (isNil(selectedOption)) {
83-
value = [];
84-
} else {
85-
value = pluck('value', selectedOption);
86-
}
87-
setProps({value});
88-
} else {
89-
let value;
90-
if (isNil(selectedOption)) {
91-
value = null;
92-
} else {
93-
value = selectedOption.value;
94-
}
95-
setProps({value});
96-
}
97-
}}
98-
onInputChange={search_value => setProps({search_value})}
99-
backspaceRemoves={clearable}
100-
deleteRemoves={clearable}
101-
inputProps={{autoComplete: 'off'}}
102-
{...omit(['setProps', 'value', 'options'], this.props)}
103-
/>
104-
</div>
105-
);
106-
}
107-
}
95+
} else {
96+
if (!values.includes(value)) {
97+
setProps({value: null});
98+
}
99+
}
100+
setOptionsCheck(sanitizedOptions);
101+
}
102+
}, [sanitizedOptions, optionsCheck, multi, value]);
103+
104+
return (
105+
<div
106+
id={id}
107+
className="dash-dropdown"
108+
style={style}
109+
data-dash-is-loading={
110+
(loading_state && loading_state.is_loading) || undefined
111+
}
112+
>
113+
<ReactDropdown
114+
filterOptions={filterOptions}
115+
options={sanitizeOptions(options)}
116+
value={value}
117+
onChange={onChange}
118+
onInputChange={onInputChange}
119+
backspaceRemoves={clearable}
120+
deleteRemoves={clearable}
121+
inputProps={{autoComplete: 'off'}}
122+
{...pick(RDProps, props)}
123+
/>
124+
</div>
125+
);
126+
};
108127

109128
Dropdown.propTypes = propTypes;
110129
Dropdown.defaultProps = defaultProps;
130+
131+
export default Dropdown;

components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,24 +52,18 @@ def update_options(search_value):
5252
assert dash_dcc.get_logs() == []
5353

5454

55-
def test_dddo002_array_value(dash_dcc):
56-
dropdown_options = [
57-
{"label": "New York City", "value": "New,York,City"},
58-
{"label": "Montreal", "value": "Montreal"},
59-
{"label": "San Francisco", "value": "San,Francisco"},
60-
]
61-
55+
def test_dddo002_array_comma_value(dash_dcc):
6256
app = Dash(__name__)
63-
arrayValue = ["San", "Francisco"]
6457

6558
dropdown = dcc.Dropdown(
66-
options=dropdown_options,
67-
value=arrayValue,
59+
options=["New York, NY", "Montreal, QC", "San Francisco, CA"],
60+
value=["San Francisco, CA"],
61+
multi=True,
6862
)
69-
app.layout = html.Div([dropdown])
63+
app.layout = html.Div(dropdown)
7064

7165
dash_dcc.start_server(app)
7266

73-
dash_dcc.wait_for_text_to_equal("#react-select-2--value-item", "San Francisco")
67+
dash_dcc.wait_for_text_to_equal("#react-select-2--value-0", "San Francisco, CA\n ")
7468

7569
assert dash_dcc.get_logs() == []
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import json
2+
3+
from dash import Dash, html, dcc, Output, Input
4+
from dash.exceptions import PreventUpdate
5+
6+
7+
sample_dropdown_options = [
8+
{"label": "New York City", "value": "NYC"},
9+
{"label": "Montreal", "value": "MTL"},
10+
{"label": "San Francisco", "value": "SF"},
11+
]
12+
13+
14+
def test_ddro001_remove_option_single(dash_dcc):
15+
dropdown_options = sample_dropdown_options
16+
17+
app = Dash(__name__)
18+
value = "SF"
19+
20+
app.layout = html.Div(
21+
[
22+
dcc.Dropdown(
23+
options=dropdown_options,
24+
value=value,
25+
id="dropdown",
26+
),
27+
html.Button("Remove option", id="remove"),
28+
html.Div(id="value-output"),
29+
]
30+
)
31+
32+
@app.callback(Output("dropdown", "options"), [Input("remove", "n_clicks")])
33+
def on_click(n_clicks):
34+
if not n_clicks:
35+
raise PreventUpdate
36+
return sample_dropdown_options[:-1]
37+
38+
@app.callback(Output("value-output", "children"), [Input("dropdown", "value")])
39+
def on_change(val):
40+
if not val:
41+
raise PreventUpdate
42+
return val or "None"
43+
44+
dash_dcc.start_server(app)
45+
btn = dash_dcc.wait_for_element("#remove")
46+
btn.click()
47+
48+
dash_dcc.wait_for_text_to_equal("#value-output", "None")
49+
50+
51+
def test_ddro002_remove_option_multi(dash_dcc):
52+
dropdown_options = sample_dropdown_options
53+
54+
app = Dash(__name__)
55+
value = ["MTL", "SF"]
56+
57+
app.layout = html.Div(
58+
[
59+
dcc.Dropdown(
60+
options=dropdown_options,
61+
value=value,
62+
multi=True,
63+
id="dropdown",
64+
),
65+
html.Button("Remove option", id="remove"),
66+
html.Div(id="value-output"),
67+
]
68+
)
69+
70+
@app.callback(Output("dropdown", "options"), [Input("remove", "n_clicks")])
71+
def on_click(n_clicks):
72+
if not n_clicks:
73+
raise PreventUpdate
74+
return sample_dropdown_options[:-1]
75+
76+
@app.callback(Output("value-output", "children"), [Input("dropdown", "value")])
77+
def on_change(val):
78+
return json.dumps(val)
79+
80+
dash_dcc.start_server(app)
81+
btn = dash_dcc.wait_for_element("#remove")
82+
btn.click()
83+
84+
dash_dcc.wait_for_text_to_equal("#value-output", '["MTL"]')

components/dash-html-components/dash_html_components_base/__init__.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,27 @@
3333

3434
_js_dist = [
3535
{
36-
"relative_package_path": 'html/{}.min.js'.format(_this_module),
36+
"relative_package_path": "html/{}.min.js".format(_this_module),
3737
"external_url": (
3838
"https://unpkg.com/dash-html-components@{}"
3939
"/dash_html_components/dash_html_components.min.js"
4040
).format(__version__),
41-
"namespace": "dash"
41+
"namespace": "dash",
4242
},
4343
{
44-
'relative_package_path': 'html/{}.min.js.map'.format(_this_module),
45-
'external_url': (
46-
'https://unpkg.com/dash-html-components@{}'
47-
'/dash_html_components/dash_html_components.min.js.map'
44+
"relative_package_path": "html/{}.min.js.map".format(_this_module),
45+
"external_url": (
46+
"https://unpkg.com/dash-html-components@{}"
47+
"/dash_html_components/dash_html_components.min.js.map"
4848
).format(__version__),
49-
'namespace': 'dash',
50-
'dynamic': True
51-
}
49+
"namespace": "dash",
50+
"dynamic": True,
51+
},
5252
]
5353

5454
_css_dist = []
5555

5656

5757
for _component in __all__:
58-
setattr(locals()[_component], '_js_dist', _js_dist)
59-
setattr(locals()[_component], '_css_dist', _css_dist)
58+
setattr(locals()[_component], "_js_dist", _js_dist)
59+
setattr(locals()[_component], "_css_dist", _css_dist)

components/dash-html-components/setup.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22
import json
33
from setuptools import setup
44

5-
with open('package.json') as f:
5+
with open("package.json") as f:
66
package = json.load(f)
77

88
package_name = str(package["name"].replace(" ", "_").replace("-", "_"))
99

1010
setup(
11-
name='dash_html_components',
11+
name="dash_html_components",
1212
version=package["version"],
13-
author=package['author'],
14-
author_email='[email protected]',
13+
author=package["author"],
14+
author_email="[email protected]",
1515
packages=[package_name],
16-
url='https://github.com/plotly/dash-html-components',
16+
url="https://github.com/plotly/dash-html-components",
1717
include_package_data=True,
18-
license=package['license'],
19-
description=package['description'] if 'description' in package else package_name,
20-
long_description=io.open('README.md', encoding='utf-8').read(),
21-
long_description_content_type='text/markdown',
22-
install_requires=[]
18+
license=package["license"],
19+
description=package["description"] if "description" in package else package_name,
20+
long_description=io.open("README.md", encoding="utf-8").read(),
21+
long_description_content_type="text/markdown",
22+
install_requires=[],
2323
)

0 commit comments

Comments
 (0)