Masked Inputs

Amounts, phone numbers and codes that are written in their proper format while the person types.

Stored value: 1250.5

import * as React from "react"

import { Field, FieldControl, FieldHelperText, FieldLabel } from "@hel/ui/field"

When to use it

Use a masked input when a value has a fixed shape: a price, a phone number, a tax ID, a bank account. The text is grouped and formatted as it is typed, so people see the right shape at once and you get a clean value back.

Masked inputs are not a component you drop in. They are hooks. Each hook returns the props for a MaskedInput, and you put that input inside a Field like any other control. The hooks read the app's config, so locale, digits and currency follow the rest of the interface.

HookForValue you get
useAmountInputNumbers, money and percentsAn exact decimal string such as "1250.50"
useExpressionInputAn amount that also accepts a calculationAn exact decimal string
usePhoneInputPhone numbers with a country pickerThe number in E.164, such as "+9647701234567"
useMaskInputCodes in a fixed layoutThe typed characters without the fixed ones
useIbanInputBank account numbersThe IBAN without spaces

The value is always a string, never a JavaScript number, so a long amount keeps every digit.

Import

import {
  MaskedInput,
  useAmountInput,
  useExpressionInput,
  usePhoneInput,
  useMaskInput,
  useIbanInput,
} from "@hel/ui/masks"

Usage

Call the hook, spread props on MaskedInput, and read the value from onValueChange.

const amount = useAmountInput({ currency: "USD", onValueChange: setTotal })
 
<Field>
  <FieldLabel>Invoice total</FieldLabel>
  <MaskedInput {...amount.props} />
</Field>

Pass defaultValue to start with a value, or value to control it. A value that changes from outside, such as a form reset or a loaded record, replaces what was typed.

Examples

Amounts

An amount can be a plain number, money or a percent. Giving a currency makes it money. format="percent" shows a fraction as a percent, so "0.15" appears as 15%. currencySign="accounting" writes a negative amount in brackets once the field is left. For an input that sits inside a group, with a unit in front of it, wrap the input in a FieldGroup.

USD
import { Field, FieldGroup, FieldGroupAddon, FieldLabel } from "@hel/ui/field"
import { MaskedInput, useAmountInput } from "@hel/ui/masks"
import { Stack } from "@hel/ui/stack"

minimumFractionDigits, maximumFractionDigits, useGrouping and allowNegative tune the number. locale and numberingSystem write it in another script, such as Arabic digits.

Calculations

Sometimes people would rather type 12*3+4 than reach for a calculator. useExpressionInput accepts a number or a calculation. Enter, or leaving the field, works it out and shows the result as a normal amount. Escape goes back to the amount from before.

While the text is still a calculation, preview holds the result so you can show it below the field. If the text cannot be worked out, the typed text stays, and invalid and error are set.

import * as React from "react"

import {

The hook returns props for a FieldControl, not a MaskedInput, because the field has to hold text that is not a number yet.

Phone numbers

usePhoneInput formats the number for the chosen country and reports it as E.164. Pair it with a Select built from phone.countries, and put both in a FieldGroup. After a country is picked, focus moves to the number. A number pasted with its + picks its own country.

import * as React from "react"

import {

countries limits and orders the choices. defaultCountry is the country a number is read in when it has no +.

Codes and IBANs

useMaskInput takes a pattern. # is a digit, A is a letter, * is a letter or a digit, and any other character is fixed text. A value is good once every slot is filled. For a check digit or a list of known prefixes, pass validate, which then decides alone.

If the same layout is used in many places, give it a name in the config under inputs.masks and pass name instead of pattern.

useIbanInput is the same idea for bank accounts. It groups the characters in fours, writes them in capitals, and checks the country's length and the check digits when the field is left.

import * as React from "react"

import { Field, FieldHelperText, FieldLabel } from "@hel/ui/field"

Errors

Phone, code and IBAN hooks return invalid and error. They turn on after the field has been left with a value that is not good, so people are not told off in the middle of typing. Pass invalid to the Field to get the error ring, and show error in a FieldHelperText or FieldError. The invalidMessage option replaces the built-in message with your own.

Options

Amount settings

These apply to both useAmountInput and useExpressionInput.

OptionTypeDefault
value / defaultValuestringnone
onValueChange(value: string, details: { formattedValue: string }) => voidnone
format"decimal" | "currency" | "percent""decimal", or "currency" with a currency
currencystring (ISO 4217)The config's money.currency
currencyDisplayIntl.NumberFormatOptions["currencyDisplay"]none
currencySign"standard" | "accounting""standard"
minimumFractionDigits / maximumFractionDigitsnumberFrom the locale
useGroupingIntl.NumberFormatOptions["useGrouping"]locale default
localestringThe config's number locale
numberingSystemstringThe config's numbering system
allowNegativebooleantrue

useExpressionInput also takes invalidMessage, and returns invalid, error and preview.

Code and phone settings

OptionHookType
pattern / nameuseMaskInputstring
validateuseMaskInput(value: string) => boolean
invalidMessageuseMaskInput, usePhoneInput, useIbanInputstring
countriesusePhoneInputCountryCode[]
defaultCountryusePhoneInputCountryCode
localeallstring

useMaskInput calls onValueChange with the value and { formattedValue, complete }. usePhoneInput calls it with { formattedValue, country, valid }.