# Money.Input
Locale-aware money form input — `<.money_input>` and
`<.currency_picker>` Phoenix HEEx components, an
AutoNumeric-backed JS hook, an Ecto changeset bridge, and a
Plug-based visualizer for local development.
For a plain *number* input (no currency), see the sibling
[`localize_inputs`](https://hex.pm/packages/localize_inputs)
package — `<.number_input>` lives there.
For a full end-to-end Phoenix integration walkthrough — Elixir
deps, JS deps, asset wiring, schema, LiveView — read
[`guides/integration.md`](https://github.com/ex-money/money_input/blob/main/guides/integration.md).
## Installation
```elixir
def deps do
[
{:ex_money_input, "~> 0.1.0"},
# Components and changeset bridge:
{:phoenix_html, "~> 4.0"},
{:phoenix_live_view, "~> 1.0"},
{:ecto, "~> 3.10"},
# Visualizer (dev only):
{:plug, "~> 1.15", only: :dev},
{:bandit, "~> 1.5", only: :dev}
]
end
```
Every Phoenix/Ecto/Plug/Bandit dep is optional — the headless
layer compiles without any of them, and each higher layer
activates when its dep is present.
## Layered API
### 1. Headless (no Phoenix dependency)
Three focused modules. Parsing and formatting use `Money` and
`Localize.Number` directly — there are no wrappers here.
```elixir
# Cast — turn a form-submission *map*, a string, or a Money into a Money.t
{:ok, %Money{}} = Money.Input.Cast.cast(
%{"amount" => "1.234,56", "currency" => "EUR"},
locale: :de
)
{:ok, %Money{}} = Money.Input.Cast.cast("$1,234.56", locale: :en)
# Validator — apply *business rules* (bounds, precision, required, currency match)
:ok = Money.Input.Validator.validate_money(Money.new(:USD, "1.50"), max: Money.new(:USD, 9999))
{:error, [{:decimals, _}]} = Money.Input.Validator.validate_money(Money.new(:JPY, "1.5"))
# Currency — locale display data (separators, symbol position, currency precision)
{:ok, info} = Money.Input.Currency.currency_for_locale(:de, currency: :EUR)
info.decimal #=> ","
info.symbol #=> "€"
info.symbol_position #=> :suffix # derived from the CLDR currency format pattern
info.iso_digits #=> 2
info.number_system #=> :latn
```
**Parsing a user-typed money string is `Money.parse/2`**, which
already handles surrounding whitespace, accounting parens, and
currency symbols/ISO codes natively:
```elixir
%Money{} = Money.parse("$1,234.56")
%Money{} = Money.parse("(1.234,56)", locale: :de, default_currency: :EUR)
```
**Money formatting is `Money.to_string/2`** — pass
`currency_symbol: :none` for the amount alone (the shape a
component would render into the input field, with the symbol
positioned as a separate adornment):
```elixir
Money.to_string!(Money.new(:EUR, "1234.56"), locale: :de)
#=> "1.234,56 €"
Money.to_string!(Money.new(:EUR, "1234.56"), locale: :de, currency_symbol: :none)
#=> "1.234,56"
```
`Money.Input.Cast` vs `Money.Input.Validator`: **shape vs.
business rules**. Cast answers "can I parse this into a Money?".
Validator answers "is this Money acceptable under my app's
rules?".
### 2. Ecto Changeset
```elixir
def changeset(product, attrs) do
product
|> Ecto.Changeset.cast(attrs, [:price])
|> Money.Input.Changeset.validate_money(:price,
min: Money.new(:USD, "0.01"),
max: Money.new(:USD, 9999))
end
```
When the field isn't typed as `Money.Ecto.Composite.Type` (which
casts the map shape automatically), use
`Money.Input.Changeset.cast_money/3` first.
### 3. HEEx components
```heex
<%!-- Single fixed currency --%>
<.money_input form={@form} field={:price} default_currency={:USD} />
<%!-- Currency-selectable with the bundled picker --%>
<.money_input
form={@form}
field={:price}
default_currency={:USD}
currency_picker={true}
preferred_currencies={[:USD, :EUR, :GBP, :JPY]}
/>
<%!-- Standalone picker (e.g. "show prices in" widget) --%>
<.currency_picker
current={@viewing_currency}
form={@form}
field={:viewing_currency}
preferred={[:USD, :EUR, :GBP]}
/>
```
Import them via `import Money.Input.Components` in your view or
`use` block.
The `<.money_input>` field always submits **two nested keys**,
whether the picker is on or not:
```
params["product"]["price"] = %{"amount" => "1234.56", "currency" => "USD"}
```
That shape is exactly what `Money.Ecto.Composite.Type.cast/1` and
`Money.Input.Changeset.cast_money/3` accept directly.
### 4. JS hook (AutoNumeric)
Add the peer dep:
```bash
npm install autonumeric
```
In `assets/js/app.js`:
```javascript
import AutoNumeric from "autonumeric"
import Hooks from "money_input"
Hooks.configure({ AutoNumeric })
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...Hooks }
})
```
And in your CSS:
```css
@import "money_input/priv/static/money_input.css";
```
Without AutoNumeric loaded the inputs still work (Path A
fallback) — only live formatting and cursor preservation are
absent.
## Visualizer
```elixir
# In your dev config:
config :ex_money_input, visualizer: true
# Standalone:
{:ok, _pid} = Money.Input.Visualizer.Standalone.start(port: 4002)
# Visit http://localhost:4002
# Or mount into Phoenix:
forward "/money-input", Money.Input.Visualizer
```
Views:
* `/input` — live HEEx renders of the actual components. Picks
locale + currency, embeds the picker, mounts AutoNumeric from
jsdelivr so the live behaviour is observable.
* `/parse` — one input × every locale (separator inversion,
paste tolerance).
* `/format` — one parsed value × every locale.
* `/locale` — `Money.Input.Currency.currency_for_locale/2` snapshot per locale.
The standalone helper refuses to start unless the config flag is
set or `enabled: true` is passed explicitly, so a developer tool
can't deploy to production by accident.
## Out of scope (deliberate)
* Wise's bidirectional FX flow with live conversion — compose two
`<.money_input>` components and wire your own rate provider.
* Scientific notation input — banking apps universally reject it
(AutoNumeric does too).
* Keyboard increment/decrement — opt-in via AutoNumeric options
if you need it.
## Architecture map
```
Money.parse / Money.to_string / Money.new
│
▼
┌───────────────────────────────────────────┐
│ Money.Input.Cast ─ inputs → Money │
│ Money.Input.Validator ─ business rules │
│ Money.Input.Currency ─ display data │
└───────────────────────────────────────────┘
│
┌───────────────┴──────────────┐
▼ ▼
Money.Input.Changeset Money.Input.Components
(Ecto bridge) ─ money_input
─ currency_picker
│
priv/static/money_input.{js,css}
(LiveView hooks, AutoNumeric)
```
## License
Apache-2.0.