react-hook-form-bound currency field. Renders the @uxf/ui/text-input primitive with type="number", a currency symbol in the rightElement, and stores a Money object, managing the input's value, isInvalid, and helperText from form state.
Use @uxf/form/money-input for a monetary amount inside a @uxf/form form when the form value must be a Money object ({ amount, currency }), not a bare number. It renders the amount, shows the currency symbol, and tracks the currency. There is no same-named @uxf/ui twin; for a plain numeric field use @uxf/form/number-input.
import { Form } from "@uxf/form/form";
import { MoneyInput, MoneyInputValue } from "@uxf/form/money-input";
import { useForm } from "react-hook-form";
interface FormData {
price: MoneyInputValue;
}
function Example() {
const formApi = useForm<FormData>({ defaultValues: { price: { amount: "100", currency: "USD" } } });
return (
<Form formApi={formApi} id="example" onSubmit={(values) => console.log(values)}>
<MoneyInput control={formApi.control} label="Price" max={200} min={100} name="price" />
</Form>
);
}
Value is Money | null (MoneyInputValue), where Money is { amount: string; currency: Currency } from @uxf/core/money. Empty input becomes null; amount is stored as a string, currency defaults to defaultCurrency (or "CZK") until the value sets one.
While the user types, amount is exactly what they typed — so "1000.00" and "1000" are two different values describing the same money. When the field is left, the amount is snapped to its canonical form via normalizeMoney: "1000.00" becomes "1000", "015" becomes "15", ".5" becomes "0.5". An amount that is not a decimal number becomes null.
This is deliberately on blur rather than on change — on change it would rewrite the text mid-typing and swallow the decimals ("15.5", then typing 0, would be normalized straight back to "15.5").
Without it a Money field reads as dirty forever once the amount is retyped in another notation, because react-hook-form compares the value object leaf by leaf. The component can only canonicalize what it emits — the defaultValues are built by your own mappers, so run them through normalizeMoney too, or the two sides still will not line up:
const formApi = useForm<FormData>({ defaultValues: { price: normalizeMoney(data.price) } });
When the amount is rewritten on blur, the new value is reported through onChange as well (with no event argument), so a consumer keeping its own copy of the value outside react-hook-form stays in sync.
MoneyInputProps<FormData> = ControlProps<FormData> + Omit<TextInputProps, …> + the props below — so the field forwards the @uxf/ui/text-input prop surface apart from the props it manages itself.
Omitted, because this field owns them: inputMode, isFocused, isInvalid, maxLength, minLength, name, onKeyDown, pattern, rightElement, step, type, value — plus min / max / onChange, which are re-declared below with money-specific types.
Beyond the props listed below, everything else on TextInputProps is accepted and forwarded unchanged: autoComplete, autoFocus, className, enterKeyHint, form, hiddenLabel, onBeforeInput, onPaste, placeholder, size, style, variant.
| Prop | Type | Default | Description |
|---|---|---|---|
control | Control<FormData> | — | Required. The control from useForm. |
name | FieldPath<FormData> | — | Required. Field path in the form values. |
rules | RegisterOptions | — | Extra react-hook-form rules; merged with the built-in validation below. |
shouldUnregister | boolean | — | Unregister the field (drop its value) on unmount. |
defaultCurrency | Currency | "CZK" | Currency used until the field value carries one; drives the symbol in rightElement. |
min / max | number | — | Bounds checked against value.amount by the built-in validMinNumber / validMaxNumber validators. |
minMessage / maxMessage | (value: MoneyInputValue) => string | localized | Override the min / max validation messages (called with the current value). |
isRequired | boolean | false | Adds a required rule and the required indicator. |
requiredMessage | string | localized | Message for the required rule. |
label | string | — | Field label, forwarded to the twin. |
helperText | ReactNode | — | Helper text shown when there is no field error. |
leftAddon / rightAddon / leftElement | ReactNode | — | Forwarded to the twin. rightElement is reserved for the currency symbol. |
id | string | ${formId}__${name} | Input id. |
onChange | (value: MoneyInputValue, event) => void | — | Called after the field value updates — on every keystroke, and again on blur if the amount was normalized (then without an event). |
onBlur | FocusEventHandler<HTMLInputElement> | — | Forwarded to the input; runs after the amount is normalized and the field is marked touched. |
onFocus | FocusEventHandler<HTMLInputElement> | — | Accepted, but currently not forwarded to the input, so it never fires. See Gotchas. |
isDisabled / isReadOnly | boolean | inherited | Also inherited from the Form. |
onFocus never fires. It is part of the accepted prop type (via TextInputProps), but the component does not pass it to the rendered input, so the handler is silently dropped. onBlur is unaffected. Use the twin's onFocus directly, or a wrapper, until the passthrough is restored.isRequired adds a required rule (requiredMessage to override; key uxf-form-money-input:validation.required).min / max compare against value.amount; messages (uxf-form-money-input:validation.min-value / max-value) interpolate {{min}}/{{max}} and are overridable via the minMessage / maxMessage functions.rules (a function validate is merged in under the custom key); the field-level error is shown as the twin's helperText and sets isInvalid.Form — it reads the form context (formId, and inherits isDisabled / isReadOnly) and needs a react-hook-form control.@uxf/ui/text-input (with the uxf-money-input / uxf-input--no-spin-buttons classes), so include that component's stylesheet(s) and the @uxf/ui token layer — see @uxf/ui/text-input.@uxf/ui/text-inputMoney / Currency in @uxf/core/money