1
- import React , { useEffect , useState } from 'react' ;
1
+ import React , { forwardRef , useEffect , useRef , useState } from 'react' ;
2
2
import PropTypes from 'prop-types' ;
3
- import { omit } from 'ramda' ;
3
+ import { isNil , omit } from 'ramda' ;
4
4
import isNumeric from 'fast-isnumeric' ;
5
5
import classNames from 'classnames' ;
6
6
7
7
const convert = val => ( isNumeric ( val ) ? + val : NaN ) ;
8
+ const isEquivalent = ( v1 , v2 ) => v1 === v2 || ( isNaN ( v1 ) && isNaN ( v2 ) ) ;
8
9
9
- /**
10
- * A basic HTML input control for entering text, numbers, or passwords, with
11
- * Bootstrap styles automatically applied. This component is much like its
12
- * counterpart in dash_core_components, but with a few additions such as the
13
- * `valid` and `invalid` props for providing user feedback.
14
- *
15
- * Note that checkbox and radio types are supported through
16
- * the Checklist and RadioItems component. Dates, times, and file uploads
17
- * are supported through separate components in other libraries.
18
- */
19
- const Input = props => {
10
+ const BaseInput = forwardRef ( ( props , ref ) => {
20
11
const {
21
- value,
22
- className,
23
12
debounce,
24
13
n_blur,
25
14
n_submit,
26
- valid,
27
- invalid,
28
- bs_size,
29
- plaintext,
30
15
loading_state,
31
16
setProps,
17
+ onEvent,
18
+ onChange,
19
+ valid,
20
+ invalid,
32
21
...otherProps
33
22
} = props ;
34
23
35
- const [ valueState , setValueState ] = useState ( value || '' ) ;
36
-
37
- useEffect ( ( ) => {
38
- // "" == 0 in JavaScript, which means we need to check separately if a
39
- // cleared input is being set to 0
40
- if ( value != valueState || ( valueState === '' && value === 0 ) ) {
41
- if ( value !== null && value !== undefined ) {
42
- setValueState ( value ) ;
43
- } else {
44
- setValueState ( '' ) ;
45
- }
46
- }
47
- } , [ value ] ) ;
48
-
49
- const parseValue = value => {
50
- if ( props . type === 'number' ) {
51
- const convertedValue = convert ( value ) ;
52
- if ( isNaN ( convertedValue ) ) {
53
- return null ;
54
- } else return convertedValue ;
55
- } else return value ;
56
- } ;
57
-
58
- const onChange = e => {
59
- setValueState ( e . target . value ) ;
60
- if ( ! debounce && setProps ) {
61
- setProps ( { value : parseValue ( e . target . value ) } ) ;
62
- }
63
- } ;
64
-
65
24
const onBlur = ( ) => {
66
25
if ( setProps ) {
67
26
const payload = {
68
27
n_blur : n_blur + 1 ,
69
28
n_blur_timestamp : Date . now ( )
70
29
} ;
71
30
if ( debounce ) {
72
- payload . value = parseValue ( valueState ) ;
31
+ onEvent ( payload ) ;
32
+ } else {
33
+ setProps ( payload ) ;
73
34
}
74
- setProps ( payload ) ;
75
35
}
76
36
} ;
77
37
@@ -82,48 +42,154 @@ const Input = props => {
82
42
n_submit_timestamp : Date . now ( )
83
43
} ;
84
44
if ( debounce ) {
85
- payload . value = parseValue ( valueState ) ;
45
+ onEvent ( payload ) ;
46
+ } else {
47
+ setProps ( payload ) ;
86
48
}
87
- setProps ( payload ) ;
88
49
}
89
50
} ;
90
51
91
- const formControlClass = plaintext
92
- ? 'form-control-plaintext'
93
- : 'form-control' ;
94
-
95
- const classes = classNames (
96
- className ,
97
- invalid && 'is-invalid' ,
98
- valid && 'is-valid' ,
99
- bs_size ? `form-control-${ bs_size } ` : false ,
100
- formControlClass
101
- ) ;
102
52
return (
103
53
< input
54
+ ref = { ref }
104
55
onChange = { onChange }
105
56
onBlur = { onBlur }
106
57
onKeyPress = { onKeyPress }
107
- className = { classes }
108
- value = { valueState }
109
58
{ ...omit (
110
59
[
111
60
'n_blur_timestamp' ,
112
61
'n_submit_timestamp' ,
113
- 'selectionDirection' ,
114
- 'selectionEnd' ,
115
- 'selectionStart' ,
116
62
'persistence' ,
117
63
'persistence_type' ,
118
64
'persisted_props'
119
65
] ,
120
66
otherProps
121
67
) }
68
+ valid = { valid ? 'true' : undefined }
69
+ invalid = { invalid ? 'true' : undefined }
122
70
data-dash-is-loading = {
123
71
( loading_state && loading_state . is_loading ) || undefined
124
72
}
125
73
/>
126
74
) ;
75
+ } ) ;
76
+
77
+ const NumberInput = forwardRef ( ( props , inputRef ) => {
78
+ const { setProps, debounce, value, ...otherProps } = props ;
79
+
80
+ const onChange = ( ) => {
81
+ if ( ! debounce ) {
82
+ onEvent ( ) ;
83
+ }
84
+ } ;
85
+
86
+ useEffect ( ( ) => {
87
+ const inputValue = inputRef . current . value ;
88
+ const inputValueAsNumber = inputRef . current . checkValidity ( )
89
+ ? convert ( inputValue )
90
+ : NaN ;
91
+ const valueAsNumber = convert ( value ) ;
92
+
93
+ if ( ! isEquivalent ( valueAsNumber , inputValueAsNumber ) ) {
94
+ inputRef . current . value = isNil ( valueAsNumber ) ? valueAsNumber : value ;
95
+ }
96
+ } , [ value ] ) ;
97
+
98
+ const onEvent = ( payload = { } ) => {
99
+ const inputValue = inputRef . current . value ;
100
+ const inputValueAsNumber = inputRef . current . checkValidity ( )
101
+ ? convert ( inputValue )
102
+ : NaN ;
103
+ const valueAsNumber = convert ( value ) ;
104
+
105
+ if ( ! isEquivalent ( valueAsNumber , inputValueAsNumber ) ) {
106
+ setProps ( { ...payload , value : inputValueAsNumber } ) ;
107
+ } else if ( Object . keys ( payload ) . length ) {
108
+ setProps ( payload ) ;
109
+ }
110
+ } ;
111
+
112
+ return (
113
+ < BaseInput
114
+ ref = { inputRef }
115
+ debounce = { debounce }
116
+ onEvent = { onEvent }
117
+ onChange = { onChange }
118
+ setProps = { setProps }
119
+ { ...otherProps }
120
+ />
121
+ ) ;
122
+ } ) ;
123
+
124
+ const NonNumberInput = forwardRef ( ( props , inputRef ) => {
125
+ const { value, debounce, setProps, ...otherProps } = props ;
126
+ const [ valueState , setValueState ] = useState ( value || '' ) ;
127
+
128
+ const onChange = ( ) => {
129
+ if ( ! debounce ) {
130
+ onEvent ( ) ;
131
+ } else {
132
+ setValueState ( inputRef . current . value ) ;
133
+ }
134
+ } ;
135
+
136
+ useEffect ( ( ) => {
137
+ if ( value !== null && value !== undefined ) {
138
+ setValueState ( value ) ;
139
+ } else {
140
+ setValueState ( '' ) ;
141
+ }
142
+ } , [ value ] ) ;
143
+
144
+ const onEvent = ( payload = { } ) => {
145
+ payload . value = inputRef . current . value ;
146
+ setProps ( payload ) ;
147
+ } ;
148
+
149
+ return (
150
+ < BaseInput
151
+ ref = { inputRef }
152
+ value = { valueState }
153
+ debounce = { debounce }
154
+ onEvent = { onEvent }
155
+ onChange = { onChange }
156
+ setProps = { setProps }
157
+ { ...otherProps }
158
+ />
159
+ ) ;
160
+ } ) ;
161
+
162
+ /**
163
+ * A basic HTML input control for entering text, numbers, or passwords, with
164
+ * Bootstrap styles automatically applied. This component is much like its
165
+ * counterpart in dash_core_components, but with a few additions such as the
166
+ * `valid` and `invalid` props for providing user feedback.
167
+ *
168
+ * Note that checkbox and radio types are supported through
169
+ * the Checklist and RadioItems component. Dates, times, and file uploads
170
+ * are supported through separate components in other libraries.
171
+ */
172
+ const Input = props => {
173
+ const { plaintext, className, bs_size, ...otherProps } = props ;
174
+ const inputRef = useRef ( null ) ;
175
+
176
+ const formControlClass = plaintext
177
+ ? 'form-control-plaintext'
178
+ : 'form-control' ;
179
+
180
+ const classes = classNames (
181
+ className ,
182
+ props . invalid && 'is-invalid' ,
183
+ props . valid && 'is-valid' ,
184
+ bs_size ? `form-control-${ bs_size } ` : false ,
185
+ formControlClass
186
+ ) ;
187
+
188
+ if ( props . type === 'number' ) {
189
+ return < NumberInput ref = { inputRef } { ...otherProps } className = { classes } /> ;
190
+ }
191
+
192
+ return < NonNumberInput ref = { inputRef } { ...otherProps } className = { classes } /> ;
127
193
} ;
128
194
129
195
Input . propTypes = {
@@ -480,7 +546,8 @@ Input.defaultProps = {
480
546
n_submit_timestamp : - 1 ,
481
547
debounce : false ,
482
548
persisted_props : [ 'value' ] ,
483
- persistence_type : 'local'
549
+ persistence_type : 'local' ,
550
+ step : 'any'
484
551
} ;
485
552
486
553
export default Input ;
0 commit comments