1- import React , { useEffect , useState } from 'react' ;
1+ import React , { forwardRef , useEffect , useRef , useState } from 'react' ;
22import PropTypes from 'prop-types' ;
3- import { omit } from 'ramda' ;
3+ import { isNil , omit } from 'ramda' ;
44import isNumeric from 'fast-isnumeric' ;
55import classNames from 'classnames' ;
66
77const convert = val => ( isNumeric ( val ) ? + val : NaN ) ;
8+ const isEquivalent = ( v1 , v2 ) => v1 === v2 || ( isNaN ( v1 ) && isNaN ( v2 ) ) ;
89
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 ) => {
2011 const {
21- value,
22- className,
2312 debounce,
2413 n_blur,
2514 n_submit,
26- valid,
27- invalid,
28- bs_size,
29- plaintext,
3015 loading_state,
3116 setProps,
17+ onEvent,
18+ onChange,
19+ valid,
20+ invalid,
3221 ...otherProps
3322 } = props ;
3423
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-
6524 const onBlur = ( ) => {
6625 if ( setProps ) {
6726 const payload = {
6827 n_blur : n_blur + 1 ,
6928 n_blur_timestamp : Date . now ( )
7029 } ;
7130 if ( debounce ) {
72- payload . value = parseValue ( valueState ) ;
31+ onEvent ( payload ) ;
32+ } else {
33+ setProps ( payload ) ;
7334 }
74- setProps ( payload ) ;
7535 }
7636 } ;
7737
@@ -82,48 +42,154 @@ const Input = props => {
8242 n_submit_timestamp : Date . now ( )
8343 } ;
8444 if ( debounce ) {
85- payload . value = parseValue ( valueState ) ;
45+ onEvent ( payload ) ;
46+ } else {
47+ setProps ( payload ) ;
8648 }
87- setProps ( payload ) ;
8849 }
8950 } ;
9051
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- ) ;
10252 return (
10353 < input
54+ ref = { ref }
10455 onChange = { onChange }
10556 onBlur = { onBlur }
10657 onKeyPress = { onKeyPress }
107- className = { classes }
108- value = { valueState }
10958 { ...omit (
11059 [
11160 'n_blur_timestamp' ,
11261 'n_submit_timestamp' ,
113- 'selectionDirection' ,
114- 'selectionEnd' ,
115- 'selectionStart' ,
11662 'persistence' ,
11763 'persistence_type' ,
11864 'persisted_props'
11965 ] ,
12066 otherProps
12167 ) }
68+ valid = { valid ? 'true' : undefined }
69+ invalid = { invalid ? 'true' : undefined }
12270 data-dash-is-loading = {
12371 ( loading_state && loading_state . is_loading ) || undefined
12472 }
12573 />
12674 ) ;
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 } /> ;
127193} ;
128194
129195Input . propTypes = {
@@ -480,7 +546,8 @@ Input.defaultProps = {
480546 n_submit_timestamp : - 1 ,
481547 debounce : false ,
482548 persisted_props : [ 'value' ] ,
483- persistence_type : 'local'
549+ persistence_type : 'local' ,
550+ step : 'any'
484551} ;
485552
486553export default Input ;
0 commit comments