1
- import React , { Component } from 'react' ;
1
+ /* eslint-disable react-hooks/exhaustive-deps */
2
+ import React , { useEffect , useReducer } from 'react' ;
2
3
import ReactSelect from 'react-select' ;
4
+ import CreatableSelect from 'react-select/creatable' ;
5
+
3
6
import PropTypes from 'prop-types' ;
4
7
import clsx from 'clsx' ;
5
8
import isEqual from 'lodash/isEqual' ;
6
9
import { input } from '../prop-types-templates' ;
10
+ import fnToString from '../utils/fn-to-string' ;
11
+ import reducer from './reducer' ;
7
12
8
13
const getSelectValue = ( stateValue , simpleValue , isMulti , allOptions ) =>
9
14
simpleValue ? allOptions . filter ( ( { value } ) => ( isMulti ? stateValue . includes ( value ) : isEqual ( value , stateValue ) ) ) : stateValue ;
@@ -15,29 +20,132 @@ const handleSelectChange = (option, simpleValue, isMulti, onChange) => {
15
20
: onChange ( sanitizedOption ) ;
16
21
} ;
17
22
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 } /> ;
39
101
}
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
+ } ;
41
149
42
150
Select . propTypes = {
43
151
options : PropTypes . array ,
@@ -47,33 +155,19 @@ Select.propTypes = {
47
155
simpleValue : PropTypes . bool ,
48
156
isMulti : PropTypes . bool ,
49
157
pluckSingleValue : PropTypes . bool ,
50
- input
158
+ value : PropTypes . any ,
159
+ placeholder : PropTypes . string ,
160
+ ...input
51
161
} ;
52
162
53
163
Select . defaultProps = {
54
164
options : [ ] ,
55
165
invalid : false ,
56
166
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 ,
74
168
placeholder : 'Choose...' ,
75
169
isSearchable : false ,
76
170
isClearable : false
77
171
} ;
78
172
79
- export default DataDrivenSelect ;
173
+ export default Select ;
0 commit comments