/
NumberField.tsx
94 lines (82 loc) · 3.5 KB
/
NumberField.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import React, { useState, useCallback } from "react";
import {
asNumber,
FieldProps,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from "@rjsf/utils";
// Matches a string that ends in a . character, optionally followed by a sequence of
// digits followed by any number of 0 characters up until the end of the line.
// Ensuring that there is at least one prefixed character is important so that
// you don't incorrectly match against "0".
const trailingCharMatcherWithPrefix = /\.([0-9]*0)*$/;
// This is used for trimming the trailing 0 and . characters without affecting
// the rest of the string. Its possible to use one RegEx with groups for this
// functionality, but it is fairly complex compared to simply defining two
// different matchers.
const trailingCharMatcher = /[0.]0*$/;
/**
* The NumberField class has some special handling for dealing with trailing
* decimal points and/or zeroes. This logic is designed to allow trailing values
* to be visible in the input element, but not be represented in the
* corresponding form data.
*
* The algorithm is as follows:
*
* 1. When the input value changes the value is cached in the component state
*
* 2. The value is then normalized, removing trailing decimal points and zeros,
* then passed to the "onChange" callback
*
* 3. When the component is rendered, the formData value is checked against the
* value cached in the state. If it matches the cached value, the cached
* value is passed to the input instead of the formData value
*/
function NumberField<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: FieldProps<T, S, F>) {
const { registry, onChange, formData, value: initialValue } = props;
const [lastValue, setLastValue] = useState(initialValue);
const { StringField } = registry.fields;
let value = formData;
/** Handle the change from the `StringField` to properly convert to a number
*
* @param value - The current value for the change occurring
*/
const handleChange = useCallback(
(value: FieldProps<T, S, F>["value"]) => {
// Cache the original value in component state
setLastValue(value);
// Normalize decimals that don't start with a zero character in advance so
// that the rest of the normalization logic is simpler
if (`${value}`.charAt(0) === ".") {
value = `0${value}`;
}
// Check that the value is a string (this can happen if the widget used is a
// <select>, due to an enum declaration etc) then, if the value ends in a
// trailing decimal point or multiple zeroes, strip the trailing values
const processed =
typeof value === "string" && value.match(trailingCharMatcherWithPrefix)
? asNumber(value.replace(trailingCharMatcher, ""))
: asNumber(value);
onChange(processed as unknown as T);
},
[onChange]
);
if (typeof lastValue === "string" && typeof value === "number") {
// Construct a regular expression that checks for a string that consists
// of the formData value suffixed with zero or one '.' characters and zero
// or more '0' characters
const re = new RegExp(`${value}`.replace(".", "\\.") + "\\.?0*$");
// If the cached "lastValue" is a match, use that instead of the formData
// value to prevent the input value from changing in the UI
if (lastValue.match(re)) {
value = lastValue as unknown as T;
}
}
return <StringField {...props} formData={value} onChange={handleChange} />;
}
export default NumberField;