From ceb39f966139981c9433109617810b5c1cc8526d Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Mon, 4 Mar 2024 14:30:44 +0300 Subject: [PATCH 1/2] feat: totally disable `Maskito` if nullable options are passed inside `@maskito/{angular,react,vue}` --- projects/angular/src/lib/maskito.directive.ts | 32 +++++++++++-------- projects/react/src/lib/useMaskito.ts | 7 ++-- projects/vue/src/lib/maskito.ts | 22 ++++++++----- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/projects/angular/src/lib/maskito.directive.ts b/projects/angular/src/lib/maskito.directive.ts index fb920df07..23560e3f5 100644 --- a/projects/angular/src/lib/maskito.directive.ts +++ b/projects/angular/src/lib/maskito.directive.ts @@ -11,7 +11,6 @@ import {DefaultValueAccessor} from '@angular/forms'; import { Maskito, MASKITO_DEFAULT_ELEMENT_PREDICATE, - MASKITO_DEFAULT_OPTIONS, MaskitoElementPredicate, MaskitoOptions, maskitoTransform, @@ -23,11 +22,11 @@ export class MaskitoDirective implements OnDestroy, OnChanges { private readonly ngZone = inject(NgZone); private maskedElement: Maskito | null = null; - @Input() - public maskito: MaskitoOptions | null = null; + @Input('maskito') + public options: MaskitoOptions | null = null; - @Input() - public maskitoElement: MaskitoElementPredicate = MASKITO_DEFAULT_ELEMENT_PREDICATE; + @Input('maskitoElement') + public elementPredicate: MaskitoElementPredicate = MASKITO_DEFAULT_ELEMENT_PREDICATE; constructor() { const accessor = inject(DefaultValueAccessor, {self: true, optional: true}); @@ -36,29 +35,34 @@ export class MaskitoDirective implements OnDestroy, OnChanges { const original = accessor.writeValue.bind(accessor); accessor.writeValue = (value: unknown) => { - original(maskitoTransform(String(value ?? ''), this.options)); + original( + this.options + ? maskitoTransform(String(value ?? ''), this.options) + : value, + ); }; } } - private get options(): MaskitoOptions { - return this.maskito ?? MASKITO_DEFAULT_OPTIONS; - } - public async ngOnChanges(): Promise { + const {elementPredicate, options} = this; + this.maskedElement?.destroy(); - const predicate = this.maskitoElement; - const predicateResult = await predicate(this.elementRef); + if (!options) { + return; + } + + const predicateResult = await elementPredicate(this.elementRef); - if (this.maskitoElement !== predicate) { + if (this.elementPredicate !== elementPredicate) { // Ignore the result of the predicate if the // maskito element has changed before the predicate was resolved. return; } this.ngZone.runOutsideAngular(() => { - this.maskedElement = new Maskito(predicateResult, this.options); + this.maskedElement = new Maskito(predicateResult, options); }); } diff --git a/projects/react/src/lib/useMaskito.ts b/projects/react/src/lib/useMaskito.ts index d042590a9..c2037f382 100644 --- a/projects/react/src/lib/useMaskito.ts +++ b/projects/react/src/lib/useMaskito.ts @@ -1,7 +1,6 @@ import { Maskito, MASKITO_DEFAULT_ELEMENT_PREDICATE, - MASKITO_DEFAULT_OPTIONS, MaskitoElementPredicate, MaskitoOptions, } from '@maskito/core'; @@ -28,10 +27,10 @@ function isThenable(x: PromiseLike | T): x is PromiseLike { * useMaskito({ options: { mask: /^.*$/ }, elementPredicate: () => e.querySelector('input') }) */ export const useMaskito = ({ - options = MASKITO_DEFAULT_OPTIONS, + options = null, elementPredicate = MASKITO_DEFAULT_ELEMENT_PREDICATE, }: { - options?: MaskitoOptions; + options?: MaskitoOptions | null; elementPredicate?: MaskitoElementPredicate; } = {}): RefCallback => { const [hostElement, setHostElement] = useState(null); @@ -70,7 +69,7 @@ export const useMaskito = ({ }, [hostElement, elementPredicate, latestPredicateRef]); useIsomorphicLayoutEffect(() => { - if (!element) { + if (!element || !options) { return; } diff --git a/projects/vue/src/lib/maskito.ts b/projects/vue/src/lib/maskito.ts index a163e4cb1..175207730 100644 --- a/projects/vue/src/lib/maskito.ts +++ b/projects/vue/src/lib/maskito.ts @@ -11,11 +11,13 @@ const predicates = new Map(); async function update( element: HTMLElement, - options: MaskitoOptions & { - elementPredicate?: MaskitoElementPredicate; - }, + options: + | (MaskitoOptions & { + elementPredicate?: MaskitoElementPredicate; + }) + | null, ): Promise { - const predicate = options.elementPredicate ?? MASKITO_DEFAULT_ELEMENT_PREDICATE; + const predicate = options?.elementPredicate ?? MASKITO_DEFAULT_ELEMENT_PREDICATE; predicates.set(element, predicate); @@ -26,14 +28,18 @@ async function update( } teardown.get(element)?.destroy(); - teardown.set(element, new Maskito(predicateResult, options)); + + if (options) { + teardown.set(element, new Maskito(predicateResult, options)); + } } export const maskito: ObjectDirective< HTMLElement, - MaskitoOptions & { - elementPredicate?: MaskitoElementPredicate; - } + | (MaskitoOptions & { + elementPredicate?: MaskitoElementPredicate; + }) + | null > = { unmounted: element => { teardown.get(element)?.destroy(); From b78132be77e1f88c671b9cd35fdb5a5e4c8fc618 Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Mon, 4 Mar 2024 15:22:07 +0300 Subject: [PATCH 2/2] chore(demo-integrations): add some Cypress component tests --- .../angular/disable-mask-on-null.cy.ts | 42 +++++++++++++++++++ .../src/tests/component-testing/utils.ts | 4 ++ 2 files changed, 46 insertions(+) create mode 100644 projects/demo-integrations/src/tests/component-testing/angular/disable-mask-on-null.cy.ts diff --git a/projects/demo-integrations/src/tests/component-testing/angular/disable-mask-on-null.cy.ts b/projects/demo-integrations/src/tests/component-testing/angular/disable-mask-on-null.cy.ts new file mode 100644 index 000000000..249a4e22a --- /dev/null +++ b/projects/demo-integrations/src/tests/component-testing/angular/disable-mask-on-null.cy.ts @@ -0,0 +1,42 @@ +import {MaskitoOptions} from '@maskito/core'; + +import {TestInput} from '../utils'; + +describe('@maskito/angular | Disable mask if null is passed as options', () => { + describe('type="email" is not compatible with Maskito (it does not have `setSelectionRange`)', () => { + it('should throw error if non-nullable options are passed', done => { + const maskitoOptions: MaskitoOptions = { + mask: [/[a-z]/, /[a-z]/, '@', /[a-z]/], + }; + + cy.mount(TestInput, { + componentProperties: { + type: 'email', + maskitoOptions, + }, + }); + + cy.on('uncaught:exception', ({message}) => { + expect(message).to.include( + "Failed to execute 'setSelectionRange' on 'HTMLInputElement': The input element's type ('email') does not support selection", + ); + + // ensure that an uncaught exception was thrown + done(); + }); + + cy.get('input').type('a12bc'); + }); + + it('should not throw any error is options are equal to `null`', () => { + cy.mount(TestInput, { + componentProperties: { + type: 'email', + maskitoOptions: null, + }, + }); + + cy.get('input').type('a12bc').should('have.value', 'a12bc'); + }); + }); +}); diff --git a/projects/demo-integrations/src/tests/component-testing/utils.ts b/projects/demo-integrations/src/tests/component-testing/utils.ts index 79f541a23..28eca8fcc 100644 --- a/projects/demo-integrations/src/tests/component-testing/utils.ts +++ b/projects/demo-integrations/src/tests/component-testing/utils.ts @@ -12,6 +12,7 @@ import { template: `