Merge pull request #1213 from ing-bank/feat/preprocessor
feat: add preprocessor hook and example amount preprocessor
This commit is contained in:
commit
4dc169c65f
12 changed files with 139 additions and 122 deletions
5
.changeset/five-olives-pay.md
Normal file
5
.changeset/five-olives-pay.md
Normal 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.
|
||||||
5
.changeset/popular-melons-search.md
Normal file
5
.changeset/popular-melons-search.md
Normal 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.
|
||||||
|
|
@ -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.
|
> 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
|
## Faulty prefilled
|
||||||
|
|
||||||
This example will show the error message by prefilling it with a faulty `modelValue`.
|
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 |
|
|
@ -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.
|
> You need to call `el.deserializer(el.modelValue)` manually yourself.
|
||||||
|
|
||||||
```js preview-story
|
```js preview-story
|
||||||
export const deSerializers = () => {
|
export const deserializers = () => {
|
||||||
const mySerializer = (modelValue, options) => {
|
const mySerializer = (modelValue, options) => {
|
||||||
return parseInt(modelValue, 8);
|
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
|
## 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:
|
Below we show three flow diagrams to show the flow of formatting, serializing and parsing user input, with the example of a date input:
|
||||||
|
|
|
||||||
|
|
@ -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
|
* Converts formattedValue to modelValue
|
||||||
* For instance, a localized date to a Date Object
|
* For instance, a localized date to a Date Object
|
||||||
|
|
@ -225,6 +233,13 @@ const FormatMixinImplementation = superclass =>
|
||||||
this.__preventRecursiveTrigger = false;
|
this.__preventRecursiveTrigger = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
__callPreprocessor(value) {
|
||||||
|
return this.preprocessor(value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string|undefined} value
|
* @param {string|undefined} value
|
||||||
* @return {?}
|
* @return {?}
|
||||||
|
|
@ -328,11 +343,12 @@ const FormatMixinImplementation = superclass =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronization from `._inputNode.value` to `LionField` (flow [2])
|
* 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() {
|
_syncValueUpwards() {
|
||||||
// Downwards syncing should only happen for `LionField`.value changes from 'above'
|
this.value = this.__callPreprocessor(this.value);
|
||||||
// This triggers _onModelValueChanged and connects user input to the
|
|
||||||
// parsing/formatting/serializing loop
|
|
||||||
this.modelValue = this.__callParser(this.value);
|
this.modelValue = this.__callParser(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -285,13 +285,20 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
}).to.not.throw();
|
}).to.not.throw();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parsers/formatters/serializers', () => {
|
describe('parsers/formatters/serializers/preprocessors', () => {
|
||||||
it('should call the parser|formatter|serializer provided by user', async () => {
|
it('should call the parser|formatter|serializer|preprocessor provided by user', async () => {
|
||||||
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
const formatterSpy = sinon.spy(value => `foo: ${value}`);
|
||||||
const parserSpy = sinon.spy(value => value.replace('foo: ', ''));
|
const parserSpy = sinon.spy(value => value.replace('foo: ', ''));
|
||||||
const serializerSpy = sinon.spy(value => `[foo] ${value}`);
|
const serializerSpy = sinon.spy(value => `[foo] ${value}`);
|
||||||
|
const preprocessorSpy = sinon.spy(value => value.replace('bar', ''));
|
||||||
const el = /** @type {FormatClass} */ (await fixture(html`
|
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">
|
<input slot="input">
|
||||||
</${elem}>
|
</${elem}>
|
||||||
`));
|
`));
|
||||||
|
|
@ -300,6 +307,8 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
|
|
||||||
el.formattedValue = 'raw';
|
el.formattedValue = 'raw';
|
||||||
expect(parserSpy.called).to.equal(true);
|
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 () => {
|
it('should have formatOptions available in formatter', async () => {
|
||||||
|
|
@ -353,7 +362,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
expect(el.modelValue).to.equal('');
|
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 formatterSpy = sinon.spy(value => `foo: ${value}`);
|
||||||
|
|
||||||
const generatedModelValue = generateValueBasedOnType();
|
const generatedModelValue = generateValueBasedOnType();
|
||||||
|
|
@ -401,6 +410,31 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
|
|
||||||
expect(el.formattedValue).to.equal(`foo: ${generatedModelValue}`);
|
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', () => {
|
describe('Unparseable values', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# Lion Input
|
# Lion Input Amount
|
||||||
|
|
||||||
[=> See Source <=](../../docs/components/inputs/input-amount/overview.md)
|
[=> See Source <=](../../docs/components/inputs/input-amount/overview.md)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export { LionInputAmount } from './src/LionInputAmount.js';
|
export { LionInputAmount } from './src/LionInputAmount.js';
|
||||||
export { formatAmount } from './src/formatters.js';
|
export { formatAmount } from './src/formatters.js';
|
||||||
export { parseAmount } from './src/parsers.js';
|
export { parseAmount } from './src/parsers.js';
|
||||||
|
export { preprocessAmount } from './src/preprocessors.js';
|
||||||
|
|
|
||||||
9
packages/input-amount/src/preprocessors.js
Normal file
9
packages/input-amount/src/preprocessors.js
Normal 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, '');
|
||||||
|
}
|
||||||
16
packages/input-amount/test/preprocessors.test.js
Normal file
16
packages/input-amount/test/preprocessors.test.js
Normal 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.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue