x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<div class="flex flex-col gap-4"> <div> <h3 class="text-primary-md-medium mb-2">With Model (no error)</h3> <div class="lui-money lui-money--inline relative" data-controller="money-input"> <div data-controller="input" data-input-open-actions-value="false" class="lui-inner-input relative flex gap-2" data-input-original-input-value="199.99" data-input-mode-value="inline" data-input-form-value=""> <div class="w-full flex flex-col"> <span class="lui-input "> <span class="lui-input__addon-left"> <span class="lui-money__currency text-gray-700"> € </span> </span> <input name="product[price]" type="number" value="199.99" placeholder="0.00" class="lui-input__input" mode="inline" contentEditable="true" data-input-target="input" data-action="input->input#onChange change->input#onChange" data-money-input-target="input" min="0" step="0.01" inputmode="decimal" pattern="[0-9]*[.,]?[0-9]*"> <span class="lui-input__spinner"> <i class="fa-regular fa-spinner"></i> </span> </span> </div> <span class="lui-inner-input__actions opacity-0 flex items-center gap-1 h-fit" data-input-target="actions"> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="cancel" data-action="click->input#handleClose" type="button" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-xmark" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-outlined" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="submit" data-action="click->input#setLoading" type="submit" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-check" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-outlined" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button> </span> </div> </div> </div> <div> <h3 class="text-primary-md-medium mb-2">With Model (with error)</h3> <div class="lui-money lui-money--inline relative" data-controller="money-input"> <div data-controller="input" data-input-open-actions-value="false" class="lui-inner-input relative flex gap-2" data-input-original-input-value="" data-input-mode-value="inline" data-input-form-value=""> <div class="w-full flex flex-col"> <span class="lui-input lui-input--with-error"> <span class="lui-input__addon-left"> <span class="lui-money__currency text-gray-700"> $ </span> </span> <input name="product[price]" type="number" value="" placeholder="0.00" class="lui-input__input" mode="inline" contentEditable="true" data-input-target="input" data-action="input->input#onChange change->input#onChange" data-money-input-target="input" min="0" step="0.01" inputmode="decimal" pattern="[0-9]*[.,]?[0-9]*"> <span class="lui-input__spinner"> <i class="fa-regular fa-spinner"></i> </span> </span> <span class="lui-input__error">must be greater than 0</span> </div> <span class="lui-inner-input__actions opacity-0 flex items-center gap-1 h-fit" data-input-target="actions"> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="cancel" data-action="click->input#handleClose" type="button" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-xmark" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-outlined" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-input-target="submit" data-action="click->input#setLoading" type="submit" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-check" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-outlined" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button> </span> </div> </div> </div></div>Inputs::Money
Description
Related components
| Used Components | Components where is Used |
|---|---|
| Label |
Usage rules
- ✅ Do
- ❌ Don't
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<% # Example with a mock model product = Struct.new(:model_name, :price, :errors).new( Struct.new(:param_key).new("product"), "199.99", ActiveModel::Errors.new(self) )%><div class="flex flex-col gap-4"> <div> <h3 class="text-primary-md-medium mb-2">With Model (no error)</h3> <%= render LooposUi::Inputs::Money.new( model: product, attribute: :price, currency: "EUR", placeholder: "0.00" ) %> </div> <% # Mock model with error product_with_error = Struct.new(:model_name, :price, :errors).new( Struct.new(:param_key).new("product"), "", ActiveModel::Errors.new(self) ) product_with_error.errors.add(:price, "must be greater than 0") %> <div> <h3 class="text-primary-md-medium mb-2">With Model (with error)</h3> <%= render LooposUi::Inputs::Money.new( model: product_with_error, attribute: :price, currency: "USD", placeholder: "0.00" ) %> </div></div>No notes provided.
No params configured.
Description
The Money input component is a specialized number input for monetary values. It displays a currency symbol on the left side of the input and only accepts numeric values (with decimals).
Arguments
| Property | Type | Default | Description |
|---|---|---|---|
currency |
String | "EUR" |
The currency code (EUR, USD, GBP, JPY, CHF). Displays the appropriate symbol (€, $, £, ¥, CHF) |
min |
Number | 0 |
Minimum allowed value |
max |
Number | nil |
Maximum allowed value |
step |
Number | 0.01 |
Step increment for the input |
model |
Object | nil |
ActiveRecord model instance (alternative to manual name/value) |
attribute |
Symbol | nil |
Model attribute name (used with model) |
name |
String | nil |
Input name attribute (required if not using model) |
value |
String | nil |
Initial value |
placeholder |
String | nil |
Placeholder text |
error |
String | nil |
Error message to display |
help |
String | nil |
Help text to display |
mode |
Symbol | :inline |
Input mode (:inline, :form, :autosubmit) |
readonly |
Boolean | false |
Whether the input is read-only |
Usage
Basic Usage
<%= render LooposUi::Inputs::Money.new( name: "price", value: "99.99", currency: "EUR") %>With Different Currencies
<!-- Euro --><%= render LooposUi::Inputs::Money.new( name: "price_eur", value: "99.99", currency: "EUR") %><!-- US Dollar --><%= render LooposUi::Inputs::Money.new( name: "price_usd", value: "99.99", currency: "USD") %><!-- British Pound --><%= render LooposUi::Inputs::Money.new( name: "price_gbp", value: "99.99", currency: "GBP") %>With Model
<%= render LooposUi::Inputs::Money.new( model: @product, attribute: :price, currency: "EUR") %>With Validation
<%= render LooposUi::Inputs::Money.new( name: "price", value: "99.99", currency: "EUR", min: 0, max: 9999.99, error: "Price must be between 0 and 9999.99") %>In Form Mode
<%= render LooposUi::Inputs::Money.new( name: "price", value: "99.99", currency: "USD", mode: :form, placeholder: "0.00") %>Supported Currencies
The component supports the following currencies with their symbols:
- EUR (Euro): €
- USD (US Dollar): $
- GBP (British Pound): £
- JPY (Japanese Yen): ¥
- CHF (Swiss Franc): CHF
For other currencies, the component will display the currency code itself.
Features
- ✅ Currency symbol displayed on the left
- ✅ Only accepts numeric input (with decimals)
- ✅ Real-time input validation - invalid characters are prevented from being typed
- ✅ No spinner arrows on the input (cleaner UI)
- ✅ Supports model binding with automatic error display
- ✅ Supports min/max validation
- ✅ Supports different input modes (inline, form, autosubmit)
- ✅ Smart paste handling - automatically cleans pasted content
- ✅ Responsive and accessible
Input Validation
The Money component uses a JavaScript Stimulus controller (money-input) that prevents invalid characters from being entered in real-time. This provides a better user experience compared to browser validation that only checks after input.
How It Works
The controller uses three event listeners:
keydownevent - Prevents invalid keys from being pressedbeforeinputevent - Validates characters before they're inserted (second line of defense)pasteevent - Validates and cleans pasted content
Allowed Input
✅ Allowed:
- Digits:
0-9 - Decimal separator:
.or,(only one allowed) - Control keys: Backspace, Delete, Arrow keys, Tab, Home, End
- System shortcuts: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X, Ctrl+Z (and Cmd on Mac)
❌ Blocked:
- Letters:
a-z,A-Z - Special characters:
!@#$%^&*()etc. - Currency symbols:
€$£¥(displayed separately as addon) - Multiple decimal separators
- Any other non-numeric characters
Paste Behavior
When pasting content, the controller:
- Extracts only valid characters (digits and one decimal separator)
- Removes all invalid characters automatically
- Inserts the cleaned text at cursor position
Example:
- User pastes:
"abc€123.45xyz" - Controller inserts:
"123.45"
Notes
- The input uses
type="number"withinputmode="decimal"for better mobile keyboard support - The default step is
0.01(2 decimal places), suitable for most currencies - For currencies like JPY that don't use decimal places, you can set
step: 1 - The component inherits all the features from the base Input component (error handling, help text, etc.)
- Invalid keys simply don't work - they won't be entered into the input field