fix(input-amount): returns Unparseable as a modelValue if a wrong value has been entered (#2242)
* fix(input-amount): returns Unparseable as a modelValue if a wrong value has been entered * fix(input-amount): do not break when a large amount has been entered * Update docs/components/input-amount/overview.md Co-authored-by: Thijs Louisse <thijs.louisse@ing.com> * Update packages/ui/components/input-amount/test/parsers.test.js Co-authored-by: Thijs Louisse <thijs.louisse@ing.com> --------- Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>
This commit is contained in:
parent
efcd4bf15b
commit
e8e9c07ec5
7 changed files with 70 additions and 9 deletions
5
.changeset/ninety-rats-retire.md
Normal file
5
.changeset/ninety-rats-retire.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
[input-amount] returns Unparseable as a modelValue if a wrong value has been entered
|
||||||
|
|
@ -4,7 +4,7 @@ A web component based on the generic text input field. Its purpose is to provide
|
||||||
|
|
||||||
For formatting, we use [Intl NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat) with some overrides.
|
For formatting, we use [Intl NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat) with some overrides.
|
||||||
|
|
||||||
For parsing user input, we provide our own parser that takes into account a number of heuristics, locale and ignores invalid characters.
|
For parsing user input, we provide our own parser that takes into account locale, a number of heuristics and allows for pasting amount strings like 'EUR 100,00'.
|
||||||
Valid characters are digits and separators. Formatting happens on-blur.
|
Valid characters are digits and separators. Formatting happens on-blur.
|
||||||
|
|
||||||
If there are no valid characters in the input whatsoever, it will provide an error feedback.
|
If there are no valid characters in the input whatsoever, it will provide an error feedback.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ import { parseNumber, getFractionDigits } from '@lion/ui/localize-no-side-effect
|
||||||
* @return {number} new value with rounded up decimals
|
* @return {number} new value with rounded up decimals
|
||||||
*/
|
*/
|
||||||
function round(value, decimals) {
|
function round(value, decimals) {
|
||||||
if (typeof decimals === 'undefined') {
|
const numberContainsExponent = value?.toString().includes('e');
|
||||||
|
if (typeof decimals === 'undefined' || numberContainsExponent) {
|
||||||
return Number(value);
|
return Number(value);
|
||||||
}
|
}
|
||||||
return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`);
|
return Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`);
|
||||||
|
|
@ -30,10 +31,17 @@ function round(value, decimals) {
|
||||||
* @param {FormatOptions} [givenOptions] Locale Options
|
* @param {FormatOptions} [givenOptions] Locale Options
|
||||||
*/
|
*/
|
||||||
export function parseAmount(value, givenOptions) {
|
export function parseAmount(value, givenOptions) {
|
||||||
|
const unmatchedInput = value.match(/[^0-9,.\- ]/g);
|
||||||
|
// for the full paste behavior documentation:
|
||||||
|
// ./docs/components/input-amount/use-cases.md#paste-behavior
|
||||||
|
if (unmatchedInput && givenOptions?.mode !== 'pasted') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const number = parseNumber(value, givenOptions);
|
const number = parseNumber(value, givenOptions);
|
||||||
|
|
||||||
if (typeof number !== 'number') {
|
if (typeof number !== 'number' || Number.isNaN(number)) {
|
||||||
return number;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {FormatOptions} */
|
/** @type {FormatOptions} */
|
||||||
|
|
|
||||||
|
|
@ -74,4 +74,15 @@ describe('formatAmount()', () => {
|
||||||
localizeManager.locale = 'nl-NL';
|
localizeManager.locale = 'nl-NL';
|
||||||
expect(formatAmount(12345678)).to.equal('12.345.678,00');
|
expect(formatAmount(12345678)).to.equal('12.345.678,00');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: make it work with big numbers, e.g. make use of BigInt
|
||||||
|
it.skip('rounds up big numbers', async () => {
|
||||||
|
expect(formatAmount(1e21, { locale: 'en-GB', currency: 'EUR' })).to.equal(
|
||||||
|
'1,000,000,000,000,000,000,000.00',
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-loss-of-precision
|
||||||
|
expect(formatAmount(12345678987654321.42, { locale: 'en-GB', currency: 'EUR' })).to.equal(
|
||||||
|
'12,345,678,987,654,321.42',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
import { expect } from '@open-wc/testing';
|
import { expect } from '@open-wc/testing';
|
||||||
import { localize } from '@lion/ui/localize.js';
|
import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js';
|
||||||
|
import { localizeTearDown } from '@lion/ui/localize-test-helpers.js';
|
||||||
import { parseAmount } from '@lion/ui/input-amount.js';
|
import { parseAmount } from '@lion/ui/input-amount.js';
|
||||||
|
|
||||||
describe('parseAmount()', async () => {
|
describe('parseAmount()', async () => {
|
||||||
|
const localizeManager = getLocalizeManager();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localizeManager.locale = 'en-GB';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localizeTearDown();
|
||||||
|
});
|
||||||
|
|
||||||
it('with currency set to correct amount of decimals', async () => {
|
it('with currency set to correct amount of decimals', async () => {
|
||||||
localize.locale = 'en-GB';
|
|
||||||
expect(
|
expect(
|
||||||
parseAmount('1.015', {
|
parseAmount('1.015', {
|
||||||
currency: 'EUR',
|
currency: 'EUR',
|
||||||
|
|
@ -29,7 +38,26 @@ describe('parseAmount()', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with no currency keeps all decimals', async () => {
|
it('with no currency keeps all decimals', async () => {
|
||||||
localize.locale = 'en-GB';
|
|
||||||
expect(parseAmount('1.015')).to.equal(1.015);
|
expect(parseAmount('1.015')).to.equal(1.015);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: make it work with big numbers, e.g. make use of BigInt
|
||||||
|
it.skip('rounds up big numbers', async () => {
|
||||||
|
// eslint-disable-next-line no-loss-of-precision
|
||||||
|
expect(parseAmount('999999999999999999999,42')).to.equal(999999999999999999999.42);
|
||||||
|
// eslint-disable-next-line no-loss-of-precision
|
||||||
|
expect(parseAmount('12,345,678,987,654,321.42')).to.equal(12345678987654321.42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined if an invalid value is entered', async () => {
|
||||||
|
expect(parseAmount('foo')).to.equal(undefined);
|
||||||
|
expect(parseAmount('foo1')).to.equal(undefined);
|
||||||
|
expect(parseAmount('EUR 1,50')).to.equal(undefined);
|
||||||
|
expect(parseAmount('--1')).to.equal(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores letters when "pasted" mode used', async () => {
|
||||||
|
expect(parseAmount('foo1', { mode: 'pasted' })).to.equal(1);
|
||||||
|
expect(parseAmount('EUR 1,50', { mode: 'pasted' })).to.equal(1.5);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function formatNumber(number, options = /** @type {FormatOptions} */ ({})
|
||||||
}
|
}
|
||||||
let printNumberOfParts = '';
|
let printNumberOfParts = '';
|
||||||
// update numberOfParts because there may be some parts added
|
// update numberOfParts because there may be some parts added
|
||||||
const numberOfParts = formattedToParts && formattedToParts.length;
|
const numberOfParts = formattedToParts ? formattedToParts.length : 0;
|
||||||
for (let i = 0; i < numberOfParts; i += 1) {
|
for (let i = 0; i < numberOfParts; i += 1) {
|
||||||
const part = /** @type {FormatNumberPart} */ (formattedToParts[i]);
|
const part = /** @type {FormatNumberPart} */ (formattedToParts[i]);
|
||||||
printNumberOfParts += part.value;
|
printNumberOfParts += part.value;
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,15 @@ Please specify .groupSeparator / .decimalSeparator on the formatOptions object t
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: make it work with big numbers, e.g. make use of BigInt
|
||||||
|
it.skip('can handle big numbers', async () => {
|
||||||
|
expect(formatNumber(1e21)).to.equal('1,000,000,000,000,000,000,000');
|
||||||
|
// eslint-disable-next-line no-loss-of-precision
|
||||||
|
expect(formatNumber(999999999999999999999.42)).to.equal('999,999,999,999,999,999,999.42');
|
||||||
|
// eslint-disable-next-line no-loss-of-precision
|
||||||
|
expect(formatNumber(12345678987654321.42)).to.equal('12,345,678,987,654,321.42');
|
||||||
|
});
|
||||||
|
|
||||||
describe('normalization', () => {
|
describe('normalization', () => {
|
||||||
describe('en-GB', () => {
|
describe('en-GB', () => {
|
||||||
it('supports basics', () => {
|
it('supports basics', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue