Skip to content
This repository was archived by the owner on Jan 15, 2021. It is now read-only.

Commit 22db05f

Browse files
committed
fix(Picky): Stop using internal state
Internal state was flaky to manage, there wasn't a real benefit to being an uncontrolled component since you needed to provide onChange and a value anyway. A couple of bugs have been caused due to this. Better just to remove it. BREAKING CHANGE: No more uncontrolled components. Use onChange and value, set the value from state and update the state from onChange.
1 parent a5b6661 commit 22db05f

7 files changed

Lines changed: 69 additions & 108 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Picky.propTypes = {
137137
- `numberDisplayed` - Then number of selected options displayed until it turns into '(selected count) selected'.
138138
- `multiple` - Set to true for a multiselect, defaults to false.
139139
- `options` - Array of possible options.
140-
- `onChange` - Called whenever selected value(s) have changed. If you are using this as a controlled component, pass the selected value back into `value`.
140+
- `onChange` - Called whenever selected value(s) have changed. Pass the selected value back into `value`.
141141
- `open` - Can open or close the dropdown manually. Defaults to false.
142142
- `includeSelectAll` - If set to `true` will add a `Select All` checkbox at the top of the list.
143143
- `includeFilter` - If set to `true` will add an input at the top of the dropdown for filtering the results.

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ This is a multiselect with checkboxes, a select all option, and a filter. Along
2525

2626
If you like the tag list like [React-Select](https://github.com/JedWatson/react-select), then that would be a great option for you. It's a really great, well-tested library. Give it a look.
2727

28+
You can also achieve the same result with a great deal of flexibility using [Paypal's Downshift](https://github.com/paypal/downshift#usage).
29+
2830
# Peer Dependencies
2931

3032
```
@@ -135,7 +137,7 @@ Picky.propTypes = {
135137
- `numberDisplayed` - Then number of selected options displayed until it turns into '(selected count) selected'.
136138
- `multiple` - Set to true for a multiselect, defaults to false.
137139
- `options` - Array of possible options.
138-
- `onChange` - Called whenever selected value(s) have changed. If you are using this as a controlled component, pass the selected value back into `value`.
140+
- `onChange` - Called whenever selected value(s) have changed. Pass the selected value back into `value`.
139141
- `open` - Can open or close the dropdown manually. Defaults to false.
140142
- `includeSelectAll` - If set to `true` will add a `Select All` checkbox at the top of the list.
141143
- `includeFilter` - If set to `true` will add an input at the top of the dropdown for filtering the results.

src/Filter.js

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
1-
import React, { Component } from 'react';
1+
import React, { PureComponent } from 'react';
22
import PropTypes from 'prop-types';
3-
class Filter extends Component {
4-
constructor(props) {
5-
super(props);
6-
this.state = {
7-
filterTerm: '',
8-
};
9-
10-
this.onFilterChange = this.onFilterChange.bind(this);
11-
}
12-
onFilterChange(event) {
13-
const query = event.target.value;
14-
this.setState(
15-
{
16-
filterTerm: query,
17-
},
18-
() => {
19-
this.props.onFilterChange(query);
20-
}
21-
);
22-
}
3+
class Filter extends PureComponent {
234
render() {
245
return (
256
<div className="picky__filter">
@@ -31,8 +12,7 @@ class Filter extends Component {
3112
placeholder="Filter..."
3213
tabIndex={this.props.tabIndex}
3314
aria-label="filter options"
34-
value={this.state.filterTerm}
35-
onChange={this.onFilterChange}
15+
onChange={e => this.props.onFilterChange(e.target.value)}
3616
/>
3717
</div>
3818
);

src/Picky.js

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,23 @@ class Picky extends React.PureComponent {
5050
UNSAFE_componentWillReceiveProps(nextProps) {
5151
if (
5252
this.props.options !== nextProps.options ||
53-
this.state.selectedValue !== nextProps.value
53+
this.props.value !== nextProps.value
5454
) {
5555
this.setState({
56-
allSelected: this.allSelected(),
57-
});
58-
}
59-
60-
if (!isEqual(nextProps.value, this.state.selectedValue)) {
61-
this.setState({
62-
selectedValue: nextProps.value,
63-
allSelected: this.allSelected(nextProps.value),
56+
allSelected: !isEqual(nextProps.value, this.props.value)
57+
? this.allSelected(nextProps.value)
58+
: this.allSelected(),
6459
});
6560
}
6661
}
6762

6863
selectValue(val) {
69-
const valueLookup = this.isControlled()
70-
? this.props.value
71-
: this.state.selectedValue;
64+
const valueLookup = this.props.value;
65+
const isObject = isDataObject(
66+
val,
67+
this.props.valueKey,
68+
this.props.labelKey
69+
);
7270
if (this.props.multiple && Array.isArray(valueLookup)) {
7371
const itemIndex = hasItemIndex(
7472
valueLookup,
@@ -84,26 +82,26 @@ class Picky extends React.PureComponent {
8482
...valueLookup.slice(itemIndex + 1),
8583
];
8684
} else {
87-
selectedValue = [...this.state.selectedValue, val];
85+
let ids = valueLookup;
86+
const item = isObject ? val[this.props.valueKey] : val;
87+
if (isObject) {
88+
ids = valueLookup.map(value => value[this.props.valueKey]);
89+
}
90+
selectedValue =
91+
ids.indexOf(item) === -1
92+
? [...this.props.value, val]
93+
: [...this.props.value];
8894
}
8995
this.setState(
9096
{
91-
selectedValue,
9297
allSelected: this.allSelected(selectedValue),
9398
},
9499
() => {
95-
this.props.onChange(this.state.selectedValue);
100+
this.props.onChange(selectedValue);
96101
}
97102
);
98103
} else {
99-
this.setState(
100-
{
101-
selectedValue: val,
102-
},
103-
() => {
104-
this.props.onChange(this.state.selectedValue);
105-
}
106-
);
104+
this.props.onChange(val);
107105
}
108106
}
109107

@@ -114,7 +112,7 @@ class Picky extends React.PureComponent {
114112
* @memberof Picky
115113
*/
116114
allSelected(overrideSelected) {
117-
const selectedValue = overrideSelected || this.state.selectedValue;
115+
const selectedValue = overrideSelected || this.props.value;
118116
const { options } = this.props;
119117
const copiedOptions = options.slice(0);
120118
const copiedSelectedValue = Array.isArray(selectedValue)
@@ -131,30 +129,25 @@ class Picky extends React.PureComponent {
131129
toggleSelectAll() {
132130
this.setState(
133131
{
134-
selectedValue: !this.state.allSelected ? this.props.options : [],
135132
allSelected: !this.state.allSelected,
136133
},
137134
() => {
138-
// Call onChange prop with new values
139-
this.props.onChange(this.state.selectedValue);
135+
if (!this.state.allSelected) {
136+
this.props.onChange([]);
137+
} else {
138+
this.props.onChange(this.props.options);
139+
}
140140
}
141141
);
142142
}
143-
/**
144-
* Determine whether the user is treating this as a controlled component or not.
145-
*
146-
* @returns
147-
* @memberof Picky
148-
*/
149-
isControlled() {
150-
return !!this.props.value;
151-
}
152143

153144
isItemSelected(item) {
154-
const value = this.isControlled()
155-
? this.props.value
156-
: this.state.selectedValue;
157-
return hasItem(value, item, this.props.valueKey, this.props.labelKey);
145+
return hasItem(
146+
this.props.value,
147+
item,
148+
this.props.valueKey,
149+
this.props.labelKey
150+
);
158151
}
159152

160153
/**
@@ -176,7 +169,7 @@ class Picky extends React.PureComponent {
176169
if (renderList) {
177170
return renderList({
178171
items,
179-
selected: this.state.selectedValue,
172+
selected: this.props.value,
180173
multiple,
181174
tabIndex,
182175
getIsSelected: this.isItemSelected,
@@ -360,7 +353,7 @@ class Picky extends React.PureComponent {
360353
placeholder={placeholder}
361354
manySelectedPlaceholder={this.props.manySelectedPlaceholder}
362355
allSelectedPlaceholder={this.props.allSelectedPlaceholder}
363-
value={this.isControlled() ? value : this.state.selectedValue}
356+
value={value}
364357
multiple={multiple}
365358
numberDisplayed={numberDisplayed}
366359
valueKey={valueKey}

tests/Picky.test.js

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ const selSelected = id => `[data-testid="${id}"][data-selected="selected"]`;
1313
describe('Picky', () => {
1414
afterEach(cleanup); // <-- add this
1515

16-
it('should select initial values on load', () => {
17-
const wrapper = mount(<Picky value={[1, 2, 3]} multiple />);
18-
expect(wrapper.state('selectedValue')).toEqual([1, 2, 3]);
19-
});
20-
2116
it('should have Placeholder component', () => {
2217
const wrapper = mount(<Picky value={[]} />);
2318
expect(wrapper.find(Placeholder)).toHaveLength(1);
@@ -328,11 +323,12 @@ describe('Picky', () => {
328323

329324
it('should select all options when select all is clicked', () => {
330325
const onChange = jest.fn();
326+
const options = [1, 2, 3, 4, 5];
331327
const wrapper = mount(
332328
<Picky
333329
value={[1, 2, 3]}
334330
includeSelectAll={true}
335-
options={[1, 2, 3, 4, 5]}
331+
options={options}
336332
open={true}
337333
multiple={true}
338334
onChange={onChange}
@@ -342,11 +338,9 @@ describe('Picky', () => {
342338

343339
expect(wrapper.find(selSelected('option'))).toHaveLength(3);
344340
selectAllItem.simulate('click');
345-
expect(wrapper.state('selectedValue')).toHaveLength(5);
341+
expect(onChange).toHaveBeenLastCalledWith(options);
346342
selectAllItem.simulate('click');
347-
expect(wrapper.state('selectedValue')).toHaveLength(0);
348-
expect(onChange).toHaveBeenCalled();
349-
expect(onChange).toHaveBeenCalledWith([1, 2, 3, 4, 5]);
343+
expect(onChange).toHaveBeenLastCalledWith([]);
350344
});
351345

352346
it('should select single value controlled', () => {
@@ -364,7 +358,7 @@ describe('Picky', () => {
364358
.find(sel('option'))
365359
.at(1)
366360
.simulate('click');
367-
expect(onChange).lastCalledWith(2);
361+
expect(onChange).toHaveBeenLastCalledWith(2);
368362
});
369363

370364
it('should select single value uncontrolled', () => {
@@ -377,7 +371,7 @@ describe('Picky', () => {
377371
.find(sel('option'))
378372
.at(1)
379373
.simulate('click');
380-
expect(onChange).lastCalledWith(2);
374+
expect(onChange).toHaveBeenLastCalledWith(2);
381375
});
382376
it('should select multiple value controlled', () => {
383377
const onChange = jest.fn();
@@ -391,32 +385,11 @@ describe('Picky', () => {
391385
/>
392386
);
393387

394-
expect(wrapper.state('selectedValue')).toEqual([]);
395388
wrapper
396389
.find(sel('option'))
397390
.at(1)
398391
.simulate('click');
399-
expect(onChange).lastCalledWith([2]);
400-
expect(wrapper.state('selectedValue')).toEqual([2]);
401-
});
402-
it('should select multiple value uncontrolled', () => {
403-
const onChange = jest.fn();
404-
const wrapper = mount(
405-
<Picky
406-
options={[1, 2, 3, 4, 5]}
407-
open={true}
408-
multiple
409-
onChange={onChange}
410-
/>
411-
);
412-
413-
expect(wrapper.state('selectedValue')).toEqual([]);
414-
wrapper
415-
.find(sel('option'))
416-
.at(1)
417-
.simulate('click');
418-
expect(onChange).lastCalledWith([2]);
419-
expect(wrapper.state('selectedValue')).toEqual([2]);
392+
expect(onChange).toHaveBeenLastCalledWith([2]);
420393
});
421394

422395
it('should deselect multiple value', () => {
@@ -431,13 +404,11 @@ describe('Picky', () => {
431404
/>
432405
);
433406

434-
expect(wrapper.state('selectedValue')).toEqual([2]);
435407
wrapper
436408
.find(sel('option'))
437409
.at(1)
438410
.simulate('click');
439-
expect(onChange).lastCalledWith([]);
440-
expect(wrapper.state('selectedValue')).toEqual([]);
411+
expect(onChange).toHaveBeenLastCalledWith([]);
441412
});
442413

443414
it('should support object options single select', () => {
@@ -446,13 +417,15 @@ describe('Picky', () => {
446417
{ id: 2, name: 'Item 2' },
447418
{ id: 3, name: 'Item 3' },
448419
];
420+
const mockOnChange = jest.fn();
449421
const wrapper = mount(
450422
<Picky
451423
value={null}
452424
options={options}
453425
open={true}
454426
valueKey="id"
455427
labelKey="name"
428+
onChange={mockOnChange}
456429
/>
457430
);
458431

@@ -461,7 +434,7 @@ describe('Picky', () => {
461434
.at(0)
462435
.simulate('click');
463436

464-
expect(wrapper.state('selectedValue')).toEqual({
437+
expect(mockOnChange).toHaveBeenLastCalledWith({
465438
id: 1,
466439
name: 'Item 1',
467440
});
@@ -713,7 +686,16 @@ describe('Picky', () => {
713686

714687
describe('Issue #38', () => {
715688
test('should set selectedValue state when async value prop updates', done => {
716-
const wrapper = mount(<Picky multiple open value={[]} options={[]} />);
689+
const mockOnChange = jest.fn();
690+
const wrapper = mount(
691+
<Picky
692+
multiple
693+
open
694+
value={[]}
695+
options={[]}
696+
onChange={mockOnChange}
697+
/>
698+
);
717699
const componentWillUpdateSpy = jest.spyOn(
718700
Picky.prototype,
719701
'UNSAFE_componentWillReceiveProps'
@@ -725,7 +707,6 @@ describe('Picky', () => {
725707
options: ['1', '2', '3', '4', '5'],
726708
});
727709
expect(componentWillUpdateSpy).toHaveBeenCalled();
728-
expect(wrapper.state('selectedValue')).toHaveLength(2);
729710
expect(wrapper.state('allSelected')).toEqual(false);
730711
componentWillUpdateSpy.mockReset();
731712
componentWillUpdateSpy.mockRestore();

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,11 @@ babel-plugin-transform-property-literals@^6.9.4:
14101410
dependencies:
14111411
esutils "^2.0.2"
14121412

1413+
babel-plugin-transform-react-remove-prop-types@^0.4.19:
1414+
version "0.4.19"
1415+
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.19.tgz#dc9d8fb176a407a75efe73f231550450e29a3b17"
1416+
integrity sha512-f49NsaohQ1ByY20nUrpc30QFdbeT4ntV4PAL2vSZe6uCB5nqAcqXS/qzU+aI6ZfYhWASx5eIsTFvFrs1B2ffGg==
1417+
14131418
babel-plugin-transform-regexp-constructors@^0.4.3:
14141419
version "0.4.3"
14151420
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.4.3.tgz#58b7775b63afcf33328fae9a5f88fbd4fb0b4965"

0 commit comments

Comments
 (0)