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) * `@user-input-changed` (this will happen later, when `formatOn` condition is met)
*/ */
_reflectBackFormattedValueToUser() { _reflectBackFormattedValueToUser() {
if (!this.__isHandlingUserInput) { if (this._reflectBackOn()) {
// Text 'undefined' should not end up in <input> // Text 'undefined' should not end up in <input>
this.value = typeof this.formattedValue !== 'undefined' ? this.formattedValue : ''; 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 // 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 // ("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 // 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(); super();
this.parser = parseAmount; this.parser = parseAmount;
this.formatter = formatAmount; this.formatter = formatAmount;
this.__isPasting = false;
this.addEventListener('paste', () => {
this.__isPasting = true;
this.__parserCallcountSincePaste = 0;
});
this.defaultValidators.push(new IsNumber()); 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() { connectedCallback() {
// eslint-disable-next-line wc/guard-super-call // eslint-disable-next-line wc/guard-super-call
super.connectedCallback(); super.connectedCallback();

View file

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

View file

@ -34,8 +34,12 @@ storiesOf('Forms|Input Amount', module)
.add( .add(
'Force locale to nl-NL', 'Force locale to nl-NL',
() => html` () => html`
<lion-input-amount label="Price" currency="JOD"> <lion-input-amount
.formatOptions="${{ locale: 'nl-NL' }}" .modelValue=${123456.78} label="Price"
currency="JOD"
.formatOptions="${{ locale: 'nl-NL' }}"
.modelValue=${123456.78}
>
</lion-input-amount> </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 Make sure to set the modelValue last as otherwise formatOptions will not be taken into
account account
</p> </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); expect(parseAmount('1 234,56789')).to.equal(1234.56789);
}); });
it('detects separators heuristically when there is only one e.g. 123456,78', () => { it('detects separators heuristically when there is only one and "pasted" mode used e.g. 123456,78', () => {
expect(parseAmount('1.')).to.equal(1); expect(parseAmount('1.', { mode: 'pasted' })).to.equal(1);
expect(parseAmount('1,')).to.equal(1); expect(parseAmount('1,', { mode: 'pasted' })).to.equal(1);
expect(parseAmount('1 ')).to.equal(1); expect(parseAmount('1 ', { mode: 'pasted' })).to.equal(1);
expect(parseAmount('1.2')).to.equal(1.2); expect(parseAmount('1.2', { mode: 'pasted' })).to.equal(1.2);
expect(parseAmount('1,2')).to.equal(1.2); expect(parseAmount('1,2', { mode: 'pasted' })).to.equal(1.2);
expect(parseAmount('1 2')).to.equal(12); expect(parseAmount('1 2', { mode: 'pasted' })).to.equal(12);
expect(parseAmount('1.23')).to.equal(1.23); expect(parseAmount('1.23', { mode: 'pasted' })).to.equal(1.23);
expect(parseAmount('1,23')).to.equal(1.23); expect(parseAmount('1,23', { mode: 'pasted' })).to.equal(1.23);
expect(parseAmount('1 23')).to.equal(123); 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', { mode: 'pasted' })).to.equal(1.2345);
expect(parseAmount('1,2345')).to.equal(1.2345); expect(parseAmount('1,2345', { mode: 'pasted' })).to.equal(1.2345);
expect(parseAmount('1 2345')).to.equal(12345); expect(parseAmount('1 2345', { mode: 'pasted' })).to.equal(12345);
expect(parseAmount('1.23456')).to.equal(1.23456); expect(parseAmount('1.23456', { mode: 'pasted' })).to.equal(1.23456);
expect(parseAmount('1,23456')).to.equal(1.23456); expect(parseAmount('1,23456', { mode: 'pasted' })).to.equal(1.23456);
expect(parseAmount('1 23456')).to.equal(123456); expect(parseAmount('1 23456', { mode: 'pasted' })).to.equal(123456);
expect(parseAmount('1.234567')).to.equal(1.234567); expect(parseAmount('1.234567', { mode: 'pasted' })).to.equal(1.234567);
expect(parseAmount('1,234567')).to.equal(1.234567); expect(parseAmount('1,234567', { mode: 'pasted' })).to.equal(1.234567);
expect(parseAmount('1 234567')).to.equal(1234567); expect(parseAmount('1 234567', { mode: 'pasted' })).to.equal(1234567);
expect(parseAmount('123456,78')).to.equal(123456.78); expect(parseAmount('123456,78', { mode: 'pasted' })).to.equal(123456.78);
expect(parseAmount('123456.78')).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', () => { 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); 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'; 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(1.234);
expect(parseAmount('1,234')).to.equal(1234); expect(parseAmount('1,234')).to.equal(1234);
localize.locale = 'nl-NL'; 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(1234);
expect(parseAmount('1,234')).to.equal(1.234); expect(parseAmount('1,234')).to.equal(1.234);
}); });