Skip to content
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

Fix Dropdown multi option removed update value. #1970

Merged
merged 9 commits into from
Apr 21, 2022
Merged
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
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),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha, we should include this file in our linting / formatting runs. If we did, the new linter would ask for this to become an f-string ;)

"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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can delete components/*/setup.py now, right? Also MANIFEST.in, maybe some others too... same in dash-renderer.

Anyway I won't make you do this now, but I'm reminded of it after seeing this file being edited.

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