From 3098169a0cc8e9ad1aafc91e684ed94058a6cd02 Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Fri, 19 Jan 2024 11:23:40 +0300 Subject: [PATCH 1/2] fix(kit): `Placeholder` is not compatible with `maskitoEventHandler` + `focus`/`blur` events --- .../tests/recipes/placeholder/us-phone.cy.ts | 38 ++++++++++------ .../examples/1-cvc-code/component.ts | 6 +-- .../placeholder/examples/2-phone/component.ts | 22 +--------- .../placeholder/examples/2-phone/mask.ts | 29 +++++++++--- .../placeholder/examples/3-date/component.ts | 7 +-- .../processors/tests/with-placeholder.spec.ts | 44 +++++++++++++++---- .../src/lib/processors/with-placeholder.ts | 12 ++++- 7 files changed, 98 insertions(+), 60 deletions(-) diff --git a/projects/demo-integrations/src/tests/recipes/placeholder/us-phone.cy.ts b/projects/demo-integrations/src/tests/recipes/placeholder/us-phone.cy.ts index 8f2c537f7..c90ebc3ca 100644 --- a/projects/demo-integrations/src/tests/recipes/placeholder/us-phone.cy.ts +++ b/projects/demo-integrations/src/tests/recipes/placeholder/us-phone.cy.ts @@ -16,26 +16,28 @@ describe('Placeholder | US phone', () => { describe('basic typing (1 character per keydown)', () => { const tests = [ - // [Typed value, Masked value, caretIndex] - ['2', '+1 (2  ) ___-____', '+1 (2'.length], - ['21', '+1 (21 ) ___-____', '+1 (21'.length], - ['212', '+1 (212) ___-____', '+1 (212'.length], - ['2125', '+1 (212) 5__-____', '+1 (212) 5'.length], - ['21255', '+1 (212) 55_-____', '+1 (212) 55'.length], - ['212555', '+1 (212) 555-____', '+1 (212) 555'.length], - ['2125552', '+1 (212) 555-2___', '+1 (212) 555-2'.length], - ['21255523', '+1 (212) 555-23__', '+1 (212) 555-23'.length], - ['212555236', '+1 (212) 555-236_', '+1 (212) 555-236'.length], - ['2125552368', '+1 (212) 555-2368', '+1 (212) 555-2368'.length], + // [Typed value, Masked value, valueWithoutPlaceholder] + ['2', '+1 (2  ) ___-____', '+1 (2'], + ['21', '+1 (21 ) ___-____', '+1 (21'], + ['212', '+1 (212) ___-____', '+1 (212'], + ['2125', '+1 (212) 5__-____', '+1 (212) 5'], + ['21255', '+1 (212) 55_-____', '+1 (212) 55'], + ['212555', '+1 (212) 555-____', '+1 (212) 555'], + ['2125552', '+1 (212) 555-2___', '+1 (212) 555-2'], + ['21255523', '+1 (212) 555-23__', '+1 (212) 555-23'], + ['212555236', '+1 (212) 555-236_', '+1 (212) 555-236'], + ['2125552368', '+1 (212) 555-2368', '+1 (212) 555-2368'], ] as const; - tests.forEach(([typed, masked, caretIndex]) => { + tests.forEach(([typed, masked, valueWithoutPlaceholder]) => { it(`Type ${typed} => ${masked}`, () => { cy.get('@input') .type(typed) .should('have.value', masked) - .should('have.prop', 'selectionStart', caretIndex) - .should('have.prop', 'selectionEnd', caretIndex); + .should('have.prop', 'selectionStart', valueWithoutPlaceholder.length) + .should('have.prop', 'selectionEnd', valueWithoutPlaceholder.length) + .blur() + .should('have.value', valueWithoutPlaceholder); }); }); }); @@ -69,4 +71,12 @@ describe('Placeholder | US phone', () => { .should('have.prop', 'selectionStart', 0) .should('have.prop', 'selectionEnd', '+1'.length); }); + + it('Value contains only country code and placeholder => Blur => Value is empty', () => { + cy.get('@input') + .focus() + .should('have.value', '+1 (   ) ___-____') + .blur() + .should('have.value', ''); + }); }); diff --git a/projects/demo/src/pages/recipes/placeholder/examples/1-cvc-code/component.ts b/projects/demo/src/pages/recipes/placeholder/examples/1-cvc-code/component.ts index 27cb9f22b..325c90623 100644 --- a/projects/demo/src/pages/recipes/placeholder/examples/1-cvc-code/component.ts +++ b/projects/demo/src/pages/recipes/placeholder/examples/1-cvc-code/component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, Component, ElementRef, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {MaskitoDirective} from '@maskito/angular'; import {TuiTextfieldControllerModule} from '@taiga-ui/core'; @@ -23,7 +23,6 @@ import mask from './mask'; > Enter CVC code ; - readonly maskitoOptions = mask; value = 'xxx'; } diff --git a/projects/demo/src/pages/recipes/placeholder/examples/2-phone/component.ts b/projects/demo/src/pages/recipes/placeholder/examples/2-phone/component.ts index 0355a0707..18e0b0368 100644 --- a/projects/demo/src/pages/recipes/placeholder/examples/2-phone/component.ts +++ b/projects/demo/src/pages/recipes/placeholder/examples/2-phone/component.ts @@ -1,10 +1,10 @@ -import {ChangeDetectionStrategy, Component, ElementRef, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {MaskitoDirective} from '@maskito/angular'; import {TuiFlagPipeModule, TuiTextfieldControllerModule} from '@taiga-ui/core'; import {TuiInputModule} from '@taiga-ui/kit'; -import mask, {PLACEHOLDER, removePlaceholder} from './mask'; +import mask from './mask'; @Component({ standalone: true, @@ -24,12 +24,9 @@ import mask, {PLACEHOLDER, removePlaceholder} from './mask'; > Enter US phone number @@ -44,21 +41,6 @@ import mask, {PLACEHOLDER, removePlaceholder} from './mask'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class PlaceholderDocExample2 { - @ViewChild('inputRef', {read: ElementRef}) - inputRef!: ElementRef; - readonly maskitoOptions = mask; value = ''; - - onBlur(): void { - const cleanValue = removePlaceholder(this.value); - - this.value = cleanValue === '+1' ? '' : cleanValue; - } - - onFocus(): void { - const initialValue = this.value || '+1 ('; - - this.value = initialValue + PLACEHOLDER.slice(initialValue.length); - } } diff --git a/projects/demo/src/pages/recipes/placeholder/examples/2-phone/mask.ts b/projects/demo/src/pages/recipes/placeholder/examples/2-phone/mask.ts index d763999a5..1cd104fcb 100644 --- a/projects/demo/src/pages/recipes/placeholder/examples/2-phone/mask.ts +++ b/projects/demo/src/pages/recipes/placeholder/examples/2-phone/mask.ts @@ -1,13 +1,17 @@ -import {MaskitoOptions} from '@maskito/core'; -import {maskitoPrefixPostprocessorGenerator, maskitoWithPlaceholder} from '@maskito/kit'; +import {MaskitoOptions, maskitoUpdateElement} from '@maskito/core'; +import { + maskitoEventHandler, + maskitoPrefixPostprocessorGenerator, + maskitoWithPlaceholder, +} from '@maskito/kit'; /** * It is better to use en quad for placeholder characters * instead of simple space. * @see https://symbl.cc/en/2000 */ -export const PLACEHOLDER = '+  (   ) ___-____'; -export const { +const PLACEHOLDER = '+  (   ) ___-____'; +const { /** * Use this utility to remove placeholder characters * ___ @@ -30,7 +34,6 @@ export default { maskitoPrefixPostprocessorGenerator('+1'), ...placeholderOptions.postprocessors, ], - plugins, mask: [ '+', '1', @@ -50,4 +53,20 @@ export default { /\d/, /\d/, ], + plugins: [ + ...plugins, + maskitoEventHandler('focus', element => { + const initialValue = element.value || '+1 ('; + + maskitoUpdateElement( + element, + initialValue + PLACEHOLDER.slice(initialValue.length), + ); + }), + maskitoEventHandler('blur', element => { + const cleanValue = removePlaceholder(element.value); + + maskitoUpdateElement(element, cleanValue === '+1' ? '' : cleanValue); + }), + ], } as MaskitoOptions; diff --git a/projects/demo/src/pages/recipes/placeholder/examples/3-date/component.ts b/projects/demo/src/pages/recipes/placeholder/examples/3-date/component.ts index 63f02e0d6..8ca42c3e3 100644 --- a/projects/demo/src/pages/recipes/placeholder/examples/3-date/component.ts +++ b/projects/demo/src/pages/recipes/placeholder/examples/3-date/component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, Component, ElementRef, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {MaskitoDirective} from '@maskito/angular'; import {TuiTextfieldControllerModule} from '@taiga-ui/core'; @@ -23,7 +23,6 @@ import mask from './mask'; > Enter date ; - readonly maskitoOptions = mask; - value = ''; } diff --git a/projects/kit/src/lib/processors/tests/with-placeholder.spec.ts b/projects/kit/src/lib/processors/tests/with-placeholder.spec.ts index 5898e954e..d38a2e859 100644 --- a/projects/kit/src/lib/processors/tests/with-placeholder.spec.ts +++ b/projects/kit/src/lib/processors/tests/with-placeholder.spec.ts @@ -39,13 +39,12 @@ describe('maskitoWithPlaceholder("dd/mm/yyyy")', () => { describe('postprocessors', () => { describe('different initial element state (2nd argument of postprocessor)', () => { - [ - EMPTY_ELEMENT_STATE, - { - value: 'dd/mm/yyyy', - selection: [0, 0] as const, - }, - ].forEach(initialState => { + const ONLY_PLACEHOLDER_STATE = { + value: 'dd/mm/yyyy', + selection: [0, 0] as const, + }; + + [EMPTY_ELEMENT_STATE, ONLY_PLACEHOLDER_STATE].forEach(initialState => { const check = (valueBefore: string, valueAfter: string): void => { const {value} = postprocessor( { @@ -59,7 +58,6 @@ describe('maskitoWithPlaceholder("dd/mm/yyyy")', () => { }; describe(`Initial element value is "${initialState.value}"`, () => { - it('Empty', () => check('', 'dd/mm/yyyy')); it('1 => 1d/mm/yyyy', () => check('1', '1d/mm/yyyy')); it('16 => 16/mm/yyyy', () => check('16', '16/mm/yyyy')); it('16/0 => 16/0m/yyyy', () => check('16/0', '16/0m/yyyy')); @@ -71,6 +69,36 @@ describe('maskitoWithPlaceholder("dd/mm/yyyy")', () => { check('16/05/2023', '16/05/2023')); }); }); + + describe('postprocessor gets empty value', () => { + /** + * We can get this case only if textfield is updated programmatically. + * User can't erase symbols from already empty textfield. + */ + it('if initial state has empty value too => Empty', () => { + const {value} = postprocessor( + { + value: '', + selection: [0, 0] as const, + }, + EMPTY_ELEMENT_STATE, + ); + + expect(value).toBe(''); + }); + + it('initial value is not empty => placeholder', () => { + const {value} = postprocessor( + { + value: '', + selection: [0, 0] as const, + }, + ONLY_PLACEHOLDER_STATE, + ); + + expect(value).toBe('dd/mm/yyyy'); + }); + }); }); }); }); diff --git a/projects/kit/src/lib/processors/with-placeholder.ts b/projects/kit/src/lib/processors/with-placeholder.ts index 81127be6d..9b61e8a7c 100644 --- a/projects/kit/src/lib/processors/with-placeholder.ts +++ b/projects/kit/src/lib/processors/with-placeholder.ts @@ -63,8 +63,16 @@ export function maskitoWithPlaceholder( }, ], postprocessors: [ - ({value, selection}) => - focused || !focusedOnly + ({value, selection}, initialElementState) => + /** + * If `value` still equals to `initialElementState.value`, + * then it means that value is patched programmatically (from Maskito's plugin or externally). + * In this case, we don't want to mutate value and automatically add placeholder. + * ___ + * For example, developer wants to remove manually placeholder (+ do something else with value) on blur. + * Without this condition, placeholder will be unexpectedly added again. + */ + value !== initialElementState.value && (focused || !focusedOnly) ? { value: value + placeholder.slice(value.length), selection, From cd1a46eb2578c3f3b9401d35a6f6eb12d4de210b Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Fri, 19 Jan 2024 16:07:59 +0300 Subject: [PATCH 2/2] chore(demo): improve `Placeholder`' example `Phone` --- .../src/pages/recipes/placeholder/placeholder-doc.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/demo/src/pages/recipes/placeholder/placeholder-doc.component.ts b/projects/demo/src/pages/recipes/placeholder/placeholder-doc.component.ts index a2abeddcc..483e97980 100644 --- a/projects/demo/src/pages/recipes/placeholder/placeholder-doc.component.ts +++ b/projects/demo/src/pages/recipes/placeholder/placeholder-doc.component.ts @@ -36,7 +36,6 @@ export default class PlaceholderDocComponent { readonly phoneExample2: TuiDocExample = { [DocExamplePrimaryTab.MaskitoOptions]: import('./examples/2-phone/mask.ts?raw'), - [DocExamplePrimaryTab.Angular]: import('./examples/2-phone/component.ts?raw'), }; readonly dateExample3: TuiDocExample = {