@canutin/svelte-currency-input 0.13.0 → 1.0.0-next.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +231 -110
  2. package/dist/currency-input.svelte +361 -0
  3. package/dist/currency-input.svelte.d.ts +29 -0
  4. package/dist/index.d.ts +3 -2
  5. package/dist/index.js +4 -2
  6. package/dist/types.d.ts +36 -9
  7. package/dist/utils/addSeparators.d.ts +1 -0
  8. package/dist/utils/addSeparators.js +3 -0
  9. package/dist/utils/cleanValue.d.ts +2 -0
  10. package/dist/utils/cleanValue.js +43 -0
  11. package/dist/utils/escapeRegExp.d.ts +1 -0
  12. package/dist/utils/escapeRegExp.js +3 -0
  13. package/dist/utils/fixedDecimalValue.d.ts +1 -0
  14. package/dist/utils/fixedDecimalValue.js +25 -0
  15. package/dist/utils/formatValue.d.ts +2 -0
  16. package/dist/utils/formatValue.js +101 -0
  17. package/dist/utils/getLocaleConfig.d.ts +2 -0
  18. package/dist/utils/getLocaleConfig.js +33 -0
  19. package/dist/utils/getSuffix.d.ts +6 -0
  20. package/dist/utils/getSuffix.js +6 -0
  21. package/dist/utils/index.d.ts +13 -0
  22. package/dist/utils/index.js +13 -0
  23. package/dist/utils/isNumber.d.ts +1 -0
  24. package/dist/utils/isNumber.js +1 -0
  25. package/dist/utils/padTrimValue.d.ts +1 -0
  26. package/dist/utils/padTrimValue.js +26 -0
  27. package/dist/utils/parseAbbrValue.d.ts +2 -0
  28. package/dist/utils/parseAbbrValue.js +23 -0
  29. package/dist/utils/removeInvalidChars.d.ts +1 -0
  30. package/dist/utils/removeInvalidChars.js +6 -0
  31. package/dist/utils/removeSeparators.d.ts +1 -0
  32. package/dist/utils/removeSeparators.js +5 -0
  33. package/dist/utils/repositionCursor.d.ts +12 -0
  34. package/dist/utils/repositionCursor.js +18 -0
  35. package/package.json +77 -58
  36. package/dist/CurrencyInput.svelte +0 -181
  37. package/dist/CurrencyInput.svelte.d.ts +0 -30
  38. package/dist/constants.d.ts +0 -11
  39. package/dist/constants.js +0 -11
package/README.md CHANGED
@@ -1,172 +1,293 @@
1
1
  # svelte-currency-input
2
2
 
3
- A masked form input that converts numbers to localized currency formats as you type
3
+ A fully-featured currency input component for Svelte 5 that handles formatting, localization, and validation as you type.
4
4
 
5
- [<img width="962" alt="image" src="https://user-images.githubusercontent.com/1434675/190873948-c0385747-6fa9-4077-8bd5-717e4d1124a0.png">](https://svelte.dev/repl/d8f7d22e5b384555b430f62b157ac503?version=3.50.1)
5
+ ![svelte-currency-input examples](static/svelte-currency-input-examples.png)
6
6
 
7
7
  <p align="center">
8
- 👩‍💻 Play with it on <a href="https://svelte.dev/repl/d8f7d22e5b384555b430f62b157ac503?version=3.50.1" target="_blank">REPL</a> &nbsp;—&nbsp; 💵 See it in a <a href="https://github.com/Canutin/desktop/blob/master/sveltekit/src/lib/components/FormCurrency.svelte" target="_blank">real project</a>!
8
+ 👩‍💻 Play with it in the <a href="https://svelte-currency-input.fernando.is" target="_blank">live demo</a>
9
9
  </p>
10
10
 
11
11
  ---
12
12
 
13
+ - [Features](#features)
14
+ - [Installation](#installation)
15
+ - [Usage](#usage)
16
+ - [How it works](#how-it-works)
17
+ - [API](#api)
18
+ - [Examples](#examples)
19
+ - [Styling](#styling)
20
+ - [Exported utilities](#exported-utilities)
21
+ - [Svelte 4 / migration guide](#svelte-4)
22
+ - [Contributing](#contributing)
23
+
13
24
  ## Features
14
25
 
15
26
  - Formats **positive** and **negative** values
16
- - Leverages [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) for **localizing** currency denominations and masking the input
17
- - Simple [API](#api)
18
- - Minimal default styling, easy to [customize](#styling)
19
-
20
- ## Usage
27
+ - Leverages [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) for **localizing** currency denominations
28
+ - Supports **abbreviations** (k, m, b) for quick input
29
+ - Configurable **decimal precision** with multiple control options
30
+ - **Min/max constraints** with arrow key stepping
31
+ - Custom **prefix** and **suffix** support
32
+ - **Zero built-in styles** — works with Tailwind, vanilla CSS, or any framework
33
+ - Simple, single `<input>` element (no wrapper)
34
+ - Full **TypeScript** support
35
+ - **Lightweight** — ~3.6KB gzipped with no runtime dependencies
36
+ - API and logic heavily inspired by [@cchanxzy](https://github.com/cchanxzy)'s [react-currency-input-field](https://github.com/cchanxzy/react-currency-input-field)
37
+
38
+ ## Installation
21
39
 
22
40
  ```bash
23
- npm install @canutin/svelte-currency-input --save
41
+ # bun
42
+ bun add @canutin/svelte-currency-input
43
+
44
+ # pnpm
45
+ pnpm add @canutin/svelte-currency-input
46
+
47
+ # npm
48
+ npm install @canutin/svelte-currency-input
49
+
50
+ # yarn
51
+ yarn add @canutin/svelte-currency-input
24
52
  ```
25
53
 
26
- ```html
54
+ ## Usage
55
+
56
+ ```svelte
27
57
  <script lang="ts">
28
- import CurrencyInput from '@canutin/svelte-currency-input';
58
+ import { CurrencyInput } from '@canutin/svelte-currency-input';
59
+
60
+ let value = $state('1234.56');
29
61
  </script>
30
62
 
31
- <CurrencyInput name="total" value={-420.69} locale="nl-NL" currency="EUR" />
63
+ <CurrencyInput bind:value intlConfig={{ locale: 'en-US', currency: 'USD' }} />
32
64
  ```
33
65
 
66
+ The input displays `$1,234.56` while `value` contains the raw string `"1234.56"`.
67
+
34
68
  ## How it works
35
69
 
36
- When the form is submitted you get _unformatted_ or _formatted_ values from two `<input />`'s.
37
- This is more or less what `<CurrencyInput />` looks like under the hood:
38
-
39
- ```html
40
- <div class="currencyInput">
41
- <!-- Unformatted value -->
42
- <input
43
- class="currencyInput__unformatted"
44
- type="hidden"
45
- name="total"
46
- value="-420.69"
47
- />
48
-
49
- <!-- Formatted value -->
50
- <input
51
- class="currencyInput__formatted"
52
- type="text"
53
- name="formatted-total"
54
- value="€ -420,69"
55
- />
56
- </div>
57
- ```
70
+ The component renders a single `<input>` element. The `value` prop is a **string** representing the unformatted number:
58
71
 
59
- ## API
72
+ - `""` = empty input
73
+ - `"0"` = zero
74
+ - `"1234.56"` = the number 1234.56
60
75
 
61
- Option | Type | Default | Description |
62
- ----------------- | --------------- | ----------- | ----------- |
63
- value | `number` | `undefined` | Initial value. If left `undefined` a formatted value of `0` is visible as a placeholder |
64
- locale | `string` | `en-US` | Overrides default locale. [Examples](https://gist.github.com/ncreated/9934896) |
65
- currency | `string` | `USD` | Overrides default currency. [Examples](https://www.xe.com/symbols/) |
66
- name | `string` | `total` | Applies the name to the [input fields](#how-it-works) for _unformatted_ (e.g `[name=total]`) and _formatted_ (e.g. `[name=formatted-total]`) values |
67
- id | `string` | `undefined` | Sets the `id` attribute on the input |
68
- required | `boolean` | `false` | Marks the input as required |
69
- disabled | `boolean` | `false` | Marks the input as disabled |
70
- placeholder | `string` `number` `null` | `0` | A `string` will override the default placeholder. A `number` will override it by formatting it to the set currency. Setting it to `null` will not show a placeholder |
71
- isZeroNullish | `boolean` | `false` | If `true` and when the value is `0`, it will override the default placeholder and render the formatted value in the field like any other value. _Note: this option might become the default in future versions_ |
72
- autocomplete | `string` | `undefined` | Sets the autocomplete attribute. Accepts any valid HTML [autocomplete attribute values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values) |
73
- isNegativeAllowed | `boolean` | `true` | If `false`, forces formatting only to positive values and ignores `--positive` and `--negative` styling modifiers |
74
- fractionDigits | `number` | `2` | Sets `maximumFractionDigits` in [`Intl.NumberFormat()` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumfractiondigits) used for formatting the currency. Supported digits: `0` to `20` |
75
- inputClasses | `object` | [See below](#Styling) | Selectively overrides any class names passed |
76
- onValueChange | `Callback` | `undefined` | Runs a callback function after the value changes |
76
+ The formatted display (e.g., `$1,234.56`) is handled internally. For form submissions where you need the raw value, you can add a hidden input:
77
77
 
78
- ## Styling
78
+ ```svelte
79
+ <form>
80
+ <CurrencyInput bind:value name="display" />
81
+ <input type="hidden" name="amount" {value} />
82
+ </form>
83
+ ```
79
84
 
80
- There are two ways of customizing the styling of the input:
81
- 1. Passing it your own CSS classes
82
- 2. Overriding the styles using the existing class names
85
+ ## API
83
86
 
84
- You can **override all of the class names** by passing an object to `inputClasses` that has **one or more** of these properties:
87
+ ### Props
88
+
89
+ | Prop | Type | Default | Description |
90
+ | ------------------------ | --------------------------------------- | ----------- | ------------------------------------------------------------------------------- |
91
+ | `value` | `string` | `''` | Bindable raw value (e.g., `"1234.56"`) |
92
+ | `intlConfig` | `IntlConfig` | `undefined` | Locale and currency configuration |
93
+ | `prefix` | `string` | From locale | Override the currency prefix |
94
+ | `suffix` | `string` | `''` | Override the currency suffix |
95
+ | `decimalSeparator` | `string` | From locale | Override the decimal separator |
96
+ | `groupSeparator` | `string` | From locale | Override the grouping separator |
97
+ | `disableGroupSeparators` | `boolean` | `false` | Disable thousand separators |
98
+ | `allowDecimals` | `boolean` | `true` | Allow decimal values |
99
+ | `decimalsLimit` | `number` | `2` | Max decimal digits while typing |
100
+ | `decimalScale` | `number` | `undefined` | Pad/trim decimals on blur |
101
+ | `fixedDecimalLength` | `number` | `undefined` | Fixed decimal input (e.g., `2`: typing `123` → `1.23`) |
102
+ | `allowNegativeValue` | `boolean` | `true` | Allow negative values |
103
+ | `min` | `number` | `undefined` | Minimum value (enforced on arrow key step) |
104
+ | `max` | `number` | `undefined` | Maximum value (enforced on arrow key step) |
105
+ | `maxLength` | `number` | `undefined` | Max characters (excluding formatting) |
106
+ | `step` | `number` | `undefined` | Arrow key increment/decrement |
107
+ | `disableAbbreviations` | `boolean` | `false` | Disable k/m/b abbreviations |
108
+ | `formatValueOnBlur` | `boolean` | `true` | Apply formatting when input loses focus |
109
+ | `transformRawValue` | `(value: string) => string` | `undefined` | Transform the raw value before processing |
110
+ | `oninputvalue` | `(values: CurrencyInputValues) => void` | `undefined` | Callback on every input change |
111
+ | `onchangevalue` | `(values: CurrencyInputValues) => void` | `undefined` | Callback on blur/commit |
112
+ | `ref` | `HTMLInputElement \| null` | `null` | Bindable reference to the input element |
113
+ | `class` | `string` | `undefined` | CSS class(es) for the input |
114
+ | `...restProps` | `HTMLInputAttributes` | — | All standard input attributes (id, name, placeholder, disabled, required, etc.) |
115
+
116
+ ### Types
85
117
 
86
118
  ```typescript
87
- interface InputClasses {
88
- wrapper?: string; // <div> that contains the two <input> elements
89
- unformatted?: string; // <input type="hidden"> that contains the unformatted value
90
- formatted?: string; // <input type="text"> that contains the formatted value
91
- formattedPositive?: string; // Class added when the formatted input is positive
92
- formattedNegative?: string; // Class added when the formatted input is negative
93
- formattedZero?: string; // Class added when the formatted input is zero
119
+ interface IntlConfig {
120
+ locale: string;
121
+ currency?: string;
122
+ // Also accepts other Intl.NumberFormatOptions
94
123
  }
124
+
125
+ interface CurrencyInputValues {
126
+ float: number | null; // Parsed number or null if empty
127
+ formatted: string; // Display value: "$1,234.56"
128
+ value: string; // Raw value: "1234.56"
129
+ }
130
+ ```
131
+
132
+ ## Examples
133
+
134
+ ### International currencies
135
+
136
+ ```svelte
137
+ <CurrencyInput bind:value intlConfig={{ locale: 'de-DE', currency: 'EUR' }} />
138
+ <!-- Displays: 1.234,56 € -->
95
139
  ```
96
140
 
97
- Usage (with [Tailwind CSS](https://tailwindcss.com/) as an example):
141
+ ### Decimal precision
98
142
 
99
143
  ```svelte
100
- <CurrencyInput name="total" value="{420.69}" inputClasses={
101
- {
102
- wrapper: "form-control block",
103
- formatted: 'py-1.5 text-gray-700',
104
- formattedPositive: 'text-green-700',
105
- formattedNegative: 'text-red-700'
106
- }
107
- } />
144
+ <!-- Limit to 2 decimals while typing, pad to 2 on blur -->
145
+ <CurrencyInput
146
+ bind:value
147
+ intlConfig={{ locale: 'en-US', currency: 'USD' }}
148
+ decimalsLimit={2}
149
+ decimalScale={2}
150
+ />
108
151
  ```
109
152
 
110
- Alternatively you can **write your own CSS** by overriding the [default styles](https://github.com/canutin/svelte-currency-input/blob/main/src/lib/CurrencyInput.svelte) which use [BEM naming conventions](https://getbem.com/naming/). To do so apply your styles as shown below:
153
+ ### Min, max, and step
111
154
 
112
155
  ```svelte
113
- <div class="my-currency-input">
114
- <CurrencyInput name="total" value="{420.69}" />
115
- </div>
156
+ <!-- Use arrow keys to increment/decrement by 10, constrained to 0-100 -->
157
+ <CurrencyInput
158
+ bind:value
159
+ intlConfig={{ locale: 'en-US', currency: 'USD' }}
160
+ min={0}
161
+ max={100}
162
+ step={10}
163
+ />
164
+ ```
116
165
 
117
- <style>
118
- /* Container */
119
- div.my-currency-input :global(div.currencyInput) { /* ... */ }
166
+ ### Custom prefix and suffix
120
167
 
121
- /* Formatted input */
122
- div.my-currency-input :global(input.currencyInput__formatted) { /* ... */ }
168
+ ```svelte
169
+ <!-- Points system -->
170
+ <CurrencyInput bind:value suffix=" pts" decimalsLimit={0} />
123
171
 
124
- /* Formatted input when the it's disabled */
125
- div.my-currency-input :global(input.currencyInput__formatted:disabled) { /* ... */ }
172
+ <!-- Bitcoin -->
173
+ <CurrencyInput bind:value prefix="₿ " decimalsLimit={8} />
174
+ ```
126
175
 
127
- /* Formatted input when the value is zero */
128
- div.my-currency-input :global(input.currencyInput__formatted--zero) { /* ... */ }
176
+ ### Abbreviations
129
177
 
130
- /* Formatted input when the value is positive */
131
- div.my-currency-input :global(input.currencyInput__formatted--positive) { /* ... */ }
178
+ Type `1k`, `2.5m`, or `1b` to quickly enter large numbers:
132
179
 
133
- /* Formatted input when the value is negative */
134
- div.my-currency-input :global(input.currencyInput__formatted--negative) { /* ... */ }
135
- </style>
180
+ ```svelte
181
+ <CurrencyInput
182
+ bind:value
183
+ intlConfig={{ locale: 'en-US', currency: 'USD' }}
184
+ placeholder="Try 1k, 2.5m, or 1b"
185
+ />
186
+ <!-- Typing "2.5m" results in value="2500000" -->
136
187
  ```
137
188
 
138
- ## Contributing
189
+ ### Callbacks
139
190
 
140
- Here's ways in which you can contribute:
191
+ ```svelte
192
+ <CurrencyInput
193
+ bind:value
194
+ intlConfig={{ locale: 'en-US', currency: 'USD' }}
195
+ oninputvalue={({ float, formatted, value }) => {
196
+ console.log('On input:', { float, formatted, value });
197
+ }}
198
+ onchangevalue={({ float, formatted, value }) => {
199
+ console.log('On blur:', { float, formatted, value });
200
+ }}
201
+ />
202
+ ```
141
203
 
142
- - Found a bug? Open a [new issue](https://github.com/canutin/svelte-currency-input/issues/new)
143
- - Comment or upvote [existing issues](https://github.com/canutin/svelte-currency-input/issues)
144
- - Submit a [pull request](https://github.com/canutin/svelte-currency-input/pulls)
204
+ ### Input element reference
145
205
 
146
- ## Developing
206
+ ```svelte
207
+ <script>
208
+ let inputRef = $state(null);
147
209
 
148
- This package was generated with [SvelteKit](https://kit.svelte.dev/). Install dependencies with `npm install`, then start a development server:
210
+ function focusInput() {
211
+ inputRef?.focus();
212
+ }
213
+ </script>
149
214
 
150
- ```bash
151
- npm run dev
215
+ <CurrencyInput bind:ref={inputRef} bind:value />
216
+ <button onclick={focusInput}>Focus</button>
217
+ ```
152
218
 
153
- # or start the server and open the app in a new browser tab
154
- npm run dev -- --open
219
+ ## Styling
220
+
221
+ The component renders a single `<input>` element with no built-in styles. You can use the `class` prop to style it:
222
+
223
+ ```svelte
224
+ <!-- Tailwind CSS -->
225
+ <CurrencyInput bind:value class="rounded border px-3 py-2 focus:ring-2 focus:ring-blue-500" />
226
+
227
+ <!-- Custom CSS class -->
228
+ <CurrencyInput bind:value class="my-currency-input" />
155
229
  ```
156
230
 
157
- #### Integration tests
231
+ For dynamic styling based on value (positive/negative/zero), use the callback:
158
232
 
159
- The component is tested using [Playwright](https://playwright.dev/).
160
- You can find the tests in [`tests/svelte-currency-input.test.ts`](https://github.com/Canutin/svelte-currency-input/blob/main/tests/svelte-currency-input.test.ts)
233
+ ```svelte
234
+ <script>
235
+ let value = $state('');
236
+ let colorClass = $state('');
237
+
238
+ function updateStyle({ float }) {
239
+ if (float === null || float === 0) colorClass = 'text-gray-500';
240
+ else if (float > 0) colorClass = 'text-green-600';
241
+ else colorClass = 'text-red-600';
242
+ }
243
+ </script>
161
244
 
162
- To run all tests on **Chromium**, **Firefox** and **Webkit**:
163
- ```bash
164
- npm run test
245
+ <CurrencyInput bind:value class="border px-3 py-2 {colorClass}" oninputvalue={updateStyle} />
165
246
  ```
166
247
 
167
- To run all tests on a specific browser (e.g. **Webkit**):
248
+ ## Exported utilities
249
+
250
+ The package exports utility functions for use outside the component:
251
+
252
+ ```typescript
253
+ import {
254
+ formatValue,
255
+ getLocaleConfig,
256
+ cleanValue,
257
+ parseAbbrValue,
258
+ abbrValue
259
+ } from '@canutin/svelte-currency-input';
260
+
261
+ // Format a value with locale
262
+ const formatted = formatValue({
263
+ value: '1234.56',
264
+ intlConfig: { locale: 'en-US', currency: 'USD' }
265
+ });
266
+ // → "$1,234.56"
267
+
268
+ // Get locale configuration
269
+ const config = getLocaleConfig({ locale: 'de-DE', currency: 'EUR' });
270
+ // → { decimalSeparator: ',', groupSeparator: '.', prefix: '', suffix: ' €', ... }
271
+
272
+ // Parse abbreviations
273
+ const expanded = parseAbbrValue('2.5m', 'en-US');
274
+ // → "2500000"
275
+ ```
276
+
277
+ ## Svelte 4
278
+
279
+ For Svelte 4 support, use the [0.x version](https://github.com/fmaclen/svelte-currency-input/tree/main):
280
+
168
281
  ```bash
169
- npx playwright test --project=webkit
282
+ npm install @canutin/svelte-currency-input@0
170
283
  ```
171
284
 
172
- Additional debug commands can be found on [Playwright's documentation](https://playwright.dev/docs/test-cli).
285
+ If you're upgrading from v0.x, see the [migration guide](./MIGRATION.md).
286
+
287
+ ## Contributing
288
+
289
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, testing, and contribution guidelines.
290
+
291
+ ## License
292
+
293
+ [MIT](./LICENSE)