From e098ca84ec522fd601846f844902903ab0bd53dd Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Wed, 24 Jul 2024 16:12:50 -0400 Subject: [PATCH 01/17] BugFix (ref: 1372) apply the same value to native element as what is applied to the form control when formControl.setValue is called. Log a warning and optionally return a validation error. --- .../src/lib/ngx-mask-applier.service.ts | 2 + .../ngx-mask-lib/src/lib/ngx-mask.config.ts | 5 + .../src/lib/ngx-mask.directive.ts | 61 ++++++++++- .../ngx-mask-lib/src/lib/ngx-mask.service.ts | 2 + .../src/test/formcontrol-setvalue.spec.ts | 103 ++++++++++++++++++ .../test/utils/test-component.component.ts | 2 + 6 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts index b3440ef0..c86cee05 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts @@ -36,6 +36,8 @@ export class NgxMaskApplierService { public validation: IConfig['validation'] = this._config.validation; + public setValueFailureBehavior: IConfig['setValueFailureBehavior'] = this._config.setValueFailureBehavior; + public separatorLimit: IConfig['separatorLimit'] = this._config.separatorLimit; public allowNegativeNumbers: IConfig['allowNegativeNumbers'] = diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts index 7d942204..f2d768f5 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts @@ -11,6 +11,9 @@ export interface OutputTransformFn { (value: string | number | undefined | null): unknown; } +export type SetValueFailureBehavior = 'ShowValidationError' | ''; +export const setValueValidationError = { 'masking issue': true }; + export interface IConfig { suffix: string; prefix: string; @@ -25,6 +28,7 @@ export interface IConfig { dropSpecialCharacters: boolean | string[] | readonly string[]; hiddenInput: boolean | undefined; validation: boolean; + setValueFailureBehavior: SetValueFailureBehavior; separatorLimit: string; apm: boolean; allowNegativeNumbers: boolean; @@ -66,6 +70,7 @@ export const initialConfig: IConfig = { separatorLimit: '', allowNegativeNumbers: false, validation: true, + setValueFailureBehavior: '', // eslint-disable-next-line @typescript-eslint/quotes specialCharacters: ['-', '/', '(', ')', '.', ':', ' ', '+', ',', '@', '[', ']', '"', "'"], leadZeroDateTime: false, diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index a03f34d6..65ed88ca 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -19,7 +19,7 @@ import { } from '@angular/forms'; import { CustomKeyboardEvent } from './custom-keyboard-event'; -import { IConfig, NGX_MASK_CONFIG, timeMasks, withoutValidation } from './ngx-mask.config'; +import { IConfig, NGX_MASK_CONFIG, timeMasks, withoutValidation, setValueValidationError } from './ngx-mask.config'; import { NgxMaskService } from './ngx-mask.service'; import { MaskExpression } from './ngx-mask-expression.enum'; @@ -262,6 +262,10 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } public validate({ value }: FormControl): ValidationErrors | null { + if (this._maskService.setValueFailureBehavior === 'ShowValidationError' && this._maskService.maskingIssue) { + return setValueValidationError; + } + if (!this._maskService.validation || !this._maskValue) { return null; } @@ -979,7 +983,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida this._maskService.formElementProperty = [ 'value', - this._maskService.applyMask(inputValue, this._maskService.maskExpression), + this.getNextValue(controlValue, inputValue), ]; // Let the service know we've finished writing value typeof this.inputTransformFn !== 'function' @@ -997,6 +1001,59 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } } + private get triggeredByFormControlSetValue(): boolean { + return !this._justPasted && !this._code && !this._isComposing; + } + + private getNextValue(controlValue: unknown, inputValue: string): string | boolean { + const maskedValue = this._maskService.applyMask(inputValue, this._maskService.maskExpression); + this._maskService.maskingIssue = !this.maskAppliedWithoutIssue(maskedValue, inputValue); + if (!this._maskService.maskingIssue) { + return maskedValue; + } + + console.warn(`The value ${controlValue} cannot be masked in the expected manner!`); + return controlValue as boolean | string; + } + + private maskAppliedWithoutIssue(maskedValue: string, inputValue: string): boolean { + // Issues can only arise from formControl.setValue() + if (!this.triggeredByFormControlSetValue) { + return true; + } + + if (!this.maskExpression) { + return true; + } + + // If the mask can be removed and the unmaskedValue matches the inputValue, + // then the mask was applied without issue. + const unmaskedValue = this._maskService.removeMask(maskedValue); + const maskKeptAllCharacters = unmaskedValue === inputValue; + if (maskKeptAllCharacters) { + return true; + } + + // If they don't match, + // then one explanation is that there was a loss of precision. + // If precision is lost, then the mask is irreversible, + // so we shouldn't expect an exact match when we remove it. + // In this case, let's verify that the lost precision was intended, and ignore if so. + const hasPrecision = this.maskExpression.indexOf("separator."); + const mayPossiblyLosePrecision = hasPrecision >= 0; + if (mayPossiblyLosePrecision) { + const maskExpressionPrecision = Number(this.maskExpression.split("separator.")[1]); + const unmaskedValuePrecision = unmaskedValue.split(".")[1]?.length; + const unmaskedPrecisionLossDueToMask = unmaskedValuePrecision === maskExpressionPrecision; + if (unmaskedPrecisionLossDueToMask) { + return true; + } + } + + // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? + return false; + } + public registerOnChange(fn: typeof this.onChange): void { this._maskService.onChange = this.onChange = fn; } diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts index ebe03d32..6a1c5a17 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts @@ -27,6 +27,8 @@ export class NgxMaskService extends NgxMaskApplierService { public triggerOnMaskChange = false; public _previousValue = ''; + + public maskingIssue = false; public _currentValue = ''; diff --git a/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts b/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts new file mode 100644 index 00000000..da293aba --- /dev/null +++ b/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts @@ -0,0 +1,103 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ReactiveFormsModule, ValidationErrors } from '@angular/forms'; +import { TestMaskComponent } from './utils/test-component.component'; +import { provideNgxMask } from '../lib/ngx-mask.providers'; +import { NgxMaskDirective } from '../lib/ngx-mask.directive'; +import { setValueValidationError } from 'ngx-mask'; + +describe('Directive: Mask (formControl.setValue)', () => { + let fixture: ComponentFixture; + let component: TestMaskComponent; + const mask = '(000) 000-0000'; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestMaskComponent], + imports: [ReactiveFormsModule, NgxMaskDirective], + providers: [provideNgxMask({ setValueFailureBehavior: 'ShowValidationError' })], + }); + fixture = TestBed.createComponent(TestMaskComponent); + component = fixture.componentInstance; + component.mask = mask; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it('(MATCH) should set the native element to the masked value, and set the form control to the unmasked value, which also emits the unmasked value', async () => { + const inputValue = '5555555555'; + const expectedFormControlValue = '5555555555'; + const expectedEmitedValues = '5555555555'; + const expectedNativeElementValue = '(555) 555-5555'; + const expectedConsoleWarnings = 0; + + const { + actualNativeElementValue, + actualFormControlValue, + actualEmitedValue, + consoleWarningCount, + errors + } = await setValue(inputValue); + + expect(actualNativeElementValue).toEqual(expectedNativeElementValue); + expect(actualEmitedValue).toEqual(expectedEmitedValues); + expect(actualFormControlValue).toEqual(expectedFormControlValue); + expect(consoleWarningCount).toEqual(expectedConsoleWarnings); + expect(errors).toBeNull(); + }); + + it('(NOT MATCH) should set the native element to the raw value, and set the form control to the raw value, which also emits the raw value, AND log a warning AND have a validation error', async () => { + const expectedNativeElementValue = 'AAA'; + const expectedFormControlValue = 'AAA'; + const expectedEmitedValue = 'AAA'; + const expectedConsoleWarnings = 1; + + const inputValue = 'AAA'; + const { + actualNativeElementValue, + actualFormControlValue, + actualEmitedValue, + consoleWarningCount, + errors + } = await setValue(inputValue); + + expect(actualNativeElementValue).toEqual(expectedNativeElementValue); + expect(actualEmitedValue).toEqual(expectedEmitedValue); + expect(actualFormControlValue).toEqual(expectedFormControlValue); + expect(consoleWarningCount).toEqual(expectedConsoleWarnings); + expect(errors).toEqual(setValueValidationError); + }); + + const setValue = async (value: string | number): Promise<{ + actualNativeElementValue: string | number, + actualFormControlValue: string | number, + actualEmitedValue: string | null, + consoleWarningCount: number, + errors: ValidationErrors | null + }> => { + const warnSpy = spyOn(console, 'warn'); + + let actualEmitedValue = null; + component.form.valueChanges.subscribe(emitedValue => { + actualEmitedValue = emitedValue; + }); + component.form.setValue(value); + fixture.detectChanges(); + await fixture.whenStable(); + + const actualNativeElementValue = fixture.debugElement.query(By.css('input')).nativeElement.value; + const actualFormControlValue = component.form.getRawValue(); + const consoleWarningCount = warnSpy.calls.count(); + const errors = component.form.errors; + return { + actualNativeElementValue, + actualFormControlValue, + actualEmitedValue, + consoleWarningCount, + errors + } + } +}); diff --git a/projects/ngx-mask-lib/src/test/utils/test-component.component.ts b/projects/ngx-mask-lib/src/test/utils/test-component.component.ts index 9cdcacc8..8b253537 100644 --- a/projects/ngx-mask-lib/src/test/utils/test-component.component.ts +++ b/projects/ngx-mask-lib/src/test/utils/test-component.component.ts @@ -74,6 +74,8 @@ export class TestMaskComponent { public validation: IConfig['validation'] | undefined; + public setValueFailureBehavior: IConfig['setValueFailureBehavior'] | undefined; + public apm: IConfig['apm'] | undefined; public inputTransformFn: IConfig['inputTransformFn'] | undefined; From ed6cbb754f556c57614226e895b619a909c9720a Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Thu, 25 Jul 2024 11:30:53 -0400 Subject: [PATCH 02/17] Fix(1372) removing configurability, because it seems that making something configurable impacts many projects (and not just this one). Can add later, if it's worth the complexity. updating maskAppliedWithoutIssue to also accomodate dropSpecialCharacters --- .../src/lib/ngx-mask-applier.service.ts | 2 -- .../ngx-mask-lib/src/lib/ngx-mask.config.ts | 3 --- .../src/lib/ngx-mask.directive.ts | 24 ++++++++++++++++++- .../src/test/formcontrol-setvalue.spec.ts | 2 +- .../test/utils/test-component.component.ts | 2 -- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts index 6914a096..b466292f 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts @@ -36,8 +36,6 @@ export class NgxMaskApplierService { public validation: IConfig['validation'] = this._config.validation; - public setValueFailureBehavior: IConfig['setValueFailureBehavior'] = this._config.setValueFailureBehavior; - public separatorLimit: IConfig['separatorLimit'] = this._config.separatorLimit; public allowNegativeNumbers: IConfig['allowNegativeNumbers'] = diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts index 9e744055..a3abe932 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts @@ -5,7 +5,6 @@ export type InputTransformFn = (value: unknown) => string | number; export type OutputTransformFn = (value: string | number | undefined | null) => unknown; -export type SetValueFailureBehavior = 'ShowValidationError' | ''; export const setValueValidationError = { 'masking issue': true }; export interface IConfig { @@ -22,7 +21,6 @@ export interface IConfig { dropSpecialCharacters: boolean | string[] | readonly string[]; hiddenInput: boolean | undefined; validation: boolean; - setValueFailureBehavior: SetValueFailureBehavior; separatorLimit: string; apm: boolean; allowNegativeNumbers: boolean; @@ -63,7 +61,6 @@ export const initialConfig: IConfig = { separatorLimit: '', allowNegativeNumbers: false, validation: true, - setValueFailureBehavior: '', specialCharacters: ['-', '/', '(', ')', '.', ':', ' ', '+', ',', '@', '[', ']', '"', "'"], leadZeroDateTime: false, diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 32fb7364..632b5b2b 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -264,7 +264,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } public validate({ value }: FormControl): ValidationErrors | null { - if (this._maskService.setValueFailureBehavior === 'ShowValidationError' && this._maskService.maskingIssue) { + if (this._maskService.maskingIssue) { return setValueValidationError; } @@ -1023,6 +1023,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } private maskAppliedWithoutIssue(maskedValue: string, inputValue: string): boolean { + debugger; // Issues can only arise from formControl.setValue() if (!this.triggeredByFormControlSetValue) { return true; @@ -1056,6 +1057,27 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } } + // If they don't match, + // then another explanation is that special characters were dropped. + // Again, that would make the mask irreversible, + // so we shouldn't expect an exact match when we remove it. + // In this case, let's verify that the lost characters were intended, and ignore if so. + if (this._maskService.dropSpecialCharacters) { + const specialCharacters = Array.isArray(this._maskService.dropSpecialCharacters) + ? this._maskService.dropSpecialCharacters.concat(this._maskService.specialCharacters) + : this._maskService.specialCharacters; + let inputWithoutSpecialCharacters = inputValue; + specialCharacters.forEach(sc => inputWithoutSpecialCharacters = inputWithoutSpecialCharacters.replace(sc, "")); + const unmaskedCharacterLossDueToDroppedSpecialChars = unmaskedValue === inputWithoutSpecialCharacters; + if (unmaskedCharacterLossDueToDroppedSpecialChars) { + return true; + } else { + debugger; + } + } else { + debugger; + } + // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? return false; } diff --git a/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts b/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts index da293aba..b4a9db2e 100644 --- a/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts +++ b/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts @@ -15,7 +15,7 @@ describe('Directive: Mask (formControl.setValue)', () => { TestBed.configureTestingModule({ declarations: [TestMaskComponent], imports: [ReactiveFormsModule, NgxMaskDirective], - providers: [provideNgxMask({ setValueFailureBehavior: 'ShowValidationError' })], + providers: [provideNgxMask()], }); fixture = TestBed.createComponent(TestMaskComponent); component = fixture.componentInstance; diff --git a/projects/ngx-mask-lib/src/test/utils/test-component.component.ts b/projects/ngx-mask-lib/src/test/utils/test-component.component.ts index 8b253537..9cdcacc8 100644 --- a/projects/ngx-mask-lib/src/test/utils/test-component.component.ts +++ b/projects/ngx-mask-lib/src/test/utils/test-component.component.ts @@ -74,8 +74,6 @@ export class TestMaskComponent { public validation: IConfig['validation'] | undefined; - public setValueFailureBehavior: IConfig['setValueFailureBehavior'] | undefined; - public apm: IConfig['apm'] | undefined; public inputTransformFn: IConfig['inputTransformFn'] | undefined; From 267f7005015487978f6fa8b3b5745b68e4789ffc Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Thu, 25 Jul 2024 11:36:41 -0400 Subject: [PATCH 03/17] rm debuggers --- projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 632b5b2b..7c4726ec 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1023,7 +1023,6 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } private maskAppliedWithoutIssue(maskedValue: string, inputValue: string): boolean { - debugger; // Issues can only arise from formControl.setValue() if (!this.triggeredByFormControlSetValue) { return true; @@ -1071,11 +1070,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida const unmaskedCharacterLossDueToDroppedSpecialChars = unmaskedValue === inputWithoutSpecialCharacters; if (unmaskedCharacterLossDueToDroppedSpecialChars) { return true; - } else { - debugger; } - } else { - debugger; } // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? From 42a76ff421d685edbf478b3c7bca78da33ce1e4c Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Thu, 25 Jul 2024 16:53:51 -0400 Subject: [PATCH 04/17] use maskExpression on _maskService. Do not throw special ValidationErrors. Do not attempt to interpret !_justPased and !_code as setValueTriggered --- .../ngx-mask-lib/src/lib/ngx-mask.config.ts | 2 - .../src/lib/ngx-mask.directive.ts | 29 ++++--------- .../src/test/formcontrol-setvalue.spec.ts | 43 +++++++++---------- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts index a3abe932..5bf14e3f 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.config.ts @@ -5,8 +5,6 @@ export type InputTransformFn = (value: unknown) => string | number; export type OutputTransformFn = (value: string | number | undefined | null) => unknown; -export const setValueValidationError = { 'masking issue': true }; - export interface IConfig { suffix: string; prefix: string; diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 7c4726ec..a8d07629 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -19,7 +19,7 @@ import { } from '@angular/forms'; import { CustomKeyboardEvent } from './custom-keyboard-event'; -import { IConfig, NGX_MASK_CONFIG, timeMasks, withoutValidation, setValueValidationError } from './ngx-mask.config'; +import { IConfig, NGX_MASK_CONFIG, timeMasks, withoutValidation } from './ngx-mask.config'; import { NgxMaskService } from './ngx-mask.service'; import { MaskExpression } from './ngx-mask-expression.enum'; @@ -264,10 +264,6 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } public validate({ value }: FormControl): ValidationErrors | null { - if (this._maskService.maskingIssue) { - return setValueValidationError; - } - if (!this._maskService.validation || !this._maskValue) { return null; } @@ -1007,28 +1003,19 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } } - private get triggeredByFormControlSetValue(): boolean { - return !this._justPasted && !this._code && !this._isComposing; - } - private getNextValue(controlValue: unknown, inputValue: string): string | boolean { const maskedValue = this._maskService.applyMask(inputValue, this._maskService.maskExpression); this._maskService.maskingIssue = !this.maskAppliedWithoutIssue(maskedValue, inputValue); if (!this._maskService.maskingIssue) { return maskedValue; } - - console.warn(`The value ${controlValue} cannot be masked in the expected manner!`); + + console.warn(`Unexpected error applying mask: ${this._maskService.maskExpression} to value: ${controlValue}`); return controlValue as boolean | string; } private maskAppliedWithoutIssue(maskedValue: string, inputValue: string): boolean { - // Issues can only arise from formControl.setValue() - if (!this.triggeredByFormControlSetValue) { - return true; - } - - if (!this.maskExpression) { + if (!this._maskService.maskExpression) { return true; } @@ -1045,10 +1032,10 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida // If precision is lost, then the mask is irreversible, // so we shouldn't expect an exact match when we remove it. // In this case, let's verify that the lost precision was intended, and ignore if so. - const hasPrecision = this.maskExpression.indexOf("separator."); + const hasPrecision = this._maskService.maskExpression.indexOf("separator."); const mayPossiblyLosePrecision = hasPrecision >= 0; if (mayPossiblyLosePrecision) { - const maskExpressionPrecision = Number(this.maskExpression.split("separator.")[1]); + const maskExpressionPrecision = Number(this._maskService.maskExpression.split("separator.")[1]); const unmaskedValuePrecision = unmaskedValue.split(".")[1]?.length; const unmaskedPrecisionLossDueToMask = unmaskedValuePrecision === maskExpressionPrecision; if (unmaskedPrecisionLossDueToMask) { @@ -1057,7 +1044,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } // If they don't match, - // then another explanation is that special characters were dropped. + // then another explanation is that special characters were lost. // Again, that would make the mask irreversible, // so we shouldn't expect an exact match when we remove it. // In this case, let's verify that the lost characters were intended, and ignore if so. @@ -1072,7 +1059,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return true; } } - + // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? return false; } diff --git a/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts b/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts index b4a9db2e..ad84c4ef 100644 --- a/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts +++ b/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts @@ -4,7 +4,6 @@ import { ReactiveFormsModule, ValidationErrors } from '@angular/forms'; import { TestMaskComponent } from './utils/test-component.component'; import { provideNgxMask } from '../lib/ngx-mask.providers'; import { NgxMaskDirective } from '../lib/ngx-mask.directive'; -import { setValueValidationError } from 'ngx-mask'; describe('Directive: Mask (formControl.setValue)', () => { let fixture: ComponentFixture; @@ -27,34 +26,34 @@ describe('Directive: Mask (formControl.setValue)', () => { fixture.destroy(); }); - it('(MATCH) should set the native element to the masked value, and set the form control to the unmasked value, which also emits the unmasked value', async () => { - const inputValue = '5555555555'; - const expectedFormControlValue = '5555555555'; - const expectedEmitedValues = '5555555555'; - const expectedNativeElementValue = '(555) 555-5555'; - const expectedConsoleWarnings = 0; + // it('(MATCH) should set the native element to the masked value, and set the form control to the unmasked value, which also emits the unmasked value', async () => { + // const inputValue = '5555555555'; + // const expectedFormControlValue = '5555555555'; + // const expectedEmitedValues = '5555555555'; + // const expectedNativeElementValue = '(555) 555-5555'; + // const expectedConsoleWarnings = 0; - const { - actualNativeElementValue, - actualFormControlValue, - actualEmitedValue, - consoleWarningCount, - errors - } = await setValue(inputValue); + // const { + // actualNativeElementValue, + // actualFormControlValue, + // actualEmitedValue, + // consoleWarningCount, + // errors + // } = await setValue(inputValue); - expect(actualNativeElementValue).toEqual(expectedNativeElementValue); - expect(actualEmitedValue).toEqual(expectedEmitedValues); - expect(actualFormControlValue).toEqual(expectedFormControlValue); - expect(consoleWarningCount).toEqual(expectedConsoleWarnings); - expect(errors).toBeNull(); - }); + // expect(actualNativeElementValue).toEqual(expectedNativeElementValue); + // expect(actualEmitedValue).toEqual(expectedEmitedValues); + // expect(actualFormControlValue).toEqual(expectedFormControlValue); + // expect(consoleWarningCount).toEqual(expectedConsoleWarnings); + // expect(errors).toBeNull(); + // }); it('(NOT MATCH) should set the native element to the raw value, and set the form control to the raw value, which also emits the raw value, AND log a warning AND have a validation error', async () => { const expectedNativeElementValue = 'AAA'; const expectedFormControlValue = 'AAA'; const expectedEmitedValue = 'AAA'; const expectedConsoleWarnings = 1; - + const inputValue = 'AAA'; const { actualNativeElementValue, @@ -68,7 +67,7 @@ describe('Directive: Mask (formControl.setValue)', () => { expect(actualEmitedValue).toEqual(expectedEmitedValue); expect(actualFormControlValue).toEqual(expectedFormControlValue); expect(consoleWarningCount).toEqual(expectedConsoleWarnings); - expect(errors).toEqual(setValueValidationError); + expect(errors).not.toBeNull(); }); const setValue = async (value: string | number): Promise<{ From 075626e2718e8b329d33e5cf29ee5b0dd16e8ba2 Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Thu, 25 Jul 2024 20:31:19 -0400 Subject: [PATCH 05/17] accounting for more possibilities in unmasking --- .../src/lib/ngx-mask.directive.ts | 121 ++++++++++++------ 1 file changed, 81 insertions(+), 40 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index a8d07629..a254cc1c 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1015,53 +1015,94 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } private maskAppliedWithoutIssue(maskedValue: string, inputValue: string): boolean { - if (!this._maskService.maskExpression) { - return true; - } - - // If the mask can be removed and the unmaskedValue matches the inputValue, - // then the mask was applied without issue. - const unmaskedValue = this._maskService.removeMask(maskedValue); - const maskKeptAllCharacters = unmaskedValue === inputValue; - if (maskKeptAllCharacters) { - return true; - } + try { + if (!this._maskService.maskExpression) { + return true; + } + + const unmaskedValue = this._maskService.removeMask(maskedValue); + if (this._maskService.dropSpecialCharacters) { + inputValue = this.removeSpecialCharactersFrom(inputValue); + } - // If they don't match, - // then one explanation is that there was a loss of precision. - // If precision is lost, then the mask is irreversible, - // so we shouldn't expect an exact match when we remove it. - // In this case, let's verify that the lost precision was intended, and ignore if so. - const hasPrecision = this._maskService.maskExpression.indexOf("separator."); - const mayPossiblyLosePrecision = hasPrecision >= 0; - if (mayPossiblyLosePrecision) { - const maskExpressionPrecision = Number(this._maskService.maskExpression.split("separator.")[1]); - const unmaskedValuePrecision = unmaskedValue.split(".")[1]?.length; - const unmaskedPrecisionLossDueToMask = unmaskedValuePrecision === maskExpressionPrecision; - if (unmaskedPrecisionLossDueToMask) { + if (this._maskService.hiddenInput) { + for (let i = 0; i < unmaskedValue.length; i++) { + const charAt = unmaskedValue.charAt(i); + const isHidden = charAt === MaskExpression.SYMBOL_STAR; + if (isHidden) { + const part_1 = inputValue.substring(0, i); + const part_2 = MaskExpression.SYMBOL_STAR; + const part_3 = inputValue.substring(i + 1) + inputValue = `${part_1}${part_2}${part_3}` + } + } + } + + if (unmaskedValue === inputValue) { return true; } - } - // If they don't match, - // then another explanation is that special characters were lost. - // Again, that would make the mask irreversible, - // so we shouldn't expect an exact match when we remove it. - // In this case, let's verify that the lost characters were intended, and ignore if so. - if (this._maskService.dropSpecialCharacters) { - const specialCharacters = Array.isArray(this._maskService.dropSpecialCharacters) - ? this._maskService.dropSpecialCharacters.concat(this._maskService.specialCharacters) - : this._maskService.specialCharacters; - let inputWithoutSpecialCharacters = inputValue; - specialCharacters.forEach(sc => inputWithoutSpecialCharacters = inputWithoutSpecialCharacters.replace(sc, "")); - const unmaskedCharacterLossDueToDroppedSpecialChars = unmaskedValue === inputWithoutSpecialCharacters; - if (unmaskedCharacterLossDueToDroppedSpecialChars) { + // If they don't match, + // then one explanation is that there was a loss of precision. + // If precision is lost, then the mask is irreversible, + // so we shouldn't expect an exact match when we remove it. + // In this case, let's verify that the lost precision was intended, and ignore if so. + const hasPrecision = this._maskService.maskExpression.indexOf(MaskExpression.SEPARATOR + "."); + const mayPossiblyLosePrecision = hasPrecision >= 0; + if (mayPossiblyLosePrecision) { + const maskExpressionPrecision = Number(this._maskService.maskExpression.split("separator.")[1]); + const decimalMarkers = Array.isArray(this._maskService.decimalMarker) ? this._maskService.decimalMarker : [ this._maskService.decimalMarker ]; + const unmaskedPrecisionLossDueToMask = decimalMarkers.some((dm) => { + const split = unmaskedValue.split(dm); + const unmaskedValuePrecision = split[split.length - 1]?.length; + const unmaskedPrecisionLossDueToMask = unmaskedValuePrecision === maskExpressionPrecision; + return unmaskedPrecisionLossDueToMask; + }); + if (unmaskedPrecisionLossDueToMask) { + return true; + } + + const scientificNotation = inputValue.indexOf("e") > 0; + if (scientificNotation) { + const power = inputValue.split("e")[1]; + if (power && unmaskedValue.endsWith(power)) { + return true; + } + } + } + + // IS THIS A BUG? + // removeMask() is not removing the thousandth separator + const unmaskedWithoutThousandth = this.replaceEachCharacterWith(unmaskedValue, this._maskService.thousandSeparator, MaskExpression.EMPTY_STRING); + if (unmaskedWithoutThousandth === inputValue) { return true; } + + // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? + return false; + } catch(err) { + return true; } - - // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? - return false; + } + + private removeSpecialCharactersFrom(inputValue: string): string { + const specialCharacters = Array.isArray(this._maskService.dropSpecialCharacters) + ? this._maskService.dropSpecialCharacters.concat(this._maskService.specialCharacters) + : this._maskService.specialCharacters; + let result = inputValue; + specialCharacters.forEach(sc => { + result = this.replaceEachCharacterWith(result, sc, MaskExpression.EMPTY_STRING); + }); + + return result; + } + + private replaceEachCharacterWith(result: string, replace: string, replaceWith: string): string { + while (result.indexOf(replace) >= 0) { + result = result.replace(replace, replaceWith); + } + + return result; } public registerOnChange(fn: typeof this.onChange): void { From 10584db038eb89e228b9dc5f846976c82de72920 Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Thu, 25 Jul 2024 20:40:19 -0400 Subject: [PATCH 06/17] uncomment setValue test --- .../src/lib/ngx-mask.directive.ts | 6 +-- .../src/test/formcontrol-setvalue.spec.ts | 38 +++++++++---------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index a254cc1c..1c1f812e 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1042,11 +1042,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return true; } - // If they don't match, - // then one explanation is that there was a loss of precision. - // If precision is lost, then the mask is irreversible, - // so we shouldn't expect an exact match when we remove it. - // In this case, let's verify that the lost precision was intended, and ignore if so. + // They may still not match due to lost precision const hasPrecision = this._maskService.maskExpression.indexOf(MaskExpression.SEPARATOR + "."); const mayPossiblyLosePrecision = hasPrecision >= 0; if (mayPossiblyLosePrecision) { diff --git a/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts b/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts index ad84c4ef..e165aa74 100644 --- a/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts +++ b/projects/ngx-mask-lib/src/test/formcontrol-setvalue.spec.ts @@ -26,27 +26,27 @@ describe('Directive: Mask (formControl.setValue)', () => { fixture.destroy(); }); - // it('(MATCH) should set the native element to the masked value, and set the form control to the unmasked value, which also emits the unmasked value', async () => { - // const inputValue = '5555555555'; - // const expectedFormControlValue = '5555555555'; - // const expectedEmitedValues = '5555555555'; - // const expectedNativeElementValue = '(555) 555-5555'; - // const expectedConsoleWarnings = 0; + it('(MATCH) should set the native element to the masked value, and set the form control to the unmasked value, which also emits the unmasked value', async () => { + const inputValue = '5555555555'; + const expectedFormControlValue = '5555555555'; + const expectedEmitedValues = '5555555555'; + const expectedNativeElementValue = '(555) 555-5555'; + const expectedConsoleWarnings = 0; - // const { - // actualNativeElementValue, - // actualFormControlValue, - // actualEmitedValue, - // consoleWarningCount, - // errors - // } = await setValue(inputValue); + const { + actualNativeElementValue, + actualFormControlValue, + actualEmitedValue, + consoleWarningCount, + errors + } = await setValue(inputValue); - // expect(actualNativeElementValue).toEqual(expectedNativeElementValue); - // expect(actualEmitedValue).toEqual(expectedEmitedValues); - // expect(actualFormControlValue).toEqual(expectedFormControlValue); - // expect(consoleWarningCount).toEqual(expectedConsoleWarnings); - // expect(errors).toBeNull(); - // }); + expect(actualNativeElementValue).toEqual(expectedNativeElementValue); + expect(actualEmitedValue).toEqual(expectedEmitedValues); + expect(actualFormControlValue).toEqual(expectedFormControlValue); + expect(consoleWarningCount).toEqual(expectedConsoleWarnings); + expect(errors).toBeNull(); + }); it('(NOT MATCH) should set the native element to the raw value, and set the form control to the raw value, which also emits the raw value, AND log a warning AND have a validation error', async () => { const expectedNativeElementValue = 'AAA'; From 49a06c6c9208ae311136ed26f637c7481a7f063b Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Thu, 25 Jul 2024 20:59:12 -0400 Subject: [PATCH 07/17] maskingIssue does not need module-level scope --- projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts | 6 +++--- projects/ngx-mask-lib/src/lib/ngx-mask.service.ts | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 1c1f812e..027de71c 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1005,8 +1005,8 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida private getNextValue(controlValue: unknown, inputValue: string): string | boolean { const maskedValue = this._maskService.applyMask(inputValue, this._maskService.maskExpression); - this._maskService.maskingIssue = !this.maskAppliedWithoutIssue(maskedValue, inputValue); - if (!this._maskService.maskingIssue) { + const maskingFault = !this.maskAppliedWithoutFault(maskedValue, inputValue); + if (maskingFault) { return maskedValue; } @@ -1014,7 +1014,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return controlValue as boolean | string; } - private maskAppliedWithoutIssue(maskedValue: string, inputValue: string): boolean { + private maskAppliedWithoutFault(maskedValue: string, inputValue: string): boolean { try { if (!this._maskService.maskExpression) { return true; diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts index 57d21a93..e2df7eed 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts @@ -27,9 +27,6 @@ export class NgxMaskService extends NgxMaskApplierService { public triggerOnMaskChange = false; public _previousValue = ''; - - public maskingIssue = false; - public _currentValue = ''; private _emitValue = false; From 254abc01620dbc9bd2d0a0edee47e9787441bcc0 Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Thu, 25 Jul 2024 21:04:24 -0400 Subject: [PATCH 08/17] fault, separator --- projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts | 9 +++------ projects/ngx-mask-lib/src/lib/ngx-mask.service.ts | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 027de71c..d9d3c9d7 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1010,16 +1010,12 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return maskedValue; } - console.warn(`Unexpected error applying mask: ${this._maskService.maskExpression} to value: ${controlValue}`); + console.warn(`Unexpected fault applying mask: ${this._maskService.maskExpression} to value: ${controlValue}`); return controlValue as boolean | string; } private maskAppliedWithoutFault(maskedValue: string, inputValue: string): boolean { try { - if (!this._maskService.maskExpression) { - return true; - } - const unmaskedValue = this._maskService.removeMask(maskedValue); if (this._maskService.dropSpecialCharacters) { inputValue = this.removeSpecialCharactersFrom(inputValue); @@ -1046,7 +1042,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida const hasPrecision = this._maskService.maskExpression.indexOf(MaskExpression.SEPARATOR + "."); const mayPossiblyLosePrecision = hasPrecision >= 0; if (mayPossiblyLosePrecision) { - const maskExpressionPrecision = Number(this._maskService.maskExpression.split("separator.")[1]); + const maskExpressionPrecision = Number(this._maskService.maskExpression.split(MaskExpression.SEPARATOR + ".")[1]); const decimalMarkers = Array.isArray(this._maskService.decimalMarker) ? this._maskService.decimalMarker : [ this._maskService.decimalMarker ]; const unmaskedPrecisionLossDueToMask = decimalMarkers.some((dm) => { const split = unmaskedValue.split(dm); @@ -1075,6 +1071,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? + debugger; return false; } catch(err) { return true; diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts index e2df7eed..4fa8ca15 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts @@ -27,6 +27,7 @@ export class NgxMaskService extends NgxMaskApplierService { public triggerOnMaskChange = false; public _previousValue = ''; + public _currentValue = ''; private _emitValue = false; From f29d6b47cf0155ef968f6b84e19aac5732227b43 Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Thu, 25 Jul 2024 21:04:50 -0400 Subject: [PATCH 09/17] whitespace --- projects/ngx-mask-lib/src/lib/ngx-mask.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts index 4fa8ca15..55fa078a 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts @@ -27,7 +27,7 @@ export class NgxMaskService extends NgxMaskApplierService { public triggerOnMaskChange = false; public _previousValue = ''; - + public _currentValue = ''; private _emitValue = false; From f4134e31bf810227ccfc220b04417df8edb271b5 Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Fri, 26 Jul 2024 07:06:52 -0400 Subject: [PATCH 10/17] refactor into methods --- .../src/lib/ngx-mask.directive.ts | 104 +++++++++--------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index d9d3c9d7..c907d459 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1015,67 +1015,54 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } private maskAppliedWithoutFault(maskedValue: string, inputValue: string): boolean { - try { - const unmaskedValue = this._maskService.removeMask(maskedValue); - if (this._maskService.dropSpecialCharacters) { - inputValue = this.removeSpecialCharactersFrom(inputValue); - } + const unmaskedValue = this._maskService.removeMask(maskedValue); + if (this._maskService.dropSpecialCharacters) { + inputValue = this.removeSpecialCharactersFrom(inputValue); + } - if (this._maskService.hiddenInput) { - for (let i = 0; i < unmaskedValue.length; i++) { - const charAt = unmaskedValue.charAt(i); - const isHidden = charAt === MaskExpression.SYMBOL_STAR; - if (isHidden) { - const part_1 = inputValue.substring(0, i); - const part_2 = MaskExpression.SYMBOL_STAR; - const part_3 = inputValue.substring(i + 1) - inputValue = `${part_1}${part_2}${part_3}` - } - } - } - - if (unmaskedValue === inputValue) { + if (this._maskService.hiddenInput) { + inputValue = this.removeHiddenCharacters(unmaskedValue, inputValue); + } + + if (unmaskedValue === inputValue) { + return true; + } + + // They may still not match due to lost precision + const hasPrecision = this._maskService.maskExpression.indexOf(MaskExpression.SEPARATOR + "."); + const mayPossiblyLosePrecision = hasPrecision >= 0; + if (mayPossiblyLosePrecision) { + const maskExpressionPrecision = Number(this._maskService.maskExpression.split(MaskExpression.SEPARATOR + ".")[1]); + const decimalMarkers = Array.isArray(this._maskService.decimalMarker) ? this._maskService.decimalMarker : [ this._maskService.decimalMarker ]; + const unmaskedPrecisionLossDueToMask = decimalMarkers.some((dm) => { + const split = unmaskedValue.split(dm); + const unmaskedValuePrecision = split[split.length - 1]?.length; + const unmaskedPrecisionLossDueToMask = unmaskedValuePrecision === maskExpressionPrecision; + return unmaskedPrecisionLossDueToMask; + }); + if (unmaskedPrecisionLossDueToMask) { return true; } - // They may still not match due to lost precision - const hasPrecision = this._maskService.maskExpression.indexOf(MaskExpression.SEPARATOR + "."); - const mayPossiblyLosePrecision = hasPrecision >= 0; - if (mayPossiblyLosePrecision) { - const maskExpressionPrecision = Number(this._maskService.maskExpression.split(MaskExpression.SEPARATOR + ".")[1]); - const decimalMarkers = Array.isArray(this._maskService.decimalMarker) ? this._maskService.decimalMarker : [ this._maskService.decimalMarker ]; - const unmaskedPrecisionLossDueToMask = decimalMarkers.some((dm) => { - const split = unmaskedValue.split(dm); - const unmaskedValuePrecision = split[split.length - 1]?.length; - const unmaskedPrecisionLossDueToMask = unmaskedValuePrecision === maskExpressionPrecision; - return unmaskedPrecisionLossDueToMask; - }); - if (unmaskedPrecisionLossDueToMask) { + const scientificNotation = inputValue.indexOf("e") > 0; + if (scientificNotation) { + const power = inputValue.split("e")[1]; + if (power && unmaskedValue.endsWith(power)) { return true; } - - const scientificNotation = inputValue.indexOf("e") > 0; - if (scientificNotation) { - const power = inputValue.split("e")[1]; - if (power && unmaskedValue.endsWith(power)) { - return true; - } - } } + } - // IS THIS A BUG? - // removeMask() is not removing the thousandth separator - const unmaskedWithoutThousandth = this.replaceEachCharacterWith(unmaskedValue, this._maskService.thousandSeparator, MaskExpression.EMPTY_STRING); - if (unmaskedWithoutThousandth === inputValue) { - return true; - } - - // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? - debugger; - return false; - } catch(err) { + // IS THIS A BUG? + // removeMask() is not removing the thousandth separator + const unmaskedWithoutThousandth = this.replaceEachCharacterWith(unmaskedValue, this._maskService.thousandSeparator, MaskExpression.EMPTY_STRING); + if (unmaskedWithoutThousandth === inputValue) { return true; } + + // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? + debugger; + return false; } private removeSpecialCharactersFrom(inputValue: string): string { @@ -1090,6 +1077,21 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return result; } + private removeHiddenCharacters(unmaskedValue: string, inputValue: string): string { + for (let i = 0; i < unmaskedValue.length; i++) { + const charAt = unmaskedValue.charAt(i); + const isHidden = charAt === MaskExpression.SYMBOL_STAR; + if (isHidden) { + const part_1 = inputValue.substring(0, i); + const part_2 = MaskExpression.SYMBOL_STAR; + const part_3 = inputValue.substring(i + 1) + inputValue = `${part_1}${part_2}${part_3}` + } + } + + return inputValue; + } + private replaceEachCharacterWith(result: string, replace: string, replaceWith: string): string { while (result.indexOf(replace) >= 0) { result = result.replace(replace, replaceWith); From e27d200bc51e819b865055de4423a08f6b9e843f Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Fri, 26 Jul 2024 07:35:10 -0400 Subject: [PATCH 11/17] rename maskAppliedWithoutFault to maskApplicationFault --- .../ngx-mask-lib/src/lib/ngx-mask.directive.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index c907d459..183b295d 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1005,8 +1005,8 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida private getNextValue(controlValue: unknown, inputValue: string): string | boolean { const maskedValue = this._maskService.applyMask(inputValue, this._maskService.maskExpression); - const maskingFault = !this.maskAppliedWithoutFault(maskedValue, inputValue); - if (maskingFault) { + const maskingFault = this.maskApplicationFault(maskedValue, inputValue); + if (!maskingFault) { return maskedValue; } @@ -1014,7 +1014,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return controlValue as boolean | string; } - private maskAppliedWithoutFault(maskedValue: string, inputValue: string): boolean { + private maskApplicationFault(maskedValue: string, inputValue: string): boolean { const unmaskedValue = this._maskService.removeMask(maskedValue); if (this._maskService.dropSpecialCharacters) { inputValue = this.removeSpecialCharactersFrom(inputValue); @@ -1025,7 +1025,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } if (unmaskedValue === inputValue) { - return true; + return false; } // They may still not match due to lost precision @@ -1041,14 +1041,14 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return unmaskedPrecisionLossDueToMask; }); if (unmaskedPrecisionLossDueToMask) { - return true; + return false; } const scientificNotation = inputValue.indexOf("e") > 0; if (scientificNotation) { const power = inputValue.split("e")[1]; if (power && unmaskedValue.endsWith(power)) { - return true; + return false; } } } @@ -1057,12 +1057,11 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida // removeMask() is not removing the thousandth separator const unmaskedWithoutThousandth = this.replaceEachCharacterWith(unmaskedValue, this._maskService.thousandSeparator, MaskExpression.EMPTY_STRING); if (unmaskedWithoutThousandth === inputValue) { - return true; + return false; } // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? - debugger; - return false; + return true; } private removeSpecialCharactersFrom(inputValue: string): string { From bf66fefce51231fd1033b109bca0236332d2e57f Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Fri, 26 Jul 2024 07:47:56 -0400 Subject: [PATCH 12/17] update comments --- projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 183b295d..b2268bcc 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1053,14 +1053,13 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } } - // IS THIS A BUG? - // removeMask() is not removing the thousandth separator + // removeMask() might not be removing the thousandth separator const unmaskedWithoutThousandth = this.replaceEachCharacterWith(unmaskedValue, this._maskService.thousandSeparator, MaskExpression.EMPTY_STRING); if (unmaskedWithoutThousandth === inputValue) { return false; } - // [TODO] Is there any other reason to ignore a diff between unmaskedValue and inputValue? + // Is there any other reason to ignore a diff between unmaskedValue and inputValue? return true; } From 89e5e908985f206973d90c59b169a3a5da02ed3b Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Fri, 26 Jul 2024 08:06:31 -0400 Subject: [PATCH 13/17] refactor into its own class --- .../lib/ngx-mask-fault-detection.service.ts | 99 +++++++++++++++++++ .../src/lib/ngx-mask.directive.ts | 90 +---------------- 2 files changed, 104 insertions(+), 85 deletions(-) create mode 100644 projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts new file mode 100644 index 00000000..22930a1f --- /dev/null +++ b/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts @@ -0,0 +1,99 @@ +import { Injectable } from "@angular/core"; +import { MaskExpression } from "./ngx-mask-expression.enum"; +import { NgxMaskService } from "./ngx-mask.service"; + +@Injectable() +export class NgxMaskFaultDetectionService { + + constructor(private _maskService: NgxMaskService){ } + + /** + * Attempts to remove the mask from the maskedValue and compare it with inputValue. Accounts for irreversible changes, such as loss of precision or dropped special characters. + * @param maskedValue the input value after being masked (don't call applyMask() more than once because it has side effects). + * @param inputValue the value that would be sent to the native element. + * @returns whether the mask can be removed without any unexpected loss of characters. + */ + public maskApplicationFault(maskedValue: string, inputValue: string): boolean { + const unmaskedValue = this._maskService.removeMask(maskedValue); + if (this._maskService.dropSpecialCharacters) { + inputValue = this.removeSpecialCharactersFrom(inputValue); + } + + if (this._maskService.hiddenInput) { + inputValue = this.removeHiddenCharacters(unmaskedValue, inputValue); + } + + if (unmaskedValue === inputValue) { + return false; + } + + // They may still not match due to lost precision + const hasPrecision = this._maskService.maskExpression.indexOf(MaskExpression.SEPARATOR + "."); + const mayPossiblyLosePrecision = hasPrecision >= 0; + if (mayPossiblyLosePrecision) { + const maskExpressionPrecision = Number(this._maskService.maskExpression.split(MaskExpression.SEPARATOR + ".")[1]); + const decimalMarkers = Array.isArray(this._maskService.decimalMarker) ? this._maskService.decimalMarker : [ this._maskService.decimalMarker ]; + const unmaskedPrecisionLossDueToMask = decimalMarkers.some((dm) => { + const split = unmaskedValue.split(dm); + const unmaskedValuePrecision = split[split.length - 1]?.length; + const unmaskedPrecisionLossDueToMask = unmaskedValuePrecision === maskExpressionPrecision; + return unmaskedPrecisionLossDueToMask; + }); + if (unmaskedPrecisionLossDueToMask) { + return false; + } + + const scientificNotation = inputValue.indexOf("e") > 0; + if (scientificNotation) { + const power = inputValue.split("e")[1]; + if (power && unmaskedValue.endsWith(power)) { + return false; + } + } + } + + // removeMask() might not be removing the thousandth separator + const unmaskedWithoutThousandth = this.replaceEachCharacterWith(unmaskedValue, this._maskService.thousandSeparator, MaskExpression.EMPTY_STRING); + if (unmaskedWithoutThousandth === inputValue) { + return false; + } + + // Is there any other reason to ignore a diff between unmaskedValue and inputValue? + return true; + } + + private removeSpecialCharactersFrom(inputValue: string): string { + const specialCharacters = Array.isArray(this._maskService.dropSpecialCharacters) + ? this._maskService.dropSpecialCharacters.concat(this._maskService.specialCharacters) + : this._maskService.specialCharacters; + let result = inputValue; + specialCharacters.forEach(sc => { + result = this.replaceEachCharacterWith(result, sc, MaskExpression.EMPTY_STRING); + }); + + return result; + } + + private removeHiddenCharacters(unmaskedValue: string, inputValue: string): string { + for (let i = 0; i < unmaskedValue.length; i++) { + const charAt = unmaskedValue.charAt(i); + const isHidden = charAt === MaskExpression.SYMBOL_STAR; + if (isHidden) { + const part_1 = inputValue.substring(0, i); + const part_2 = MaskExpression.SYMBOL_STAR; + const part_3 = inputValue.substring(i + 1) + inputValue = `${part_1}${part_2}${part_3}` + } + } + + return inputValue; + } + + private replaceEachCharacterWith(result: string, replace: string, replaceWith: string): string { + while (result.indexOf(replace) >= 0) { + result = result.replace(replace, replaceWith); + } + + return result; + } +} \ No newline at end of file diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index b2268bcc..b275be82 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -22,6 +22,7 @@ import { CustomKeyboardEvent } from './custom-keyboard-event'; import { IConfig, NGX_MASK_CONFIG, timeMasks, withoutValidation } from './ngx-mask.config'; import { NgxMaskService } from './ngx-mask.service'; import { MaskExpression } from './ngx-mask-expression.enum'; +import { NgxMaskFaultDetectionService } from './ngx-mask-fault-detection.service'; @Directive({ selector: 'input[mask], textarea[mask]', @@ -38,6 +39,7 @@ import { MaskExpression } from './ngx-mask-expression.enum'; multi: true, }, NgxMaskService, + NgxMaskFaultDetectionService, ], exportAs: 'mask,ngxMask', }) @@ -115,6 +117,8 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida public _maskService = inject(NgxMaskService, { self: true }); + public _maskFaultDetector = inject(NgxMaskFaultDetectionService, { self: true }); + protected _config = inject(NGX_MASK_CONFIG); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1005,7 +1009,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida private getNextValue(controlValue: unknown, inputValue: string): string | boolean { const maskedValue = this._maskService.applyMask(inputValue, this._maskService.maskExpression); - const maskingFault = this.maskApplicationFault(maskedValue, inputValue); + const maskingFault = this._maskFaultDetector.maskApplicationFault(maskedValue, inputValue); if (!maskingFault) { return maskedValue; } @@ -1014,90 +1018,6 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return controlValue as boolean | string; } - private maskApplicationFault(maskedValue: string, inputValue: string): boolean { - const unmaskedValue = this._maskService.removeMask(maskedValue); - if (this._maskService.dropSpecialCharacters) { - inputValue = this.removeSpecialCharactersFrom(inputValue); - } - - if (this._maskService.hiddenInput) { - inputValue = this.removeHiddenCharacters(unmaskedValue, inputValue); - } - - if (unmaskedValue === inputValue) { - return false; - } - - // They may still not match due to lost precision - const hasPrecision = this._maskService.maskExpression.indexOf(MaskExpression.SEPARATOR + "."); - const mayPossiblyLosePrecision = hasPrecision >= 0; - if (mayPossiblyLosePrecision) { - const maskExpressionPrecision = Number(this._maskService.maskExpression.split(MaskExpression.SEPARATOR + ".")[1]); - const decimalMarkers = Array.isArray(this._maskService.decimalMarker) ? this._maskService.decimalMarker : [ this._maskService.decimalMarker ]; - const unmaskedPrecisionLossDueToMask = decimalMarkers.some((dm) => { - const split = unmaskedValue.split(dm); - const unmaskedValuePrecision = split[split.length - 1]?.length; - const unmaskedPrecisionLossDueToMask = unmaskedValuePrecision === maskExpressionPrecision; - return unmaskedPrecisionLossDueToMask; - }); - if (unmaskedPrecisionLossDueToMask) { - return false; - } - - const scientificNotation = inputValue.indexOf("e") > 0; - if (scientificNotation) { - const power = inputValue.split("e")[1]; - if (power && unmaskedValue.endsWith(power)) { - return false; - } - } - } - - // removeMask() might not be removing the thousandth separator - const unmaskedWithoutThousandth = this.replaceEachCharacterWith(unmaskedValue, this._maskService.thousandSeparator, MaskExpression.EMPTY_STRING); - if (unmaskedWithoutThousandth === inputValue) { - return false; - } - - // Is there any other reason to ignore a diff between unmaskedValue and inputValue? - return true; - } - - private removeSpecialCharactersFrom(inputValue: string): string { - const specialCharacters = Array.isArray(this._maskService.dropSpecialCharacters) - ? this._maskService.dropSpecialCharacters.concat(this._maskService.specialCharacters) - : this._maskService.specialCharacters; - let result = inputValue; - specialCharacters.forEach(sc => { - result = this.replaceEachCharacterWith(result, sc, MaskExpression.EMPTY_STRING); - }); - - return result; - } - - private removeHiddenCharacters(unmaskedValue: string, inputValue: string): string { - for (let i = 0; i < unmaskedValue.length; i++) { - const charAt = unmaskedValue.charAt(i); - const isHidden = charAt === MaskExpression.SYMBOL_STAR; - if (isHidden) { - const part_1 = inputValue.substring(0, i); - const part_2 = MaskExpression.SYMBOL_STAR; - const part_3 = inputValue.substring(i + 1) - inputValue = `${part_1}${part_2}${part_3}` - } - } - - return inputValue; - } - - private replaceEachCharacterWith(result: string, replace: string, replaceWith: string): string { - while (result.indexOf(replace) >= 0) { - result = result.replace(replace, replaceWith); - } - - return result; - } - public registerOnChange(fn: typeof this.onChange): void { this._maskService.onChange = this.onChange = fn; } From c82d45caac4c20dfae3e1b338a5e6a8eb91fc80d Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Fri, 26 Jul 2024 08:20:50 -0400 Subject: [PATCH 14/17] test and provide NgxMaskFaultDetectionService --- .../lib/ngx-mask-fault-detection.service.ts | 1 + .../src/lib/ngx-mask.directive.ts | 1 - .../src/lib/ngx-mask.providers.ts | 2 + .../src/test/mask-fault-detection.spec.ts | 51 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 projects/ngx-mask-lib/src/test/mask-fault-detection.spec.ts diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts index 22930a1f..c6874a54 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts @@ -59,6 +59,7 @@ export class NgxMaskFaultDetectionService { } // Is there any other reason to ignore a diff between unmaskedValue and inputValue? + console.warn(`Unexpected fault applying mask: ${this._maskService.maskExpression} to value: ${inputValue}`); return true; } diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index b275be82..33b9eb15 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1014,7 +1014,6 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida return maskedValue; } - console.warn(`Unexpected fault applying mask: ${this._maskService.maskExpression} to value: ${controlValue}`); return controlValue as boolean | string; } diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.providers.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.providers.ts index 8d7a5724..f91521bc 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.providers.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.providers.ts @@ -8,6 +8,7 @@ import { optionsConfig, } from './ngx-mask.config'; import { NgxMaskService } from './ngx-mask.service'; +import { NgxMaskFaultDetectionService } from './ngx-mask-fault-detection.service'; /** * @internal @@ -36,6 +37,7 @@ export function provideNgxMask(configValue?: optionsConfig | (() => optionsConfi useFactory: _configFactory, }, NgxMaskService, + NgxMaskFaultDetectionService ]; } diff --git a/projects/ngx-mask-lib/src/test/mask-fault-detection.spec.ts b/projects/ngx-mask-lib/src/test/mask-fault-detection.spec.ts new file mode 100644 index 00000000..e54a0d1f --- /dev/null +++ b/projects/ngx-mask-lib/src/test/mask-fault-detection.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TestMaskComponent } from './utils/test-component.component'; +import { provideNgxMask } from '../lib/ngx-mask.providers'; +import { NgxMaskDirective } from '../lib/ngx-mask.directive'; +import { NgxMaskFaultDetectionService } from '../lib/ngx-mask-fault-detection.service'; +import { NgxMaskService } from '../lib/ngx-mask.service'; + +describe('MaskFaultDetectionService', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestMaskComponent], + imports: [ReactiveFormsModule, NgxMaskDirective], + providers: [provideNgxMask()], + }); + fixture = TestBed.createComponent(TestMaskComponent); + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it('should detect no issues if the mask is applied without any unexpected losses', async () => { + const faultSvc = TestBed.inject(NgxMaskFaultDetectionService); + const maskSvc = TestBed.inject(NgxMaskService); + + const inputValue = "123"; + const maskExpression = maskSvc.maskExpression = "AAA"; + + const maskedValue = maskSvc.applyMask(inputValue, maskExpression); + const maskingFault = faultSvc.maskApplicationFault(maskedValue, inputValue); + + expect(maskingFault).toBe(false); + }); + + it('should detect an issue if the mask is applied with any unexpected losses', async () => { + const faultSvc = TestBed.inject(NgxMaskFaultDetectionService); + const maskSvc = TestBed.inject(NgxMaskService); + + const inputValue = "123"; + const maskExpression = maskSvc.maskExpression = "AAS"; + + const maskedValue = maskSvc.applyMask(inputValue, maskExpression); + const maskingFault = faultSvc.maskApplicationFault(maskedValue, inputValue); + + expect(maskingFault).toBe(true); + }); +}); From ebca34679309ee0bafa80a785393f3eab24ddf60 Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Fri, 26 Jul 2024 08:32:06 -0400 Subject: [PATCH 15/17] do not use controlValue --- projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 33b9eb15..1078b425 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -989,7 +989,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida this._maskService.formElementProperty = [ 'value', - this.getNextValue(controlValue, inputValue), + this.getNextValue(inputValue), ]; // Let the service know we've finished writing value typeof this.inputTransformFn !== 'function' @@ -1007,14 +1007,14 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } } - private getNextValue(controlValue: unknown, inputValue: string): string | boolean { + private getNextValue(inputValue: string): string | boolean { const maskedValue = this._maskService.applyMask(inputValue, this._maskService.maskExpression); const maskingFault = this._maskFaultDetector.maskApplicationFault(maskedValue, inputValue); if (!maskingFault) { return maskedValue; } - return controlValue as boolean | string; + return inputValue; } public registerOnChange(fn: typeof this.onChange): void { From e50bc3d6226b94f8dc123adeb56a68085b81c606 Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Sat, 27 Jul 2024 10:49:11 -0400 Subject: [PATCH 16/17] only returns string, not boolean. --- projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 1078b425..6adc097f 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -1007,7 +1007,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } } - private getNextValue(inputValue: string): string | boolean { + private getNextValue(inputValue: string): string { const maskedValue = this._maskService.applyMask(inputValue, this._maskService.maskExpression); const maskingFault = this._maskFaultDetector.maskApplicationFault(maskedValue, inputValue); if (!maskingFault) { From 8bc3a0f605e09a4bb871c597a3d9d806218f2cbe Mon Sep 17 00:00:00 2001 From: Matt Kuhn Date: Sat, 27 Jul 2024 10:49:22 -0400 Subject: [PATCH 17/17] update comment for clarity --- .../ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts index c6874a54..add0e13c 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask-fault-detection.service.ts @@ -9,7 +9,7 @@ export class NgxMaskFaultDetectionService { /** * Attempts to remove the mask from the maskedValue and compare it with inputValue. Accounts for irreversible changes, such as loss of precision or dropped special characters. - * @param maskedValue the input value after being masked (don't call applyMask() more than once because it has side effects). + * @param maskedValue the input value after being masked (doesn't call applyMask() itself because doing so causes undesired side effects). * @param inputValue the value that would be sent to the native element. * @returns whether the mask can be removed without any unexpected loss of characters. */