From 13f808af57b0fd425bbe0fe131bef474c0574c83 Mon Sep 17 00:00:00 2001 From: Joren Broekema Date: Mon, 15 Feb 2021 11:58:02 +0100 Subject: [PATCH] feat: add preprocessor hook and example amount preprocessor Co-authored-by: Thijs Louisse --- .changeset/five-olives-pay.md | 5 + .changeset/popular-melons-search.md | 5 + .../inputs/input-amount/features.md | 19 +++ .../form/assets/FormatMixinDiagram-1.svg | 2 +- .../form/assets/FormatMixinDiagram-2.svg | 113 +----------------- .../systems/form/formatting-and-parsing.md | 25 +++- packages/form-core/src/FormatMixin.js | 22 +++- .../test-suites/FormatMixin.suite.js | 42 ++++++- packages/input-amount/README.md | 2 +- packages/input-amount/index.js | 1 + packages/input-amount/src/preprocessors.js | 9 ++ .../input-amount/test/preprocessors.test.js | 16 +++ 12 files changed, 139 insertions(+), 122 deletions(-) create mode 100644 .changeset/five-olives-pay.md create mode 100644 .changeset/popular-melons-search.md create mode 100644 packages/input-amount/src/preprocessors.js create mode 100644 packages/input-amount/test/preprocessors.test.js 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 @@ -FormatMixinDiagram1formattedValueUser(inputElement.input)valuemodelValueserializedValueparserserializerdeserializersyncon-blurdelegatesyncformatter5interact123445761. User changes the input 2. Native input syncs with lion-input-date value 3. Value is parsed to modelValue4. modelValue is formatted to formattedValue and modelValue is serialized to serializedValue 5. User blurs the field. Input gets validated by ValidateMixin6. formattedValue synced back to the value (preventRecursiveTrigger prevents infinite looping)7. Value is delegated back to native input value (what the user sees) “10/30/2010”“10/30/2010”[DateObject] “30-10-2010”“2010-10-30T00:00:00.000Z”“30-10-2010”“30-10-2010”“30-10-2010” @user-input-changed @model-value-changed ! [DateObject]5. On request: serializedValue is deserialized to modelValueUser flowcheckhasFeedbackFor.includes('error') +formattedValueUser(inputElement.input)valuemodelValueserializedValueparserserializerdeserializersyncpreprocesson-blurdelegatesyncformatter5interact123445761. User changes the input 2. Native input syncs with lion-input-date value and gets preprocessed3. Value is parsed to modelValue4. modelValue is formatted to formattedValue and modelValue is serialized to serializedValue 5. User blurs the field. Input gets validated by ValidateMixin6. formattedValue synced back to the value (preventRecursiveTrigger prevents infinite looping)7. Value is delegated back to native input value (what the user sees) “10/30/2010”“10/30/2010”[DateObject] “30-10-2010”“2010-10-30T00:00:00.000Z”“30-10-2010”“30-10-2010”“30-10-2010” @user-input-changed @model-value-changed @formatted-value-changed@serialized-value-changed ! [DateObject]User flow(if errorState = false) \ 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 @@ - - - - - - User flow 2(Unparseable) - User(inputElement.input) - value - modelValue - serializedValue - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - parser - - serializer - - - deserializer - - sync - delegate - sync - - - - - - interact - - 1 - 2 - 3 - 4 - 5 - 5 - - 4 - 1. User changes the input 2. Native input syncs with lion-input-date value 3. Value is parsed to modelValue but it is unparseable (parser returns undefined)4. modelValue.viewValue, which is the value prior to the parsing step, is synced back to the user. It is also serialized 5. Value is delegated back to native input value (what the user sees) - “10%30%2010”“10%30%2010”[Unparseable]“10%30%2010” “10%30%2010” - @user-input-changed @model-value-changed - ! - 5. On request: serializedValue is deserialized to modelValue, which will again be Unparseable - [Unparseable] - "{"type":"unparseable","viewValue":"10%30%2010"}" - - +User flow 2(Unparseable)User(inputElement.input)valuemodelValueserializedValueparserserializerdeserializerdelegatesyncinteract1345541. User changes the input 2. Native input syncs with lion-input-date value and gets preprocessed3. Value is parsed to modelValue but it is unparseable (parser returns undefined)4. modelValue.viewValue, which is the value prior to the parsing step, is synced back to the user. It is also serialized 5. Value is delegated back to native input value (what the user sees)“10%30%2010”“10%30%2010”[Unparseable]“10%30%2010” “10%30%2010” @user-input-changed @model-value-changed @serialized-value-changed !5. On request: serializedValue is deserialized to modelValue, which will again be Unparseable[Unparseable]"{"type":"unparseable","viewValue":"10%30%2010"}"syncpreprocess2 \ 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'} + > `)); @@ -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}> + + + `)); + + 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.', + ); + }); +});