Skip to content

Commit 32b9f1e

Browse files
authored
fix: Input not reformatting after external value changes (#75)
* #74 - Format with min fraction digits, except when the field is active * add chained inputs to demo page * update existing tests * test: chained inputs have the correct behavior * apply formatting * update test comment
1 parent 7d5c21b commit 32b9f1e

File tree

3 files changed

+210
-71
lines changed

3 files changed

+210
-71
lines changed

src/lib/CurrencyInput.svelte

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,20 @@
7878
7979
// Formats the value when the input loses focus and sets the correct number of
8080
// fraction digits when the value is zero
81-
const handleOnBlur = () => setFormattedValue(true);
81+
const handleOnBlur = () => setFormattedValue();
8282
83-
// Also set the correct fraction digits when the value is zero on initial load
84-
onMount(() => setFormattedValue(true));
83+
let dom: Document;
84+
let inputElement: HTMLInputElement;
8585
86-
let inputTarget: HTMLInputElement;
86+
onMount(() => {
87+
// Set the document object as a variable so we know the page has mounted
88+
dom = document;
89+
90+
// Set the correct fraction digits when the value is zero on initial load
91+
setFormattedValue();
92+
});
93+
94+
let inputTarget: EventTarget | null;
8795
const currencyDecimal = new Intl.NumberFormat(locale).format(1.1).charAt(1); // '.' or ','
8896
const isDecimalComma = currencyDecimal === ',';
8997
const currencySymbol = formatCurrency(0, 0)
@@ -110,7 +118,7 @@
110118
if (ignoreSymbols.includes(strippedUnformattedValue)) return;
111119
112120
// Set the starting caret positions
113-
inputTarget = event.target as HTMLInputElement;
121+
inputTarget = event.target;
114122
115123
// Reverse the value when minus is pressed
116124
if (isNegativeAllowed && event.key === '-') value = value * -1;
@@ -147,13 +155,23 @@
147155
}
148156
};
149157
150-
const setFormattedValue = (hasMinFractionDigits?: boolean) => {
158+
const setFormattedValue = () => {
159+
// Do nothing because the page hasn't mounted yet
160+
if (!dom) return;
161+
151162
// Previous caret position
152-
const startCaretPosition = inputTarget?.selectionStart || 0;
163+
const startCaretPosition = inputElement?.selectionStart || 0;
153164
const previousFormattedValueLength = formattedValue.length;
154165
155166
// Apply formatting to input
156-
formattedValue = isZero && !isZeroNullish ? '' : formatCurrency(value, fractionDigits, hasMinFractionDigits ? fractionDigits : 0);
167+
formattedValue =
168+
isZero && !isZeroNullish
169+
? ''
170+
: formatCurrency(
171+
value,
172+
fractionDigits,
173+
dom.activeElement === inputElement ? 0 : fractionDigits
174+
);
157175
158176
// Update `value` after formatting
159177
setUnformattedValue();
@@ -164,16 +182,17 @@
164182
if (previousFormattedValueLength !== formattedValue.length) {
165183
const endCaretPosition =
166184
startCaretPosition + formattedValue.length - previousFormattedValueLength;
167-
inputTarget?.setSelectionRange(endCaretPosition, endCaretPosition);
185+
inputElement?.setSelectionRange(endCaretPosition, endCaretPosition);
168186
}
169187
170188
// Run callback function when `value` changes
171189
onValueChange(value);
172190
};
173191
174192
const handlePlaceholder = (placeholder: string | number | null) => {
175-
if (typeof placeholder === "number") return formatCurrency(placeholder, fractionDigits, fractionDigits);
176-
if (placeholder === null) return "";
193+
if (typeof placeholder === 'number')
194+
return formatCurrency(placeholder, fractionDigits, fractionDigits);
195+
if (placeholder === null) return '';
177196
return placeholder;
178197
};
179198
@@ -204,12 +223,13 @@
204223
: ''}
205224
"
206225
type="text"
207-
inputmode={fractionDigits > 0 ? "decimal" : "numeric"}
226+
inputmode={fractionDigits > 0 ? 'decimal' : 'numeric'}
208227
name={`formatted-${name}`}
209228
required={required && !isZero}
210229
placeholder={handlePlaceholder(placeholder)}
211230
{autocomplete}
212231
{disabled}
232+
bind:this={inputElement}
213233
bind:value={formattedValue}
214234
on:keydown={handleKeyDown}
215235
on:keyup={setUnformattedValue}

src/routes/+page.svelte

Lines changed: 117 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
};
1313
1414
let unchangedValue = 999; // Used for onValueChange()
15+
let chainedValue = 9999.99; // Used for chained inputs
16+
17+
const setChainedValue = () => {
18+
chainedValue = 420.69;
19+
};
1520
</script>
1621

1722
<form class="demoForm" on:submit={handleSubmit}>
@@ -43,61 +48,105 @@
4348
</nav>
4449

4550
<div class="demoForm__container">
46-
<CurrencyInput name="default" value={-42069.69} />
47-
<CurrencyInput name="colon" locale="es-CR" currency="CRC" />
48-
<CurrencyInput
49-
name="pound"
50-
value={1234.56}
51-
isNegativeAllowed={false}
52-
placeholder={null}
53-
locale="en-GB"
54-
currency="GBP"
55-
/>
56-
<CurrencyInput
57-
name="bitcoin"
58-
value={0.87654321}
59-
locale="th-TH"
60-
currency="THB"
61-
fractionDigits={8}
62-
/>
63-
64-
<CurrencyInput name="yen" value={5678.9} locale="en-JP" currency="JPY" />
65-
<CurrencyInput name="shekel" value={97532.95} disabled={true} locale="il-IL" currency="ILS" />
66-
<CurrencyInput name="euro" value={-42069.69} locale="nl-NL" currency="EUR" />
67-
<CurrencyInput
68-
name="won"
69-
placeholder={1234.56}
70-
isNegativeAllowed={false}
71-
locale="ko-KO"
72-
currency="KRW"
73-
inputClasses={{
74-
wrapper: 'currencyInput custom-wrapper-class',
75-
unformatted: 'custom-unformatted-class'
76-
}}
77-
/>
78-
<CurrencyInput
79-
name="pesos"
80-
value={unchangedValue}
81-
isNegativeAllowed={false}
82-
placeholder={null}
83-
autocomplete="off"
84-
locale="es-AR"
85-
currency="ARS"
86-
onValueChange={(value) => {
87-
// Prevent alerting on initial load
88-
if (unchangedValue !== value) {
89-
unchangedValue = value; // Update the unchanged value
90-
if (browser) window.alert(`The value for ARS has changed to: ${value}`); // Alert the user
91-
}
92-
}}
93-
/>
94-
<CurrencyInput name="rupees" value={678} locale="hi-IN" currency="INR" fractionDigits={3} />
95-
<CurrencyInput name="soles" value={0} isZeroNullish={true} placeholder={null} locale="es-PE" currency="PEN" />
96-
<CurrencyInput name="dinars" value={0} placeholder={"How many Dinars?"} locale="en-US" currency="RSD" fractionDigits={0} />
51+
<fieldset class="demoForm__fieldset">
52+
<legend class="demoForm__legend">Stand-alone inputs</legend>
53+
<CurrencyInput name="default" value={-42069.69} />
54+
<CurrencyInput name="colon" locale="es-CR" currency="CRC" />
55+
<CurrencyInput
56+
name="pound"
57+
value={1234.56}
58+
isNegativeAllowed={false}
59+
placeholder={null}
60+
locale="en-GB"
61+
currency="GBP"
62+
/>
63+
<CurrencyInput
64+
name="bitcoin"
65+
value={0.87654321}
66+
locale="th-TH"
67+
currency="THB"
68+
fractionDigits={8}
69+
/>
70+
71+
<CurrencyInput name="yen" value={5678.9} locale="en-JP" currency="JPY" />
72+
<CurrencyInput name="shekel" value={97532.95} disabled={true} locale="il-IL" currency="ILS" />
73+
<CurrencyInput name="euro" value={-42069.69} locale="nl-NL" currency="EUR" />
74+
<CurrencyInput
75+
name="won"
76+
placeholder={1234.56}
77+
isNegativeAllowed={false}
78+
locale="ko-KO"
79+
currency="KRW"
80+
inputClasses={{
81+
wrapper: 'currencyInput custom-wrapper-class',
82+
unformatted: 'custom-unformatted-class'
83+
}}
84+
/>
85+
<CurrencyInput
86+
name="pesos"
87+
value={unchangedValue}
88+
isNegativeAllowed={false}
89+
placeholder={null}
90+
autocomplete="off"
91+
locale="es-AR"
92+
currency="ARS"
93+
onValueChange={(value) => {
94+
// Prevent alerting on initial load
95+
if (unchangedValue !== value) {
96+
unchangedValue = value; // Update the unchanged value
97+
if (browser) window.alert(`The value for ARS has changed to: ${value}`); // Alert the user
98+
}
99+
}}
100+
/>
101+
<CurrencyInput name="rupees" value={678} locale="hi-IN" currency="INR" fractionDigits={3} />
102+
<CurrencyInput
103+
name="soles"
104+
value={0}
105+
isZeroNullish={true}
106+
placeholder={null}
107+
locale="es-PE"
108+
currency="PEN"
109+
/>
110+
<CurrencyInput
111+
name="dinars"
112+
value={0}
113+
placeholder={'How many Dinars?'}
114+
locale="en-US"
115+
currency="RSD"
116+
fractionDigits={0}
117+
/>
118+
</fieldset>
119+
120+
<fieldset class="demoForm__fieldset">
121+
<legend class="demoForm__legend">Chained inputs</legend>
122+
<CurrencyInput
123+
name="chained-east-caribbean-dollar"
124+
bind:value={chainedValue}
125+
locale="aig"
126+
currency="XCD"
127+
fractionDigits={4}
128+
/>
129+
<CurrencyInput name="chained-euros" bind:value={chainedValue} locale="nl-NL" currency="EUR" />
130+
<CurrencyInput
131+
name="chained-dollars"
132+
bind:value={chainedValue}
133+
locale="en-US"
134+
currency="USD"
135+
fractionDigits={0}
136+
/>
137+
<button
138+
id="set-chained-value"
139+
type="button"
140+
class="demoForm__button"
141+
on:click={setChainedValue}
142+
>
143+
Set chained value to <strong>420.69</strong>
144+
</button>
145+
</fieldset>
97146
</div>
98147

99148
<nav class="demoForm__output">
100-
<button type="submit" class="demoForm__submit">Submit form</button>
149+
<button id="submit-form" type="submit" class="demoForm__button">Submit form</button>
101150

102151
<pre class="demoForm__pre {!output && 'demoForm__pre--placeholder'}">{output
103152
? output
@@ -144,22 +193,35 @@
144193
}
145194
146195
div.demoForm__container {
196+
display: flex;
197+
flex-direction: column;
198+
gap: 32px;
199+
}
200+
201+
fieldset.demoForm__fieldset {
147202
display: grid;
148203
grid-template-columns: repeat(4, 1fr);
149204
align-items: center;
150205
justify-content: center;
151206
gap: calc(var(--gap) / 2);
152207
height: max-content;
208+
border: 1px solid #ccc;
209+
padding: 16px;
153210
154211
@media (max-width: 768px) {
155212
grid-template-columns: repeat(2, 1fr);
156213
}
157-
214+
158215
@media (max-width: 512px) {
159216
grid-template-columns: 1fr;
160217
}
161218
}
162219
220+
legend.demoForm__legend {
221+
font-size: 13px;
222+
color: #666;
223+
}
224+
163225
h1.demoForm__h1 {
164226
color: #333;
165227
font-size: 20px;
@@ -224,7 +286,7 @@
224286
font-size: 13px;
225287
}
226288
227-
button.demoForm__submit {
289+
button.demoForm__button {
228290
border: none;
229291
background-color: #333;
230292
color: #fff;
@@ -234,7 +296,7 @@
234296
height: max-content;
235297
}
236298
237-
button.demoForm__submit:hover {
299+
button.demoForm__button:hover {
238300
background-color: #000;
239301
}
240302
</style>

0 commit comments

Comments
 (0)