diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ab0315eb0..eb46a1c33d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js
index 43dd15e006..c7ca287d59 100644
--- a/components/dash-core-components/src/fragments/Dropdown.react.js
+++ b/components/dash-core-components/src/fragments/Dropdown.react.js
@@ -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';
@@ -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 (
-
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)});
}
- >
- {
- 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)}
- />
-
- );
- }
-}
+ } else {
+ if (!values.includes(value)) {
+ setProps({value: null});
+ }
+ }
+ setOptionsCheck(sanitizedOptions);
+ }
+ }, [sanitizedOptions, optionsCheck, multi, value]);
+
+ return (
+
+
+
+ );
+};
Dropdown.propTypes = propTypes;
Dropdown.defaultProps = defaultProps;
+
+export default Dropdown;
diff --git a/components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py b/components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py
index c16c3f39eb..cd15bd709b 100644
--- a/components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py
+++ b/components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py
@@ -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() == []
diff --git a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py
new file mode 100644
index 0000000000..f56371b6ab
--- /dev/null
+++ b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py
@@ -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"]')
diff --git a/components/dash-html-components/dash_html_components_base/__init__.py b/components/dash-html-components/dash_html_components_base/__init__.py
index dee9b02959..fb4145dccf 100644
--- a/components/dash-html-components/dash_html_components_base/__init__.py
+++ b/components/dash-html-components/dash_html_components_base/__init__.py
@@ -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)
diff --git a/components/dash-html-components/setup.py b/components/dash-html-components/setup.py
index ecd37e2b84..cf8fc48205 100644
--- a/components/dash-html-components/setup.py
+++ b/components/dash-html-components/setup.py
@@ -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=[],
)
diff --git a/components/dash-html-components/tests/test_dash_html_components.py b/components/dash-html-components/tests/test_dash_html_components.py
index 8ce332d445..206f4fb3e8 100644
--- a/components/dash-html-components/tests/test_dash_html_components.py
+++ b/components/dash-html-components/tests/test_dash_html_components.py
@@ -23,7 +23,7 @@ def test_imports():
def test_sample_items():
layout = html.Div(
html.Div(html.Img(src="https://plotly.com/~chris/1638.png")),
- style={"color": "red"}
+ style={"color": "red"},
)
expected = (
diff --git a/components/dash-html-components/tests/utils.py b/components/dash-html-components/tests/utils.py
index 349264fd26..89791183ce 100644
--- a/components/dash-html-components/tests/utils.py
+++ b/components/dash-html-components/tests/utils.py
@@ -4,18 +4,14 @@
def assert_clean_console(TestClass):
def assert_no_console_errors(TestClass):
TestClass.assertEqual(
- TestClass.driver.execute_script(
- 'return window.tests.console.error.length'
- ),
- 0
+ TestClass.driver.execute_script("return window.tests.console.error.length"),
+ 0,
)
def assert_no_console_warnings(TestClass):
TestClass.assertEqual(
- TestClass.driver.execute_script(
- 'return window.tests.console.warn.length'
- ),
- 0
+ TestClass.driver.execute_script("return window.tests.console.warn.length"),
+ 0,
)
assert_no_console_warnings(TestClass)
diff --git a/package.json b/package.json
index 1c9bfbdcf8..383a2240c0 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"scripts": {
"private::format.black": "black dash tests --exclude metadata_test.py",
"private::format.renderer": "cd dash/dash-renderer && npm run format",
+ "private::format.dcc": "cd components/dash-core-components && npm run format",
"private::initialize.renderer": "cd dash/dash-renderer && npm ci",
"private::build.components": "python dash/development/update_components.py 'all'",
"private::build.renderer": "cd dash/dash-renderer && renderer build",