From 5928d80f54ea165bc3e5a2f0564e31b8e1a49bf2 Mon Sep 17 00:00:00 2001 From: Gabriel Micko Date: Fri, 1 Mar 2019 17:11:23 +0100 Subject: [PATCH] feat: adds support for dropdown being used as controlled component --- packages/react-dropdown/src/MultiDownshift.js | 96 +++++++++++-------- packages/react-dropdown/src/MultiSelect.md | 2 +- packages/react-dropdown/src/index.js | 6 +- packages/react-dropdown/src/index.md | 40 +++++++- packages/react-dropdown/src/index.test.js | 31 +++++- yarn.lock | 2 +- 6 files changed, 132 insertions(+), 45 deletions(-) diff --git a/packages/react-dropdown/src/MultiDownshift.js b/packages/react-dropdown/src/MultiDownshift.js index 2e2d4202..93e3ce14 100644 --- a/packages/react-dropdown/src/MultiDownshift.js +++ b/packages/react-dropdown/src/MultiDownshift.js @@ -18,7 +18,8 @@ export type MultiControllerStateAndHelpers = ControllerStateAndHelpers, + initialSelectedItems: Array, + selectedItems?: Array, onSelect?: ( Array, MultiControllerStateAndHelpers @@ -38,7 +39,9 @@ type State = { }; class MultiDownshift extends React.Component { - state = { selectedItems: this.props.selectedItems }; + state = { + selectedItems: this.props.initialSelectedItems, + }; stateReducer = ( state: ControllerStateAndHelpers, @@ -65,80 +68,97 @@ class MultiDownshift extends React.Component { selectedItem: DropdownSelectedItem | null, downshift: ControllerStateAndHelpers ) => { - const callOnChange = () => { + const callOnChange = selectedItems => { const { onChange } = this.props; - const { selectedItems } = this.state; if (onChange) { onChange(selectedItems, this.getStateAndHelpers(downshift)); } }; + const selectedItems = this.getSelectedItems(); + let newSelectedItems = []; if (selectedItem === null) { - this.clearItems(callOnChange); + newSelectedItems = this.clearItems(); } else { if (this.props.multiselect) { - if (includesId(this.state.selectedItems, selectedItem.id)) { - this.removeItem(selectedItem, callOnChange); + if (includesId(selectedItems, selectedItem.id)) { + newSelectedItems = this.removeItem(selectedItem, selectedItems); } else { - this.addSelectedItem(selectedItem, callOnChange); + newSelectedItems = this.addSelectedItem(selectedItem, selectedItems); } } else { - this.replaceItem(selectedItem, callOnChange); + newSelectedItems = this.replaceItem(selectedItem); } } + + if (this.isSelectedItemsPresentInProps()) { + callOnChange(newSelectedItems); + } else { + this.setState( + { + selectedItems: newSelectedItems, + }, + () => { + callOnChange(this.state.selectedItems); + } + ); + } }; - clearItems = (cb?: () => void) => { - this.setState(({ selectedItems }) => { - return { - selectedItems: [], - }; - }, cb); + isSelectedItemsPresentInProps() { + return this.props.selectedItems ? true : false; + } + + getSelectedItems() { + if (this.props.selectedItems) { + return this.props.selectedItems; + } + return this.state.selectedItems; + } + + clearItems = () => { + return []; }; - replaceItem = (item: DropdownSelectedItem, cb?: () => void) => { - this.setState(({ selectedItems }) => { - return { - selectedItems: [item], - }; - }, cb); + replaceItem = (item: DropdownSelectedItem) => { + return [item]; }; - removeItem = (item: DropdownSelectedItem, cb?: () => void) => { - this.setState(({ selectedItems }) => { - return { - selectedItems: selectedItems.filter(({ id }) => id !== item.id), - }; - }, cb); + removeItem = ( + item: DropdownSelectedItem, + selectedItems: Array + ): Array => { + return selectedItems.filter(({ id }) => id !== item.id); }; - addSelectedItem = (item: DropdownSelectedItem, cb?: () => void) => { - this.setState( - ({ selectedItems }) => ({ - selectedItems: [...selectedItems, item], - }), - cb - ); + addSelectedItem = ( + item: DropdownSelectedItem, + selectedItems: Array + ) => { + return [...selectedItems, item]; }; getStateAndHelpers = ( downshift: ControllerStateAndHelpers ): MultiControllerStateAndHelpers => { - const { selectedItems } = this.state; const { removeItem } = this; + return { removeItem, - selectedItems, + selectedItems: this.getSelectedItems(), ...downshift, }; }; render() { - const { multiselect, children, selectedItem, ...props } = this.props; + const { multiselect, children, ...props } = this.props; + const selectedItems = this.getSelectedItems(); return ( diff --git a/packages/react-dropdown/src/MultiSelect.md b/packages/react-dropdown/src/MultiSelect.md index 84e2c2e9..448eeb90 100644 --- a/packages/react-dropdown/src/MultiSelect.md +++ b/packages/react-dropdown/src/MultiSelect.md @@ -103,7 +103,7 @@ const handleDelete = ({ id }) => { items={items} categories={categories} multiselect={true} - selectedItems={selectedItems} + initialSelectedItems={selectedItems} onChange={mirrorDropdownState} > {({ getInputProps, removeItem, ...props }) => { diff --git a/packages/react-dropdown/src/index.js b/packages/react-dropdown/src/index.js index 3402c461..607a8a27 100644 --- a/packages/react-dropdown/src/index.js +++ b/packages/react-dropdown/src/index.js @@ -33,6 +33,7 @@ const DevFragment = type Props = { items: Array, categories?: Array, + initialSelectedItems?: Array, selectedItems?: Array, useFilter?: boolean, filterFn?: ( @@ -74,7 +75,8 @@ const Dropdown = ({ children, name = 'dropdown', twoColumn = true, - selectedItems = [], + initialSelectedItems = [], + selectedItems, initialIsOpen = false, placement = 'bottom-start', popperModifiers, @@ -86,9 +88,9 @@ const Dropdown = ({ }: Props) => ( (item && item.label ? item.label : '')} + initialSelectedItems={initialSelectedItems} selectedItems={selectedItems} initialIsOpen={initialIsOpen} onChange={onChange} diff --git a/packages/react-dropdown/src/index.md b/packages/react-dropdown/src/index.md index eed8ae0f..8b25f9fe 100644 --- a/packages/react-dropdown/src/index.md +++ b/packages/react-dropdown/src/index.md @@ -20,7 +20,43 @@ const items = [ { id: 131, label: 'One' }, ]; - + + {({ getInputProps }) => } +; +``` + +This is usecase will allow you to use `Dropdown` as a controlled component. + +```js +initialState = { + selectedItems: [{ id: 10, label: 'One' }], +}; + +const items = [ + { id: 10, label: 'One' }, + { id: 22, label: 'Two' }, + { id: 33, label: 'Three' }, + { id: 44, label: 'Four' }, + { id: 55, label: 'Four' }, + { id: 66, label: 'Three' }, + { id: 77, label: 'Four' }, + { id: 88, label: 'Three' }, + { id: 99, label: 'Four' }, + { id: 101, label: 'Three' }, + { id: 111, label: 'Four' }, + { id: 121, label: 'Three' }, + { id: 131, label: 'One' }, +]; + + { + setState({ + selectedItems: newSelectedItems, + }); + }} +> {({ getInputProps }) => } ; ``` @@ -56,7 +92,7 @@ const categories = [ items={items} categories={categories} twoColumn={false} - selectedItems={[items[2]]} + initialSelectedItems={[items[2]]} > {({ getInputProps }) => } ; diff --git a/packages/react-dropdown/src/index.test.js b/packages/react-dropdown/src/index.test.js index e99c9fb6..2e9898ee 100644 --- a/packages/react-dropdown/src/index.test.js +++ b/packages/react-dropdown/src/index.test.js @@ -559,7 +559,10 @@ it('should clear the previous selected items', () => { ); - wrapper.find('ul li').simulate('click'); + wrapper + .find('ul li') + .at(0) + .simulate('click'); expect( wrapper @@ -577,3 +580,29 @@ it('should clear the previous selected items', () => { .props().value ).toBe(''); }); + +it('onChange should be called with the newly selected items', () => { + const onChangeFn = jest.fn(); + + const wrapper = mount( + + {({ getInputProps }) => } + + ); + + wrapper + .find('ul li') + .hostNodes() + .at(1) + .simulate('click'); + + expect(onChangeFn).toHaveBeenCalledWith( + [{ id: 22, label: 'Two' }], + expect.anything() + ); +}); diff --git a/yarn.lock b/yarn.lock index 2ce6d247..77a7054f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12061,7 +12061,7 @@ react-resize-aware@^2.7.2: resolved "https://registry.npmjs.org/react-resize-aware/-/react-resize-aware-2.7.2.tgz#38a0040daaa28dfa9b88994889fbb1e2aa66df83" integrity sha512-XyweQ3YTiZzNG1T5PNpCXvnpsI7mdy4A2oeySFToh8v02Yf+kOiUyWprvyp6T68fFB848w2F3RKfzH7KDk75JQ== -react-router-dom@^4.3.1: +react-router-dom@^4.0.0, react-router-dom@^4.3.1: version "4.3.1" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==