Skip to content

Commit

Permalink
Merge pull request #1970 from plotly/1868-dropdown-remove-option
Browse files Browse the repository at this point in the history
Fix Dropdown multi option removed update value.
  • Loading branch information
T4rk1n authored Apr 21, 2022
2 parents 4b03e5a + ed5942e commit f6b51a8
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 122 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- 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.
- Many other mainly JS dependency upgrades to the internals of Dash renderer and components. These may patch bugs or improve performance.

### Fixed

- [#1970](https://github.com/plotly/dash/pull/1970) dcc.Dropdown Refactor fixes:
- Fix bug [#1868](https://github.com/plotly/dash/issues/1868) value does not update when selected option removed from options.
- Fix bug [#1908](https://github.com/plotly/dash/issues/1908) Selected options not showing when the value contains a comma.

## [2.3.1] - 2022-03-29

### Fixed
Expand Down
181 changes: 101 additions & 80 deletions components/dash-core-components/src/fragments/Dropdown.react.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {isNil, pluck, omit, type} from 'ramda';
import React, {Component} from 'react';
import {isNil, pluck, without, pick} from 'ramda';
import React, {useState, useCallback, useEffect, useMemo} from 'react';
import ReactDropdown from 'react-virtualized-select';
import createFilterOptions from 'react-select-fast-filter-options';
import '../components/css/react-virtualized-select@3.1.0.css';
Expand All @@ -21,90 +21,111 @@ const TOKENIZER = {
},
};

const DELIMITER = ',';
const RDProps = [
'multi',
'clearable',
'searchable',
'search_value',
'placeholder',
'disabled',
'optionHeight',
'style',
'className',
];

export default class Dropdown extends Component {
constructor(props) {
super(props);
this.state = {
filterOptions: createFilterOptions({
options: sanitizeOptions(props.options),
const Dropdown = props => {
const {
id,
clearable,
multi,
options,
setProps,
style,
loading_state,
value,
} = props;
const [optionsCheck, setOptionsCheck] = useState(null);
const [sanitizedOptions, filterOptions] = useMemo(() => {
const sanitized = sanitizeOptions(options);
return [
sanitized,
createFilterOptions({
options: sanitized,
tokenizer: TOKENIZER,
}),
};
}
];
}, [options]);

UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.options !== this.props.options) {
this.setState({
filterOptions: createFilterOptions({
options: sanitizeOptions(newProps.options),
tokenizer: TOKENIZER,
}),
});
}
}
const onChange = useCallback(
selectedOption => {
if (multi) {
let value;
if (isNil(selectedOption)) {
value = [];
} else {
value = pluck('value', selectedOption);
}
setProps({value});
} else {
let value;
if (isNil(selectedOption)) {
value = null;
} else {
value = selectedOption.value;
}
setProps({value});
}
},
[multi]
);

render() {
const {
id,
clearable,
multi,
options,
setProps,
style,
loading_state,
value,
} = this.props;
const {filterOptions} = this.state;
let selectedValue;
if (type(value) === 'Array') {
selectedValue = value.join(DELIMITER);
} else {
selectedValue = value;
}
return (
<div
id={id}
className="dash-dropdown"
style={style}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
const onInputChange = useCallback(
search_value => setProps({search_value}),
[]
);

useEffect(() => {
if (optionsCheck !== sanitizedOptions && !isNil(value)) {
const values = sanitizedOptions.map(option => option.value);
if (multi && Array.isArray(value)) {
const invalids = value.filter(v => !values.includes(v));
if (invalids.length) {
setProps({value: without(invalids, value)});
}
>
<ReactDropdown
filterOptions={filterOptions}
options={sanitizeOptions(options)}
value={selectedValue}
onChange={selectedOption => {
if (multi) {
let value;
if (isNil(selectedOption)) {
value = [];
} else {
value = pluck('value', selectedOption);
}
setProps({value});
} else {
let value;
if (isNil(selectedOption)) {
value = null;
} else {
value = selectedOption.value;
}
setProps({value});
}
}}
onInputChange={search_value => setProps({search_value})}
backspaceRemoves={clearable}
deleteRemoves={clearable}
inputProps={{autoComplete: 'off'}}
{...omit(['setProps', 'value', 'options'], this.props)}
/>
</div>
);
}
}
} else {
if (!values.includes(value)) {
setProps({value: null});
}
}
setOptionsCheck(sanitizedOptions);
}
}, [sanitizedOptions, optionsCheck, multi, value]);

return (
<div
id={id}
className="dash-dropdown"
style={style}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
>
<ReactDropdown
filterOptions={filterOptions}
options={sanitizeOptions(options)}
value={value}
onChange={onChange}
onInputChange={onInputChange}
backspaceRemoves={clearable}
deleteRemoves={clearable}
inputProps={{autoComplete: 'off'}}
{...pick(RDProps, props)}
/>
</div>
);
};

Dropdown.propTypes = propTypes;
Dropdown.defaultProps = defaultProps;

export default Dropdown;
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,18 @@ def update_options(search_value):
assert dash_dcc.get_logs() == []


def test_dddo002_array_value(dash_dcc):
dropdown_options = [
{"label": "New York City", "value": "New,York,City"},
{"label": "Montreal", "value": "Montreal"},
{"label": "San Francisco", "value": "San,Francisco"},
]

def test_dddo002_array_comma_value(dash_dcc):
app = Dash(__name__)
arrayValue = ["San", "Francisco"]

dropdown = dcc.Dropdown(
options=dropdown_options,
value=arrayValue,
options=["New York, NY", "Montreal, QC", "San Francisco, CA"],
value=["San Francisco, CA"],
multi=True,
)
app.layout = html.Div([dropdown])
app.layout = html.Div(dropdown)

dash_dcc.start_server(app)

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

assert dash_dcc.get_logs() == []
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import json

from dash import Dash, html, dcc, Output, Input
from dash.exceptions import PreventUpdate


sample_dropdown_options = [
{"label": "New York City", "value": "NYC"},
{"label": "Montreal", "value": "MTL"},
{"label": "San Francisco", "value": "SF"},
]


def test_ddro001_remove_option_single(dash_dcc):
dropdown_options = sample_dropdown_options

app = Dash(__name__)
value = "SF"

app.layout = html.Div(
[
dcc.Dropdown(
options=dropdown_options,
value=value,
id="dropdown",
),
html.Button("Remove option", id="remove"),
html.Div(id="value-output"),
]
)

@app.callback(Output("dropdown", "options"), [Input("remove", "n_clicks")])
def on_click(n_clicks):
if not n_clicks:
raise PreventUpdate
return sample_dropdown_options[:-1]

@app.callback(Output("value-output", "children"), [Input("dropdown", "value")])
def on_change(val):
if not val:
raise PreventUpdate
return val or "None"

dash_dcc.start_server(app)
btn = dash_dcc.wait_for_element("#remove")
btn.click()

dash_dcc.wait_for_text_to_equal("#value-output", "None")


def test_ddro002_remove_option_multi(dash_dcc):
dropdown_options = sample_dropdown_options

app = Dash(__name__)
value = ["MTL", "SF"]

app.layout = html.Div(
[
dcc.Dropdown(
options=dropdown_options,
value=value,
multi=True,
id="dropdown",
),
html.Button("Remove option", id="remove"),
html.Div(id="value-output"),
]
)

@app.callback(Output("dropdown", "options"), [Input("remove", "n_clicks")])
def on_click(n_clicks):
if not n_clicks:
raise PreventUpdate
return sample_dropdown_options[:-1]

@app.callback(Output("value-output", "children"), [Input("dropdown", "value")])
def on_change(val):
return json.dumps(val)

dash_dcc.start_server(app)
btn = dash_dcc.wait_for_element("#remove")
btn.click()

dash_dcc.wait_for_text_to_equal("#value-output", '["MTL"]')
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,27 @@

_js_dist = [
{
"relative_package_path": 'html/{}.min.js'.format(_this_module),
"relative_package_path": "html/{}.min.js".format(_this_module),
"external_url": (
"https://unpkg.com/dash-html-components@{}"
"/dash_html_components/dash_html_components.min.js"
).format(__version__),
"namespace": "dash"
"namespace": "dash",
},
{
'relative_package_path': 'html/{}.min.js.map'.format(_this_module),
'external_url': (
'https://unpkg.com/dash-html-components@{}'
'/dash_html_components/dash_html_components.min.js.map'
"relative_package_path": "html/{}.min.js.map".format(_this_module),
"external_url": (
"https://unpkg.com/dash-html-components@{}"
"/dash_html_components/dash_html_components.min.js.map"
).format(__version__),
'namespace': 'dash',
'dynamic': True
}
"namespace": "dash",
"dynamic": True,
},
]

_css_dist = []


for _component in __all__:
setattr(locals()[_component], '_js_dist', _js_dist)
setattr(locals()[_component], '_css_dist', _css_dist)
setattr(locals()[_component], "_js_dist", _js_dist)
setattr(locals()[_component], "_css_dist", _css_dist)
20 changes: 10 additions & 10 deletions components/dash-html-components/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@
import json
from setuptools import setup

with open('package.json') as f:
with open("package.json") as f:
package = json.load(f)

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

setup(
name='dash_html_components',
name="dash_html_components",
version=package["version"],
author=package['author'],
author_email='chris@plotly.com',
author=package["author"],
author_email="chris@plotly.com",
packages=[package_name],
url='https://github.com/plotly/dash-html-components',
url="https://github.com/plotly/dash-html-components",
include_package_data=True,
license=package['license'],
description=package['description'] if 'description' in package else package_name,
long_description=io.open('README.md', encoding='utf-8').read(),
long_description_content_type='text/markdown',
install_requires=[]
license=package["license"],
description=package["description"] if "description" in package else package_name,
long_description=io.open("README.md", encoding="utf-8").read(),
long_description_content_type="text/markdown",
install_requires=[],
)
Loading

0 comments on commit f6b51a8

Please sign in to comment.