@canutin/svelte-currency-input 0.3.0 → 0.4.0
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.
- package/README.md +10 -9
- package/package.json +1 -1
- package/src/lib/CurrencyInput.svelte +37 -21
- package/src/routes/+page.svelte +8 -2
- package/tests/svelte-currency-input.test.ts +73 -5
package/README.md
CHANGED
|
@@ -58,15 +58,16 @@ This is more or less what `<CurrencyInput />` looks like under the hood:
|
|
|
58
58
|
|
|
59
59
|
## API
|
|
60
60
|
|
|
61
|
-
| Option | Type
|
|
62
|
-
| ----------------- |
|
|
63
|
-
| value | `number`
|
|
64
|
-
| locale | `string`
|
|
65
|
-
| currency | `string`
|
|
66
|
-
| name | `string`
|
|
67
|
-
| required | `boolean`
|
|
68
|
-
| disabled | `boolean`
|
|
69
|
-
|
|
|
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://github.com/datasets/currency-codes/blob/master/data/codes-all.csv) |
|
|
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
|
+
| required | `boolean` | `false` | Marks the inputs as required |
|
|
68
|
+
| disabled | `boolean` | `false` | Marks the inputs as disabled |
|
|
69
|
+
| placeholder | `number` `null` | `0` | Overrides the default placeholder. Setting the value to a `number` will display it as formatted. Setting it to `null` will not show a placeholder |
|
|
70
|
+
| isNegativeAllowed | `boolean` | `true` | If `false`, forces formatting only to positive values and ignores `--positive` and `--negative` styling modifiers |
|
|
70
71
|
|
|
71
72
|
## Styling
|
|
72
73
|
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
const DEFAULT_LOCALE = 'en-US';
|
|
3
|
+
const DEFAULT_CURRENCY = 'USD';
|
|
4
|
+
const DEFAULT_NAME = 'total';
|
|
5
|
+
const DEFAULT_VALUE = 0;
|
|
6
|
+
|
|
7
|
+
export let value: number = DEFAULT_VALUE;
|
|
8
|
+
export let locale: string = DEFAULT_LOCALE;
|
|
9
|
+
export let currency: string = DEFAULT_CURRENCY;
|
|
10
|
+
export let name: string = DEFAULT_NAME;
|
|
6
11
|
export let required: boolean = false;
|
|
7
12
|
export let disabled: boolean = false;
|
|
13
|
+
export let placeholder: number | null = DEFAULT_VALUE;
|
|
8
14
|
export let isNegativeAllowed: boolean = true;
|
|
9
15
|
|
|
10
|
-
let formattedValue = '';
|
|
11
|
-
$: isZero = value === 0;
|
|
12
|
-
$: isNegative = value < 0;
|
|
13
|
-
$: value, applyFormatting();
|
|
14
|
-
|
|
15
16
|
// Formats value as: e.g. $1,523.00 | -$1,523.00
|
|
16
17
|
const formatCurrency = (
|
|
17
18
|
value: number,
|
|
@@ -26,18 +27,26 @@
|
|
|
26
27
|
}).format(value);
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// Checks if the key pressed is allowed
|
|
31
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
32
|
+
const isDeletion = event.key === 'Backspace' || event.key === 'Delete';
|
|
33
|
+
const isModifier = event.metaKey || event.altKey || event.ctrlKey;
|
|
34
|
+
const isArrowKey = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
|
|
35
|
+
const isInvalidCharacter = !/^\d|,|\.|-$/g.test(event.key); // Keys that are not a digit, comma, period or minus sign
|
|
36
|
+
|
|
37
|
+
if (!isDeletion && !isModifier && !isArrowKey && isInvalidCharacter) event.preventDefault();
|
|
38
|
+
};
|
|
34
39
|
|
|
35
40
|
// Updates `value` by stripping away the currency formatting
|
|
36
|
-
const
|
|
37
|
-
// Don't format if the user is typing a currencyDecimal point
|
|
38
|
-
|
|
41
|
+
const setUnformattedValue = (event: KeyboardEvent) => {
|
|
42
|
+
// Don't format if the user is typing a `currencyDecimal` point
|
|
43
|
+
const currencyDecimal = new Intl.NumberFormat(locale).format(1.1).charAt(1); // '.' or ','
|
|
44
|
+
if (event.key === currencyDecimal) return;
|
|
39
45
|
|
|
40
46
|
// If `formattedValue` is ['$', '-$', "-"] we don't need to continue
|
|
47
|
+
const currencySymbol = formatCurrency(0, 0)
|
|
48
|
+
.replace('0', '') // e.g. '$0' > '$'
|
|
49
|
+
.replace(/\u00A0/, ''); // e.g '0 €' > '€'
|
|
41
50
|
const ignoreSymbols = [currencySymbol, `-${currencySymbol}`, '-'];
|
|
42
51
|
const strippedUnformattedValue = formattedValue.replace(' ', '');
|
|
43
52
|
if (ignoreSymbols.includes(strippedUnformattedValue)) return;
|
|
@@ -48,7 +57,7 @@
|
|
|
48
57
|
: formattedValue.replace(/[^0-9,.]/g, '');
|
|
49
58
|
|
|
50
59
|
// Reverse the value when minus is pressed
|
|
51
|
-
if (isNegativeAllowed && event
|
|
60
|
+
if (isNegativeAllowed && event.key === '-') value = value * -1;
|
|
52
61
|
|
|
53
62
|
// Finally set the value
|
|
54
63
|
if (Number.isNaN(parseFloat(unformattedValue))) {
|
|
@@ -62,9 +71,15 @@
|
|
|
62
71
|
}
|
|
63
72
|
};
|
|
64
73
|
|
|
65
|
-
const
|
|
74
|
+
const setFormattedValue = () => {
|
|
66
75
|
formattedValue = isZero ? '' : formatCurrency(value, 2, 0);
|
|
67
76
|
};
|
|
77
|
+
|
|
78
|
+
let formattedValue = '';
|
|
79
|
+
let formattedPlaceholder = placeholder !== null ? formatCurrency(placeholder, 2, 2) : '';
|
|
80
|
+
$: isZero = value === 0;
|
|
81
|
+
$: isNegative = value < 0;
|
|
82
|
+
$: value, setFormattedValue();
|
|
68
83
|
</script>
|
|
69
84
|
|
|
70
85
|
<div class="currencyInput">
|
|
@@ -80,10 +95,11 @@
|
|
|
80
95
|
inputmode="numeric"
|
|
81
96
|
name={`formatted-${name}`}
|
|
82
97
|
required={required && !isZero}
|
|
83
|
-
{
|
|
98
|
+
placeholder={formattedPlaceholder}
|
|
84
99
|
{disabled}
|
|
85
100
|
bind:value={formattedValue}
|
|
86
|
-
on:
|
|
101
|
+
on:keydown={handleKeyDown}
|
|
102
|
+
on:keyup={setUnformattedValue}
|
|
87
103
|
/>
|
|
88
104
|
</div>
|
|
89
105
|
|
package/src/routes/+page.svelte
CHANGED
|
@@ -18,13 +18,19 @@
|
|
|
18
18
|
<div class="demoForm__container">
|
|
19
19
|
<CurrencyInput name="total" value={-42069.69} />
|
|
20
20
|
<CurrencyInput name="rent" />
|
|
21
|
+
<CurrencyInput name="balance" value={1234.56} isNegativeAllowed={false} placeholder={null} />
|
|
21
22
|
<CurrencyInput name="cashflow" value={5678.9} />
|
|
22
|
-
<CurrencyInput name="balance" value={1234.56} isNegativeAllowed={false} />
|
|
23
23
|
|
|
24
24
|
<CurrencyInput name="amount" value={5678.9} {locale} {currency} />
|
|
25
|
-
<CurrencyInput name="deficit" value={1234.56} isNegativeAllowed={false} {locale} {currency} />
|
|
26
25
|
<CurrencyInput name="loss" value={97532.95} disabled={true} {locale} {currency} />
|
|
27
26
|
<CurrencyInput name="cost" value={-42069.69} {locale} {currency} />
|
|
27
|
+
<CurrencyInput
|
|
28
|
+
name="deficit"
|
|
29
|
+
placeholder={1234.56}
|
|
30
|
+
isNegativeAllowed={false}
|
|
31
|
+
{locale}
|
|
32
|
+
{currency}
|
|
33
|
+
/>
|
|
28
34
|
</div>
|
|
29
35
|
|
|
30
36
|
<nav class="demoForm__output">
|
|
@@ -71,16 +71,16 @@ test.describe('CurrencyInput', () => {
|
|
|
71
71
|
'formatted-total': '-$42,069.69',
|
|
72
72
|
rent: '0',
|
|
73
73
|
'formatted-rent': '',
|
|
74
|
-
cashflow: '5678.9',
|
|
75
|
-
'formatted-cashflow': '$5,678.9',
|
|
76
74
|
balance: '1234.56',
|
|
77
75
|
'formatted-balance': '$1,234.56',
|
|
76
|
+
cashflow: '5678.9',
|
|
77
|
+
'formatted-cashflow': '$5,678.9',
|
|
78
78
|
amount: '5678.9',
|
|
79
79
|
'formatted-amount': '€ 5.678,9',
|
|
80
|
-
deficit: '1234.56',
|
|
81
|
-
'formatted-deficit': '€ 1.234,56',
|
|
82
80
|
cost: '-42069.69',
|
|
83
|
-
'formatted-cost': '€ -42.069,69'
|
|
81
|
+
'formatted-cost': '€ -42.069,69',
|
|
82
|
+
deficit: '0',
|
|
83
|
+
'formatted-deficit': ''
|
|
84
84
|
},
|
|
85
85
|
null,
|
|
86
86
|
2
|
|
@@ -136,6 +136,74 @@ test.describe('CurrencyInput', () => {
|
|
|
136
136
|
await expect(rentUnformattedInput).toHaveValue('0');
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
test("Incorrect characters can't be entered", async ({ page }) => {
|
|
140
|
+
await page.goto('/');
|
|
141
|
+
|
|
142
|
+
const isMacOs = process.platform === 'darwin';
|
|
143
|
+
const rentUnformattedInput = page.locator('.currencyInput__unformatted[name=rent]');
|
|
144
|
+
const rentFormattedInput = page.locator('.currencyInput__formatted[name="formatted-rent"]');
|
|
145
|
+
|
|
146
|
+
// Check the there is no value in the input
|
|
147
|
+
await expect(rentUnformattedInput).toHaveValue('0');
|
|
148
|
+
await expect(rentFormattedInput).toHaveValue('');
|
|
149
|
+
|
|
150
|
+
// Check typing letters doesn't do anything
|
|
151
|
+
await rentFormattedInput.focus();
|
|
152
|
+
await page.keyboard.type('abc');
|
|
153
|
+
await expect(rentUnformattedInput).toHaveValue('0');
|
|
154
|
+
await expect(rentFormattedInput).toHaveValue('');
|
|
155
|
+
|
|
156
|
+
// Check keyboard combinations don't do anything
|
|
157
|
+
await page.keyboard.press('Shift+A');
|
|
158
|
+
await expect(rentFormattedInput).toHaveValue('');
|
|
159
|
+
|
|
160
|
+
// Check keyboard shortcuts are allowed
|
|
161
|
+
await page.keyboard.type('420.69');
|
|
162
|
+
await expect(rentFormattedInput).toHaveValue('$420.69');
|
|
163
|
+
await expect(rentUnformattedInput).toHaveValue('420.69');
|
|
164
|
+
|
|
165
|
+
// Select all
|
|
166
|
+
isMacOs ? await page.keyboard.press('Meta+A') : await page.keyboard.press('Control+A');
|
|
167
|
+
|
|
168
|
+
// Check "Backspace" works
|
|
169
|
+
await page.keyboard.press('Backspace');
|
|
170
|
+
await expect(rentUnformattedInput).toHaveValue('0');
|
|
171
|
+
await expect(rentFormattedInput).toHaveValue('');
|
|
172
|
+
|
|
173
|
+
// Add data to the field again
|
|
174
|
+
await page.keyboard.type('-420.69');
|
|
175
|
+
await expect(rentFormattedInput).toHaveValue('-$420.69');
|
|
176
|
+
await expect(rentUnformattedInput).toHaveValue('-420.69');
|
|
177
|
+
|
|
178
|
+
// Select all
|
|
179
|
+
isMacOs ? await page.keyboard.press('Meta+A') : await page.keyboard.press('Control+A');
|
|
180
|
+
|
|
181
|
+
// Check "Delete" also works
|
|
182
|
+
await page.keyboard.press('Delete');
|
|
183
|
+
await expect(rentUnformattedInput).toHaveValue('0');
|
|
184
|
+
await expect(rentFormattedInput).toHaveValue('');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('Placeholders can be overriden', async ({ page }) => {
|
|
188
|
+
await page.goto('/');
|
|
189
|
+
|
|
190
|
+
// Default placeholder
|
|
191
|
+
const rentFormattedInput = page.locator('.currencyInput__formatted[name="formatted-rent"]');
|
|
192
|
+
await expect(rentFormattedInput).toHaveAttribute('placeholder', '$0.00');
|
|
193
|
+
|
|
194
|
+
// Null placeholder
|
|
195
|
+
const balanceFormattedInput = page.locator(
|
|
196
|
+
'.currencyInput__formatted[name="formatted-balance"]'
|
|
197
|
+
);
|
|
198
|
+
await expect(balanceFormattedInput).toHaveAttribute('placeholder', '');
|
|
199
|
+
|
|
200
|
+
// Overriden placeholder
|
|
201
|
+
const deficitFormattedInput = page.locator(
|
|
202
|
+
'.currencyInput__formatted[name="formatted-deficit"]'
|
|
203
|
+
);
|
|
204
|
+
await expect(deficitFormattedInput).toHaveAttribute('placeholder', '€ 1.234,56'); // The space is `%A0`, not `%20`
|
|
205
|
+
});
|
|
206
|
+
|
|
139
207
|
test.skip('Updating chained inputs have the correct behavior', async () => {
|
|
140
208
|
// TODO
|
|
141
209
|
});
|