Skip to content

Commit 7c2d3cd

Browse files
committed
feat(pf4): common select
1 parent 32e0ee2 commit 7c2d3cd

File tree

5 files changed

+256
-222
lines changed

5 files changed

+256
-222
lines changed

packages/common/src/select/index.js

Lines changed: 136 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import React, { Component } from 'react';
1+
/* eslint-disable react-hooks/exhaustive-deps */
2+
import React, { useEffect, useReducer } from 'react';
23
import ReactSelect from 'react-select';
4+
import CreatableSelect from 'react-select/creatable';
5+
36
import PropTypes from 'prop-types';
47
import clsx from 'clsx';
58
import isEqual from 'lodash/isEqual';
69
import { input } from '../prop-types-templates';
10+
import fnToString from '../utils/fn-to-string';
11+
import reducer from './reducer';
712

813
const getSelectValue = (stateValue, simpleValue, isMulti, allOptions) =>
914
simpleValue ? allOptions.filter(({ value }) => (isMulti ? stateValue.includes(value) : isEqual(value, stateValue))) : stateValue;
@@ -15,29 +20,132 @@ const handleSelectChange = (option, simpleValue, isMulti, onChange) => {
1520
: onChange(sanitizedOption);
1621
};
1722

18-
class Select extends Component {
19-
render() {
20-
const { input, invalid, classNamePrefix, simpleValue, isMulti, pluckSingleValue, options, ...props } = this.props;
21-
const { value, onChange, ...inputProps } = input;
22-
23-
const selectValue = pluckSingleValue ? (isMulti ? value : Array.isArray(value) && value[0] ? value[0] : value) : value;
24-
25-
return (
26-
<ReactSelect
27-
className={clsx(classNamePrefix, {
28-
'has-error': invalid
29-
})}
30-
{...props}
31-
{...inputProps}
32-
options={options}
33-
classNamePrefix={classNamePrefix}
34-
isMulti={isMulti}
35-
value={getSelectValue(selectValue, simpleValue, isMulti, options)}
36-
onChange={(option) => handleSelectChange(option, simpleValue, isMulti, onChange)}
37-
/>
38-
);
23+
const selectProvider = (type) =>
24+
({
25+
createable: CreatableSelect
26+
}[type] || ReactSelect);
27+
28+
const Select = ({
29+
invalid,
30+
classNamePrefix,
31+
simpleValue,
32+
isMulti,
33+
pluckSingleValue,
34+
options: propsOptions,
35+
loadOptions,
36+
loadingMessage,
37+
loadingProps,
38+
selectVariant,
39+
updatingMessage,
40+
noOptionsMessage,
41+
value,
42+
onChange,
43+
...props
44+
}) => {
45+
const [state, dispatch] = useReducer(reducer, {
46+
isLoading: false,
47+
options: propsOptions,
48+
promises: {},
49+
isMounted: false
50+
});
51+
52+
const updateOptions = () => {
53+
dispatch({ type: 'startLoading' });
54+
55+
return loadOptions().then((data) => {
56+
if (Array.isArray(value)) {
57+
const selectValue = value.filter((value) =>
58+
typeof value === 'object' ? data.find((option) => value.value === option.value) : data.find((option) => value === option.value)
59+
);
60+
onChange(selectValue.length === 0 ? undefined : selectValue);
61+
} else if (!data.find(({ value: internalValue }) => internalValue === value)) {
62+
onChange(undefined);
63+
}
64+
65+
dispatch({ type: 'updateOptions', payload: data });
66+
});
67+
};
68+
69+
useEffect(() => {
70+
if (loadOptions) {
71+
updateOptions();
72+
}
73+
74+
dispatch({ type: 'mounted' });
75+
76+
return () => {
77+
dispatch({ type: 'unmounted' });
78+
};
79+
}, []);
80+
81+
const loadOptionsStr = loadOptions ? fnToString(loadOptions) : '';
82+
83+
useEffect(() => {
84+
if (loadOptionsStr && state.isMounted) {
85+
updateOptions();
86+
}
87+
}, [loadOptionsStr]);
88+
89+
useEffect(() => {
90+
if (state.isMounted) {
91+
if (!propsOptions.map(({ value }) => value).includes(value)) {
92+
onChange(undefined);
93+
}
94+
95+
dispatch({ type: 'setOptions', payload: propsOptions });
96+
}
97+
}, [propsOptions]);
98+
99+
if (state.isLoading) {
100+
return <ReactSelect isDisabled={true} placeholder={loadingMessage} options={state.options} {...loadingProps} />;
39101
}
40-
}
102+
103+
const onInputChange = (inputValue) => {
104+
if (loadOptions && state.promises[inputValue] === undefined) {
105+
dispatch({ type: 'setPromises', payload: { [inputValue]: true } });
106+
107+
loadOptions(inputValue)
108+
.then((options) => {
109+
if (state.isMounted) {
110+
dispatch({
111+
type: 'setPromises',
112+
payload: { [inputValue]: false },
113+
options
114+
});
115+
}
116+
})
117+
.catch((error) => {
118+
dispatch({ type: 'setPromises', payload: { [inputValue]: false } });
119+
throw error;
120+
});
121+
}
122+
};
123+
124+
const renderNoOptionsMessage = () => (Object.values(state.promises).some((value) => value) ? () => updatingMessage : () => noOptionsMessage);
125+
126+
const selectValue = pluckSingleValue ? (isMulti ? value : Array.isArray(value) && value[0] ? value[0] : value) : value;
127+
128+
const SelectFinal = selectProvider(selectVariant);
129+
130+
return (
131+
<SelectFinal
132+
className={clsx(classNamePrefix, {
133+
'has-error': invalid
134+
})}
135+
{...props}
136+
options={state.options}
137+
classNamePrefix={classNamePrefix}
138+
isMulti={isMulti}
139+
value={getSelectValue(selectValue, simpleValue, isMulti, state.options)}
140+
onChange={(option) => handleSelectChange(option, simpleValue, isMulti, onChange)}
141+
onInputChange={onInputChange}
142+
isFetching={Object.values(state.promises).some((value) => value)}
143+
noOptionsMessage={renderNoOptionsMessage()}
144+
hideSelectedOptions={false}
145+
closeMenuOnSelect={!isMulti}
146+
/>
147+
);
148+
};
41149

42150
Select.propTypes = {
43151
options: PropTypes.array,
@@ -47,33 +155,19 @@ Select.propTypes = {
47155
simpleValue: PropTypes.bool,
48156
isMulti: PropTypes.bool,
49157
pluckSingleValue: PropTypes.bool,
50-
input
158+
value: PropTypes.any,
159+
placeholder: PropTypes.string,
160+
...input
51161
};
52162

53163
Select.defaultProps = {
54164
options: [],
55165
invalid: false,
56166
simpleValue: true,
57-
pluckSingleValue: true
58-
};
59-
60-
const DataDrivenSelect = ({ isMulti, ...props }) => {
61-
const closeMenuOnSelect = !isMulti;
62-
return <Select hideSelectedOptions={false} isMulti={isMulti} {...props} closeMenuOnSelect={closeMenuOnSelect} />;
63-
};
64-
65-
DataDrivenSelect.propTypes = {
66-
value: PropTypes.any,
67-
onChange: PropTypes.func,
68-
isMulti: PropTypes.bool,
69-
placeholder: PropTypes.string,
70-
classNamePrefix: PropTypes.string.isRequired
71-
};
72-
73-
DataDrivenSelect.defaultProps = {
167+
pluckSingleValue: true,
74168
placeholder: 'Choose...',
75169
isSearchable: false,
76170
isClearable: false
77171
};
78172

79-
export default DataDrivenSelect;
173+
export default Select;

packages/common/src/select/reducer.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const reducer = (state, { type, payload, options = [] }) => {
2+
switch (type) {
3+
case 'updateOptions':
4+
return {
5+
...state,
6+
options: payload,
7+
isLoading: false,
8+
promises: {}
9+
};
10+
case 'loaded':
11+
return {
12+
...state,
13+
isLoading: false
14+
};
15+
case 'startLoading':
16+
return {
17+
...state,
18+
isLoading: true
19+
};
20+
case 'setOptions':
21+
return {
22+
...state,
23+
options: payload
24+
};
25+
case 'mounted':
26+
return {
27+
...state,
28+
isMounted: true
29+
};
30+
case 'unmounted':
31+
return {
32+
...state,
33+
isMounted: false
34+
};
35+
case 'setPromises':
36+
return {
37+
...state,
38+
promises: {
39+
...state.promises,
40+
...payload
41+
},
42+
options: [...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))]
43+
};
44+
default:
45+
return state;
46+
}
47+
};
48+
49+
export default reducer;

0 commit comments

Comments
 (0)