feat: add preprocessor hook and example amount preprocessor

Co-authored-by: Thijs Louisse <Thijs.Louisse@ing.com>
This commit is contained in:
Joren Broekema 2021-02-15 11:58:02 +01:00 committed by Thijs Louisse
parent 3aa4783326
commit 13f808af57
12 changed files with 139 additions and 122 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/form-core': minor
---
Add preprocessor hook for completely preventing invalid input or doing other preprocessing steps before the parsing process of the FormatMixin.

View file

@ -0,0 +1,5 @@
---
'@lion/input-amount': minor
---
Add a digit-only preprocessor for input-amount. This will only allow users to enter characters that are digits or separator characters.

View file

@ -47,6 +47,25 @@ export const forceLocale = () => html`
> The separators are now flipped due to Dutch locale. On top of that, due to JOD currency, the minimum amount of decimals is 3 by default for this currency.
## Force digits as input
You can use the `preprocessAmount` preprocessor to disable users from inputting anything other than digits or separator characters.
This is not added by default, but you can add it yourself.
Separator characters include:
- ' ' (space)
- . (dot)
- , (comma)
```js preview-story
import { preprocessAmount } from '@lion/input-amount';
export const forceDigits = () => html`
<lion-input-amount label="Amount" .preprocessor=${preprocessAmount}></lion-input-amount>
`;
```
## Faulty prefilled
This example will show the error message by prefilling it with a faulty `modelValue`.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -127,7 +127,7 @@ A deserializer converts a value, for example one received from an API, to a `mod
> You need to call `el.deserializer(el.modelValue)` manually yourself.
```js preview-story
export const deSerializers = () => {
export const deserializers = () => {
const mySerializer = (modelValue, options) => {
return parseInt(modelValue, 8);
};
@ -148,6 +148,29 @@ export const deSerializers = () => {
};
```
### Preprocessors
A preprocessor converts the user input immediately on input.
This makes it useful for preventing invalid input or doing other processing tasks before the viewValue hits the parser.
In the example below, we do not allow you to write digits.
```js preview-story
export const preprocessors = () => {
const preprocess = (value) => {
return value.replace(/[0-9]/g, '');
}
return html`
<lion-input
label="Date Example"
help-text="Uses .preprocessor to prevent digits"
.preprocessor=${preprocess}
></lion-input>
<h-output .show="${['modelValue']}"></h-output>
`;
};
```
## Flow Diagrams
Below we show three flow diagrams to show the flow of formatting, serializing and parsing user input, with the example of a date input:

View file

@ -141,6 +141,14 @@ const FormatMixinImplementation = superclass =>
}
}
/**
* @param {string} v - the raw value from the <input> after keyUp/Down event
* @returns {string} preprocessedValue: the result of preprocessing for invalid input
*/
preprocessor(v) {
return v;
}
/**
* Converts formattedValue to modelValue
* For instance, a localized date to a Date Object
@ -225,6 +233,13 @@ const FormatMixinImplementation = superclass =>
this.__preventRecursiveTrigger = false;
}
/**
* @param {string} value
*/
__callPreprocessor(value) {
return this.preprocessor(value);
}
/**
* @param {string|undefined} value
* @return {?}
@ -328,11 +343,12 @@ const FormatMixinImplementation = superclass =>
/**
* Synchronization from `._inputNode.value` to `LionField` (flow [2])
* Downwards syncing should only happen for `LionField`.value changes from 'above'.
* This triggers _onModelValueChanged and connects user input
* to the parsing/formatting/serializing loop.
*/
_syncValueUpwards() {
// Downwards syncing should only happen for `LionField`.value changes from 'above'
// This triggers _onModelValueChanged and connects user input to the
// parsing/formatting/serializing loop
this.value = this.__callPreprocessor(this.value);
this.modelValue = this.__callParser(this.value);
}

View file

@ -285,13 +285,20 @@ export function runFormatMixinSuite(customConfig) {
}).to.not.throw();
});
describe('parsers/formatters/serializers', () => {
it('should call the parser|formatter|serializer provided by user', async () => {
describe('parsers/formatters/serializers/preprocessors', () => {
it('should call the parser|formatter|serializer|preprocessor provided by user', async () => {
const formatterSpy = sinon.spy(value => `foo: ${value}`);
const parserSpy = sinon.spy(value => value.replace('foo: ', ''));
const serializerSpy = sinon.spy(value => `[foo] ${value}`);
const preprocessorSpy = sinon.spy(value => value.replace('bar', ''));
const el = /** @type {FormatClass} */ (await fixture(html`
<${elem} .formatter=${formatterSpy} .parser=${parserSpy} .serializer=${serializerSpy} .modelValue=${'test'}>
<${elem}
.formatter=${formatterSpy}
.parser=${parserSpy}
.serializer=${serializerSpy}
.preprocessor=${preprocessorSpy}
.modelValue=${'test'}
>
<input slot="input">
</${elem}>
`));
@ -300,6 +307,8 @@ export function runFormatMixinSuite(customConfig) {
el.formattedValue = 'raw';
expect(parserSpy.called).to.equal(true);
el.dispatchEvent(new CustomEvent('user-input-changed'));
expect(preprocessorSpy.called).to.equal(true);
});
it('should have formatOptions available in formatter', async () => {
@ -353,7 +362,7 @@ export function runFormatMixinSuite(customConfig) {
expect(el.modelValue).to.equal('');
});
it.skip('will only call the formatter for valid values on `user-input-changed` ', async () => {
it('will only call the formatter for valid values on `user-input-changed` ', async () => {
const formatterSpy = sinon.spy(value => `foo: ${value}`);
const generatedModelValue = generateValueBasedOnType();
@ -401,6 +410,31 @@ export function runFormatMixinSuite(customConfig) {
expect(el.formattedValue).to.equal(`foo: ${generatedModelValue}`);
});
it('changes `.value` on keyup, before passing on to parser', async () => {
const val = generateValueBasedOnType({ viewValue: true }) || 'init-value';
if (typeof val !== 'string') {
return;
}
const toBeCorrectedVal = `${val}$`;
const preprocessorSpy = sinon.spy(v => v.replace(/\$$/g, ''));
const el = /** @type {FormatClass} */ (await fixture(html`
<${elem} .preprocessor=${preprocessorSpy}>
<input slot="input">
</${elem}>
`));
expect(preprocessorSpy.callCount).to.equal(1);
const parserSpy = sinon.spy(el, 'parser');
mimicUserInput(el, toBeCorrectedVal);
expect(preprocessorSpy.callCount).to.equal(2);
expect(parserSpy.lastCall.args[0]).to.equal(val);
expect(el._inputNode.value).to.equal(val);
});
});
describe('Unparseable values', () => {

View file

@ -1,3 +1,3 @@
# Lion Input
# Lion Input Amount
[=> See Source <=](../../docs/components/inputs/input-amount/overview.md)

View file

@ -1,3 +1,4 @@
export { LionInputAmount } from './src/LionInputAmount.js';
export { formatAmount } from './src/formatters.js';
export { parseAmount } from './src/parsers.js';
export { preprocessAmount } from './src/preprocessors.js';

View file

@ -0,0 +1,9 @@
/**
* Preprocesses by removing non-digits
* Allows space, comma and dot as separator characters
*
* @param {string} value Number to format
*/
export function preprocessAmount(value) {
return value.replace(/[^0-9,. ]/g, '');
}

View file

@ -0,0 +1,16 @@
import { expect } from '@open-wc/testing';
import { preprocessAmount } from '../src/preprocessors.js';
describe('preprocessAmount()', () => {
it('preprocesses numbers to filter out non-digits', async () => {
expect(preprocessAmount('123as@dh2^!#')).to.equal('1232');
});
it('does not filter out separator characters', async () => {
expect(preprocessAmount('123 456,78.90')).to.equal(
'123 456,78.90',
'Dot, comma and space should not be filtered out.',
);
});
});