Merge pull request #398 from ing-bank/fix/amountParsing

fix(input-amount): handle user pasting of amounts heuristically
This commit is contained in:
Thomas Allmer 2019-11-28 17:56:44 +01:00 committed by GitHub
commit 47cadb1696
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 73 additions and 30 deletions

View file

@ -310,12 +310,16 @@ export const FormatMixin = dedupeMixin(
* `@user-input-changed` (this will happen later, when `formatOn` condition is met)
*/
_reflectBackFormattedValueToUser() {
if (!this.__isHandlingUserInput) {
if (this._reflectBackOn()) {
// Text 'undefined' should not end up in <input>
this.value = typeof this.formattedValue !== 'undefined' ? this.formattedValue : '';
}
}
_reflectBackOn() {
return !this.__isHandlingUserInput;
}
// This can be called whenever the view value should be updated. Dependent on component type
// ("input" for <input> or "change" for <select>(mainly for IE)) a different event should be
// used as source for the "user-input-changed" event (which can be seen as an abstraction

View file

@ -49,10 +49,28 @@ export class LionInputAmount extends FieldCustomMixin(LocalizeMixin(LionInput))
super();
this.parser = parseAmount;
this.formatter = formatAmount;
this.__isPasting = false;
this.addEventListener('paste', () => {
this.__isPasting = true;
this.__parserCallcountSincePaste = 0;
});
this.defaultValidators.push(new IsNumber());
}
__callParser(value = this.formattedValue) {
// TODO: input and change events both trigger parsing therefore we need to handle the second parse
this.__parserCallcountSincePaste += 1;
this.__isPasting = this.__parserCallcountSincePaste === 2;
this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto';
return super.__callParser(value);
}
_reflectBackOn() {
return super._reflectBackOn() || this.__isPasting;
}
connectedCallback() {
// eslint-disable-next-line wc/guard-super-call
super.connectedCallback();

View file

@ -11,9 +11,10 @@ function isDecimalSeparator(value) {
/**
* Determines the best possible parsing mode.
*
* Parsemode depends mostely on the last 4 chars.
* - 1234 => xxx1234 (heuristic)
* - If there is only one separator (withLocale)
* - 1,23 => xxx1.23 (heuristic)
* - else parse mode depends mostly on the last 4 chars
* - 1234 => xxx1234 (heuristic)
* - [space]123 => xxx123 (heuristic)
* - ,123 => unclear
* - if 1.000,123 (we find a different separator) => 1000.123 (heuristic)
@ -30,7 +31,13 @@ function isDecimalSeparator(value) {
* @param {string} value Clean number (only [0-9 ,.]) to be parsed
* @return {string} unparseable|withLocale|heuristic
*/
function getParseMode(value) {
function getParseMode(value, { mode = 'auto' } = {}) {
const separators = value.match(/[., ]/g);
if (mode === 'auto' && separators && separators.length === 1) {
return 'withLocale';
}
if (value.length > 4) {
const charAtLastSeparatorPosition = value[value.length - 4];
if (isDecimalSeparator(charAtLastSeparatorPosition)) {
@ -120,7 +127,7 @@ export function parseAmount(value, options) {
return undefined;
}
const cleanedInput = matchedInput.join('');
const parseMode = getParseMode(cleanedInput);
const parseMode = getParseMode(cleanedInput, options);
switch (parseMode) {
case 'unparseable':
return parseFloat(cleanedInput.match(/[0-9]/g).join(''));

View file

@ -34,8 +34,12 @@ storiesOf('Forms|Input Amount', module)
.add(
'Force locale to nl-NL',
() => html`
<lion-input-amount label="Price" currency="JOD">
.formatOptions="${{ locale: 'nl-NL' }}" .modelValue=${123456.78}
<lion-input-amount
label="Price"
currency="JOD"
.formatOptions="${{ locale: 'nl-NL' }}"
.modelValue=${123456.78}
>
</lion-input-amount>
`,
)
@ -79,5 +83,11 @@ storiesOf('Forms|Input Amount', module)
Make sure to set the modelValue last as otherwise formatOptions will not be taken into
account
</p>
<p>
You can copy paste <input value="4000,0" /> and it will become 4000 independent of your
locale. <br />
If you write 4000,0 manually then it will become 4000 or 40000 dependent on your locale.
</p>
`,
);

View file

@ -43,35 +43,35 @@ describe('parseAmount()', () => {
expect(parseAmount('1 234,56789')).to.equal(1234.56789);
});
it('detects separators heuristically when there is only one e.g. 123456,78', () => {
expect(parseAmount('1.')).to.equal(1);
expect(parseAmount('1,')).to.equal(1);
expect(parseAmount('1 ')).to.equal(1);
it('detects separators heuristically when there is only one and "pasted" mode used e.g. 123456,78', () => {
expect(parseAmount('1.', { mode: 'pasted' })).to.equal(1);
expect(parseAmount('1,', { mode: 'pasted' })).to.equal(1);
expect(parseAmount('1 ', { mode: 'pasted' })).to.equal(1);
expect(parseAmount('1.2')).to.equal(1.2);
expect(parseAmount('1,2')).to.equal(1.2);
expect(parseAmount('1 2')).to.equal(12);
expect(parseAmount('1.2', { mode: 'pasted' })).to.equal(1.2);
expect(parseAmount('1,2', { mode: 'pasted' })).to.equal(1.2);
expect(parseAmount('1 2', { mode: 'pasted' })).to.equal(12);
expect(parseAmount('1.23')).to.equal(1.23);
expect(parseAmount('1,23')).to.equal(1.23);
expect(parseAmount('1 23')).to.equal(123);
expect(parseAmount('1.23', { mode: 'pasted' })).to.equal(1.23);
expect(parseAmount('1,23', { mode: 'pasted' })).to.equal(1.23);
expect(parseAmount('1 23', { mode: 'pasted' })).to.equal(123);
expect(parseAmount('1 234')).to.equal(1234);
expect(parseAmount('1 234', { mode: 'pasted' })).to.equal(1234);
expect(parseAmount('1.2345')).to.equal(1.2345);
expect(parseAmount('1,2345')).to.equal(1.2345);
expect(parseAmount('1 2345')).to.equal(12345);
expect(parseAmount('1.2345', { mode: 'pasted' })).to.equal(1.2345);
expect(parseAmount('1,2345', { mode: 'pasted' })).to.equal(1.2345);
expect(parseAmount('1 2345', { mode: 'pasted' })).to.equal(12345);
expect(parseAmount('1.23456')).to.equal(1.23456);
expect(parseAmount('1,23456')).to.equal(1.23456);
expect(parseAmount('1 23456')).to.equal(123456);
expect(parseAmount('1.23456', { mode: 'pasted' })).to.equal(1.23456);
expect(parseAmount('1,23456', { mode: 'pasted' })).to.equal(1.23456);
expect(parseAmount('1 23456', { mode: 'pasted' })).to.equal(123456);
expect(parseAmount('1.234567')).to.equal(1.234567);
expect(parseAmount('1,234567')).to.equal(1.234567);
expect(parseAmount('1 234567')).to.equal(1234567);
expect(parseAmount('1.234567', { mode: 'pasted' })).to.equal(1.234567);
expect(parseAmount('1,234567', { mode: 'pasted' })).to.equal(1.234567);
expect(parseAmount('1 234567', { mode: 'pasted' })).to.equal(1234567);
expect(parseAmount('123456,78')).to.equal(123456.78);
expect(parseAmount('123456.78')).to.equal(123456.78);
expect(parseAmount('123456,78', { mode: 'pasted' })).to.equal(123456.78);
expect(parseAmount('123456.78', { mode: 'pasted' })).to.equal(123456.78);
});
it('detects separators heuristically when there are 2 same ones e.g. 1.234.56', () => {
@ -89,12 +89,16 @@ describe('parseAmount()', () => {
expect(parseAmount('1,234,56789')).to.equal(1234.56789);
});
it('uses locale if amount can not be interpreted heuristically e.g. 1.234', () => {
it('uses locale to parse amount if there is only one separator e.g. 1.234', () => {
localize.locale = 'en-GB';
expect(parseAmount('12.34')).to.equal(12.34);
expect(parseAmount('12,34')).to.equal(1234);
expect(parseAmount('1.234')).to.equal(1.234);
expect(parseAmount('1,234')).to.equal(1234);
localize.locale = 'nl-NL';
expect(parseAmount('12.34')).to.equal(1234);
expect(parseAmount('12,34')).to.equal(12.34);
expect(parseAmount('1.234')).to.equal(1234);
expect(parseAmount('1,234')).to.equal(1.234);
});