Hi guys, this is my first post here –and first blog ever. Any feedback is welcome!
A while ago I built a component library for React in TypeScript with styled-components. This library included an input element for large amounts of money, for example 500000
. For the sake of readability and convenience, formatting the input like 500.000
was a no-brainer.
Easy, right? Just use an <input type="text" />
, apply some formatting by adding JavaScript sprinkles or add a library that does it for you. But this causes a problem. The initial value for the input element comes from an API and is a number, but a text field would convert it into a string. Now we could just parse the string value to a number before passing the property to the parent component and do a request to the server. However, with this approach the component wouldn't work out-of-the-box, since you would need extra logic outside the component to make it work, –which would also neglect the principles of a design system. So how do we handle everything inside this currency component?
I didn't include any design whatsoever, but here you'll find an example.
Let’s start with the interface and definition of our CurrencyInput
.
interface Props {
value: number | undefined;
updateValue: (value: number | undefined) => void;
}
const CurrencyInput: React.FC<Props> = ({
value: valueFromProp,
updateValue,
}) => {
return (
<input
type="text"
value={currentValue}
onChange={handleChange}
/>
)
}
Notice that I renamed the value
property to valueFromProp
, since we need the name value
later on. The value of the input element is set to currentValue
and the onChange event triggers the handleChange
function.
Adding state with React hooks
Let's add state to the component by using the useState
hook.
const CurrencyInput: React.FC<Props> = ({
value: valueFromProp,
onChange,
}) => {
const [currentValue, updateCurrentValue] =12 React.useState<string>(
valueFromProp ? valueFromProp : ""
);
return (
<input
type="text"
value={currentValue}
onChange={handleChange}
/>
)
}
We have initiated the state with either valueFromProps
or an empty string, but we’re not there yet. The value is a number and the state property expects a string. To solve this, we format the number as string and add a thousands separator with a simple regex expression.
const format = (value: number) =>
value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
Updating the state and pass on data
Now we're about to implement the handleChange
, a function that updates the state and invokes the callback function updateValue
from the parent component.
const CurrencyInput: React.FC<Props> = ({
value: valueFromProp,
updateValue,
}) => {
const [currentValue, updateCurrentValue] = React.useState<string>(
valueFromProp ? format(valueFromProp) : ""
);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { target: { value }} = event;
}
return (
<input
type="text"
value={currentValue}
onChange={handleChange}
/>
)
}
By destructuring the event object we get the input value. First we need to check if it's empty in which case we pass on undefined
to our parent component. If the input isn't empty, we can update our state with a formatted string, and send the value to the parent component as a number. To do this we remove the applied formatting and parse it to a number.
const stringToNumber = (value: string): number => parseInt(value.replace(/\./g, ""), 10);
This is what it looks like when we bring it all together.
interface Props {
value: number | undefined;
updateValue: (value: number | undefined) => void;
}
const CurrencyInput: React.FC<Props> = ({
value: valueFromProp,
updateValue,
}) => {
const [currentValue, updateCurrentValue] = React.useState<string>(
valueFromProp ? format(valueFromProp) : ""
);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { value }
} = event;
if (value === "") {
updateCurrentValue("");
return updateValue(undefined);
}
const valueAsNumber = stringToNumber(value);
updateCurrentValue(format(valueAsNumber));
return updateValue(valueAsNumber);
};
return (
<input
type="text"
value={currentValue}
onChange={handleChange}
/>
)
}
Now we have a component with a text field that shows a formatted string, but handles it as a number.
Top comments (1)
This does not work ...