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' ;
12
+ import useIsMounted from '../hooks/use-is-mounted' ;
7
13
8
14
const getSelectValue = ( stateValue , simpleValue , isMulti , allOptions ) =>
9
15
simpleValue ? allOptions . filter ( ( { value } ) => ( isMulti ? stateValue . includes ( value ) : isEqual ( value , stateValue ) ) ) : stateValue ;
@@ -15,65 +21,165 @@ const handleSelectChange = (option, simpleValue, isMulti, onChange) => {
15
21
: onChange ( sanitizedOption ) ;
16
22
} ;
17
23
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
+ }
22
78
23
- const selectValue = pluckSingleValue ? ( isMulti ? value : Array . isArray ( value ) && value [ 0 ] ? value [ 0 ] : value ) : value ;
79
+ dispatch ( { type : 'initialLoaded' } ) ;
80
+ } , [ ] ) ;
24
81
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 ) {
25
101
return (
26
102
< ReactSelect
27
- className = { clsx ( classNamePrefix , {
28
- 'has-error' : invalid
29
- } ) }
30
103
{ ...props }
31
- { ...inputProps }
32
- options = { options }
33
104
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 }
37
109
/>
38
110
) ;
39
111
}
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
+ } ;
41
160
42
161
Select . propTypes = {
43
162
options : PropTypes . array ,
44
163
onChange : PropTypes . func ,
45
- classNamePrefix : PropTypes . string . isRequired ,
164
+ classNamePrefix : PropTypes . string ,
46
165
invalid : PropTypes . bool ,
47
166
simpleValue : PropTypes . bool ,
48
167
isMulti : PropTypes . bool ,
49
168
pluckSingleValue : PropTypes . bool ,
50
- input
169
+ value : PropTypes . any ,
170
+ placeholder : PropTypes . string ,
171
+ loadOptionsChangeCounter : PropTypes . number ,
172
+ ...input
51
173
} ;
52
174
53
175
Select . defaultProps = {
54
176
options : [ ] ,
55
177
invalid : false ,
56
178
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 ,
74
180
placeholder : 'Choose...' ,
75
181
isSearchable : false ,
76
182
isClearable : false
77
183
} ;
78
184
79
- export default DataDrivenSelect ;
185
+ export default Select ;
0 commit comments