diff --git a/.changeset/five-olives-pay.md b/.changeset/five-olives-pay.md
new file mode 100644
index 000000000..06deca3f3
--- /dev/null
+++ b/.changeset/five-olives-pay.md
@@ -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.
diff --git a/.changeset/popular-melons-search.md b/.changeset/popular-melons-search.md
new file mode 100644
index 000000000..59cd2881a
--- /dev/null
+++ b/.changeset/popular-melons-search.md
@@ -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.
diff --git a/docs/components/inputs/input-amount/features.md b/docs/components/inputs/input-amount/features.md
index 5c52e2436..a0c69d031 100644
--- a/docs/components/inputs/input-amount/features.md
+++ b/docs/components/inputs/input-amount/features.md
@@ -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`
+
+`;
+```
+
## Faulty prefilled
This example will show the error message by prefilling it with a faulty `modelValue`.
diff --git a/docs/docs/systems/form/assets/FormatMixinDiagram-1.svg b/docs/docs/systems/form/assets/FormatMixinDiagram-1.svg
index 8ec51fa01..a162f4423 100644
--- a/docs/docs/systems/form/assets/FormatMixinDiagram-1.svg
+++ b/docs/docs/systems/form/assets/FormatMixinDiagram-1.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/docs/docs/systems/form/assets/FormatMixinDiagram-2.svg b/docs/docs/systems/form/assets/FormatMixinDiagram-2.svg
index 1ed1b61ae..dc0797b84 100644
--- a/docs/docs/systems/form/assets/FormatMixinDiagram-2.svg
+++ b/docs/docs/systems/form/assets/FormatMixinDiagram-2.svg
@@ -1,112 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/docs/docs/systems/form/formatting-and-parsing.md b/docs/docs/systems/form/formatting-and-parsing.md
index 43d550511..99aab66fb 100644
--- a/docs/docs/systems/form/formatting-and-parsing.md
+++ b/docs/docs/systems/form/formatting-and-parsing.md
@@ -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`
+
+
+ `;
+};
+```
+
## 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:
diff --git a/packages/form-core/src/FormatMixin.js b/packages/form-core/src/FormatMixin.js
index 8c0b8b45f..e5269bd6d 100644
--- a/packages/form-core/src/FormatMixin.js
+++ b/packages/form-core/src/FormatMixin.js
@@ -141,6 +141,14 @@ const FormatMixinImplementation = superclass =>
}
}
+ /**
+ * @param {string} v - the raw value from the 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);
}
diff --git a/packages/form-core/test-suites/FormatMixin.suite.js b/packages/form-core/test-suites/FormatMixin.suite.js
index 0e9a5156e..4e1a238d7 100644
--- a/packages/form-core/test-suites/FormatMixin.suite.js
+++ b/packages/form-core/test-suites/FormatMixin.suite.js
@@ -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'}
+ >
${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}>
+
+ ${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', () => {
diff --git a/packages/input-amount/README.md b/packages/input-amount/README.md
index fc88f08ea..b5ff83b89 100644
--- a/packages/input-amount/README.md
+++ b/packages/input-amount/README.md
@@ -1,3 +1,3 @@
-# Lion Input
+# Lion Input Amount
[=> See Source <=](../../docs/components/inputs/input-amount/overview.md)
diff --git a/packages/input-amount/index.js b/packages/input-amount/index.js
index 301152b45..49392cafc 100644
--- a/packages/input-amount/index.js
+++ b/packages/input-amount/index.js
@@ -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';
diff --git a/packages/input-amount/src/preprocessors.js b/packages/input-amount/src/preprocessors.js
new file mode 100644
index 000000000..6a7290665
--- /dev/null
+++ b/packages/input-amount/src/preprocessors.js
@@ -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, '');
+}
diff --git a/packages/input-amount/test/preprocessors.test.js b/packages/input-amount/test/preprocessors.test.js
new file mode 100644
index 000000000..48f081d1e
--- /dev/null
+++ b/packages/input-amount/test/preprocessors.test.js
@@ -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.',
+ );
+ });
+});