Money Input
Currency + locale-aware money input — grouped per locale, precise via bignumber.js, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Money Input is a currency- and locale-aware amount field. The display is grouped for the
locale (e.g. en-IN → 1,00,000) with the right currency symbol and decimal count, while the
stored value stays a precise numeric string (bignumber.js — no float rounding). It's editable
as a plain number on focus and formatted on blur. In React it's <MoneyInput>; in Angular it's
<bpdm-money-input>.
Usage
Give it a currency, a locale, and a starting amount.
import { useState } from 'react';
import { MoneyInput } from '@bpdm/ui/money-input';
export function Amount() {
const [value, setValue] = useState('1234.5');
return <MoneyInput currency="USD" locale="en-US" value={value} onValueChange={setValue} />;
}import { Component, signal } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';
@Component({
selector: 'amount',
imports: [BpdmMoneyInput],
template: `<bpdm-money-input currency="USD" locale="en-US" [(value)]="value" />`,
})
export class Amount {
readonly value = signal('1234.5');
}Currencies & locales
The same stored amount renders differently per currency + locale — symbol, grouping, and
decimal count all follow the locale (note en-IN's 1,00,000 grouping and JPY's zero decimals).
import { MoneyInput } from '@bpdm/ui/money-input';
export function Currencies() {
return (
<div className="flex w-72 flex-col gap-3">
<MoneyInput currency="USD" locale="en-US" defaultValue="100000" /> {/* $100,000.00 */}
<MoneyInput currency="EUR" locale="de-DE" defaultValue="100000" /> {/* 100.000,00 € */}
<MoneyInput currency="INR" locale="en-IN" defaultValue="100000" /> {/* ₹1,00,000.00 */}
<MoneyInput currency="JPY" locale="ja-JP" defaultValue="100000" /> {/* ¥100,000 */}
</div>
);
}import { Component } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';
@Component({
selector: 'currencies',
imports: [BpdmMoneyInput],
template: `
<div class="flex w-72 flex-col gap-3">
<bpdm-money-input currency="USD" locale="en-US" defaultValue="100000" />
<bpdm-money-input currency="EUR" locale="de-DE" defaultValue="100000" />
<bpdm-money-input currency="INR" locale="en-IN" defaultValue="100000" />
<bpdm-money-input currency="JPY" locale="ja-JP" defaultValue="100000" />
</div>
`,
})
export class Currencies {}Precision
The stored value is a string folded through bignumber.js, so large amounts keep every digit — no
float rounding, no exponential notation.
import { MoneyInput } from '@bpdm/ui/money-input';
export function Precision() {
return <MoneyInput currency="USD" locale="en-US" defaultValue="123456789012.34" />;
}import { Component } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';
@Component({
selector: 'precision',
imports: [BpdmMoneyInput],
template: `<bpdm-money-input currency="USD" locale="en-US" defaultValue="123456789012.34" />`,
})
export class Precision {}Sizes
size sets the height and text scale — sm, md (default), or lg.
import { MoneyInput } from '@bpdm/ui/money-input';
export function Sizes() {
return (
<div className="flex w-64 flex-col gap-3">
<MoneyInput size="sm" currency="USD" defaultValue="2500" />
<MoneyInput size="md" currency="USD" defaultValue="2500" />
<MoneyInput size="lg" currency="USD" defaultValue="2500" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';
@Component({
selector: 'sizes',
imports: [BpdmMoneyInput],
template: `
<div class="flex w-64 flex-col gap-3">
<bpdm-money-input size="sm" currency="USD" defaultValue="2500" />
<bpdm-money-input size="md" currency="USD" defaultValue="2500" />
<bpdm-money-input size="lg" currency="USD" defaultValue="2500" />
</div>
`,
})
export class Sizes {}States
aria-invalid turns the border destructive-red; disabled dims the field and blocks input.
import { MoneyInput } from '@bpdm/ui/money-input';
export function States() {
return (
<div className="flex flex-col gap-3">
<MoneyInput aria-invalid currency="USD" defaultValue="0" />
<MoneyInput disabled currency="USD" defaultValue="2500" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';
@Component({
selector: 'states',
imports: [BpdmMoneyInput],
template: `
<div class="flex flex-col gap-3">
<bpdm-money-input aria-invalid="true" currency="USD" defaultValue="0" />
<bpdm-money-input disabled currency="USD" defaultValue="2500" />
</div>
`,
})
export class States {}API
MoneyInput / bpdm-money-input
The stored value is always a raw numeric string (e.g. "1234.50"); only the display is formatted.
| Prop | Type | Default | Description |
|---|---|---|---|
currency | string (ISO 4217) | "USD" | Currency code — sets the symbol and decimal count. |
locale | string (BCP 47) | "en-US" | Locale — sets grouping and symbol placement. |
value | string | — | Controlled value. React: pair with onValueChange. Angular: two-way [(value)]. |
defaultValue | string | "" | Uncontrolled initial value. |
onValueChange (React) | (value: string) => void | — | Called with the raw numeric string on every change. |
placeholder | string | — | Placeholder shown when empty. |
allowNegative | boolean | false | Permit negative amounts. |
size | sm | md | lg | md | Height + text scale. |
disabled | boolean | false | Disable the field. |
name | string | — | Native name for form submission (forwarded to the <input>). |
aria-invalid | boolean | false | Invalid state — destructive border, announced by assistive tech. |
aria-label / aria-describedby | string | — | Accessible name / description, forwarded to the <input>. |
id | string | — | Field id (for <label> association). |
className (React) / class (Angular) | string | — | Extra classes on the wrapper. |
In React, MoneyInput forwards its ref to the underlying <input> and spreads any other
native <input> prop (required, autoComplete, data-*, onFocus, onBlur, …) onto it, so it
drops into forms and test harnesses unchanged. In Angular, required, autoComplete, name,
aria-label, and aria-describedby are dedicated inputs on <bpdm-money-input>.
Accessibility
- The currency symbol is a decorative adornment (
aria-hidden,select-none, muted) — it is not announced. So give the field a name that carries the currency (e.g.aria-label="Amount in USD") via a<label>(htmlFor/id) oraria-label; the symbol alone won't tell a screen-reader user which currency they're entering. - The field uses
inputmode="decimal", so mobile shows a numeric keypad. - Invalid state is set with
aria-invalid— it colours the border and is announced (never colour-only). Pair it witharia-describedbypointing at your error text. - Labelling attributes reach the
<input>directly:aria-label,aria-describedby, andnameare forwarded in both frameworks (React spreads every native<input>prop; Angular exposes them as inputs).
Internationalization
- Locale-aware formatting is the core i18n feature:
currency+localedrive the grouping, symbol placement, and decimal count viaIntl.NumberFormat(en-IN→1,00,000,de-DE→100.000,00 €,JPY→ no decimals). The stored value stays a precise numeric string. - The currency symbol is decorative, so give the field a translated accessible name that
includes the currency. RTL-safe: alignment uses a logical property (
text-end), so it mirrors underdir="rtl". See the Internationalization guide.