Skip to content

Commit 999d4c0

Browse files
authored
Merge pull request #397 from rvsia/pf4selectMulti
[V2] Share functionality in common select
2 parents 32e0ee2 + c9be80f commit 999d4c0

File tree

12 files changed

+1438
-1578
lines changed

12 files changed

+1438
-1578
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useRef, useEffect } from 'react';
2+
3+
const useIsMounted = () => {
4+
const isMounted = useRef(false);
5+
useEffect(() => {
6+
isMounted.current = true;
7+
return () => (isMounted.current = false);
8+
}, []);
9+
return isMounted;
10+
};
11+
12+
export default useIsMounted;

packages/common/src/select/index.js

Lines changed: 141 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
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';
12+
import useIsMounted from '../hooks/use-is-mounted';
713

814
const getSelectValue = (stateValue, simpleValue, isMulti, allOptions) =>
915
simpleValue ? allOptions.filter(({ value }) => (isMulti ? stateValue.includes(value) : isEqual(value, stateValue))) : stateValue;
@@ -15,65 +21,165 @@ const handleSelectChange = (option, simpleValue, isMulti, onChange) => {
1521
: onChange(sanitizedOption);
1622
};
1723

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;
24+
const selectProvider = {
25+
createable: CreatableSelect
26+
};
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+
loadOptionsChangeCounter,
44+
...props
45+
}) => {
46+
const [state, dispatch] = useReducer(reducer, {
47+
isLoading: false,
48+
options: propsOptions,
49+
promises: {},
50+
isInitialLoaded: false
51+
});
52+
53+
const isMounted = useIsMounted();
54+
55+
const updateOptions = () => {
56+
dispatch({ type: 'startLoading' });
57+
58+
return loadOptions().then((data) => {
59+
if (isMounted) {
60+
if (value && Array.isArray(value)) {
61+
const selectValue = value.filter((value) =>
62+
typeof value === 'object' ? data.find((option) => value.value === option.value) : data.find((option) => value === option.value)
63+
);
64+
onChange(selectValue.length === 0 ? undefined : selectValue);
65+
} else if (value && !data.find(({ value: internalValue }) => internalValue === value)) {
66+
onChange(undefined);
67+
}
68+
69+
dispatch({ type: 'updateOptions', payload: data });
70+
}
71+
});
72+
};
73+
74+
useEffect(() => {
75+
if (loadOptions) {
76+
updateOptions();
77+
}
2278

23-
const selectValue = pluckSingleValue ? (isMulti ? value : Array.isArray(value) && value[0] ? value[0] : value) : value;
79+
dispatch({ type: 'initialLoaded' });
80+
}, []);
2481

82+
const loadOptionsStr = loadOptions ? fnToString(loadOptions) : '';
83+
84+
useEffect(() => {
85+
if (loadOptionsStr && state.isInitialLoaded) {
86+
updateOptions();
87+
}
88+
}, [loadOptionsStr, loadOptionsChangeCounter]);
89+
90+
useEffect(() => {
91+
if (state.isInitialLoaded) {
92+
if (value && !propsOptions.map(({ value }) => value).includes(value)) {
93+
onChange(undefined);
94+
}
95+
96+
dispatch({ type: 'setOptions', payload: propsOptions });
97+
}
98+
}, [propsOptions]);
99+
100+
if (state.isLoading) {
25101
return (
26102
<ReactSelect
27-
className={clsx(classNamePrefix, {
28-
'has-error': invalid
29-
})}
30103
{...props}
31-
{...inputProps}
32-
options={options}
33104
classNamePrefix={classNamePrefix}
34-
isMulti={isMulti}
35-
value={getSelectValue(selectValue, simpleValue, isMulti, options)}
36-
onChange={(option) => handleSelectChange(option, simpleValue, isMulti, onChange)}
105+
isDisabled={true}
106+
placeholder={loadingMessage}
107+
options={state.options}
108+
{...loadingProps}
37109
/>
38110
);
39111
}
40-
}
112+
113+
const onInputChange = (inputValue) => {
114+
if (inputValue && loadOptions && state.promises[inputValue] === undefined && props.isSearchable) {
115+
dispatch({ type: 'setPromises', payload: { [inputValue]: true } });
116+
117+
loadOptions(inputValue)
118+
.then((options) => {
119+
if (isMounted) {
120+
dispatch({
121+
type: 'setPromises',
122+
payload: { [inputValue]: false },
123+
options
124+
});
125+
}
126+
})
127+
.catch((error) => {
128+
dispatch({ type: 'setPromises', payload: { [inputValue]: false } });
129+
throw error;
130+
});
131+
}
132+
};
133+
134+
const renderNoOptionsMessage = () => (Object.values(state.promises).some((value) => value) ? () => updatingMessage : () => noOptionsMessage);
135+
136+
const selectValue = pluckSingleValue ? (isMulti ? value : Array.isArray(value) && value[0] ? value[0] : value) : value;
137+
138+
const SelectFinal = selectProvider[selectVariant] || ReactSelect;
139+
140+
return (
141+
<SelectFinal
142+
className={clsx(classNamePrefix, {
143+
'has-error': invalid
144+
})}
145+
{...props}
146+
isDisabled={props.isDisabled || props.isReadOnly}
147+
options={state.options}
148+
classNamePrefix={classNamePrefix}
149+
isMulti={isMulti}
150+
value={getSelectValue(selectValue, simpleValue, isMulti, state.options)}
151+
onChange={(option) => handleSelectChange(option, simpleValue, isMulti, onChange)}
152+
onInputChange={onInputChange}
153+
isFetching={Object.values(state.promises).some((value) => value)}
154+
noOptionsMessage={renderNoOptionsMessage()}
155+
hideSelectedOptions={false}
156+
closeMenuOnSelect={!isMulti}
157+
/>
158+
);
159+
};
41160

42161
Select.propTypes = {
43162
options: PropTypes.array,
44163
onChange: PropTypes.func,
45-
classNamePrefix: PropTypes.string.isRequired,
164+
classNamePrefix: PropTypes.string,
46165
invalid: PropTypes.bool,
47166
simpleValue: PropTypes.bool,
48167
isMulti: PropTypes.bool,
49168
pluckSingleValue: PropTypes.bool,
50-
input
169+
value: PropTypes.any,
170+
placeholder: PropTypes.string,
171+
loadOptionsChangeCounter: PropTypes.number,
172+
...input
51173
};
52174

53175
Select.defaultProps = {
54176
options: [],
55177
invalid: false,
56178
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 = {
179+
pluckSingleValue: true,
74180
placeholder: 'Choose...',
75181
isSearchable: false,
76182
isClearable: false
77183
};
78184

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

packages/common/src/select/reducer.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 'initialLoaded':
26+
return {
27+
...state,
28+
isInitialLoaded: true
29+
};
30+
case 'setPromises':
31+
return {
32+
...state,
33+
promises: {
34+
...state.promises,
35+
...payload
36+
},
37+
options: [...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))]
38+
};
39+
default:
40+
return state;
41+
}
42+
};
43+
44+
export default reducer;

packages/mui-component-mapper/src/files/select.js

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,27 @@ import { meta, input } from '@data-driven-forms/common/src/prop-types-templates'
77
import MUISelect from './select/integration-select';
88
import { useFieldApi } from '@data-driven-forms/react-form-renderer';
99

10-
const selectValue = (option) => option.sort((a, b) => a.label.localeCompare(b.label, 'en', { sensitivity: 'base' })).map((item) => item.value);
11-
1210
const Select = (props) => {
13-
const {
14-
input,
15-
isReadOnly,
16-
isDisabled,
17-
placeholder,
18-
isRequired,
19-
label,
20-
helperText,
21-
validateOnMount,
22-
meta,
23-
options,
24-
isSearchable,
25-
description,
26-
...rest
27-
} = useFieldApi(props);
11+
const { input, placeholder, label, helperText, validateOnMount, meta, isSearchable, description, ...rest } = useFieldApi(props);
2812
const invalid = validationError(meta, validateOnMount);
2913

3014
return (
3115
<FormFieldGrid>
3216
<MUISelect
3317
fullWidth
3418
{...input}
35-
options={options.filter((option) => Object.prototype.hasOwnProperty.call(option, 'value') && option.value !== null)}
36-
placeholder={placeholder || 'Please choose'}
37-
value={options.filter(({ value }) => (rest.isMulti ? input.value.includes(value) : value === input.value))}
38-
isMulti={rest.isMulti}
3919
isSearchable={!!isSearchable}
4020
isClearable={false}
41-
hideSelectedOptions={false}
42-
closeMenuOnSelect={!rest.isMulti}
43-
noOptionsMessage={() => 'No option found'}
4421
invalid={invalid}
45-
isDisabled={isDisabled}
4622
textFieldProps={{
4723
label,
4824
color: invalid ? 'red' : 'blue',
4925
InputLabelProps: {
5026
shrink: true
5127
}
5228
}}
53-
onChange={(option) => input.onChange(rest.isMulti ? selectValue(option) : option ? option.value : undefined)}
5429
input={input}
5530
label={label}
56-
isRequired={isRequired}
5731
helperText={invalid || helperText || description}
5832
{...rest}
5933
/>
@@ -64,10 +38,7 @@ const Select = (props) => {
6438
Select.propTypes = {
6539
input,
6640
meta,
67-
isReadOnly: PropTypes.bool,
68-
isDisabled: PropTypes.bool,
6941
placeholder: PropTypes.node,
70-
isRequired: PropTypes.bool,
7142
label: PropTypes.node,
7243
helperText: PropTypes.node,
7344
validateOnMount: PropTypes.bool,
@@ -76,4 +47,9 @@ Select.propTypes = {
7647
description: PropTypes.node
7748
};
7849

50+
Select.defaultProps = {
51+
placeholder: 'Please choose',
52+
noOptionsMessage: 'No option found'
53+
};
54+
7955
export default Select;

packages/mui-component-mapper/src/files/select/integration-select.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import clsx from 'clsx';
4-
import Select from 'react-select';
4+
import Select from '@data-driven-forms/common/src/select';
55
import { emphasize, makeStyles, useTheme } from '@material-ui/core/styles';
66
import { Typography, NoSsr, TextField, Paper, Chip, MenuItem, FormControl, FormHelperText, FormLabel } from '@material-ui/core';
77
import CancelIcon from '@material-ui/icons/Cancel';

0 commit comments

Comments
 (0)