Skip to content
Merged

Release #1566

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 20.0.3(2025-08-01)

### fix

- Fix ([#1560](https://github.com/JsDaddy/ngx-mask/issues/1560))

# 20.0.2(2025-07-31)

### fix
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-mask",
"version": "20.0.2",
"version": "20.0.3",
"description": "Awesome ngx mask",
"license": "MIT",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion projects/ngx-mask-lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-mask",
"version": "20.0.2",
"version": "20.0.3",
"description": "awesome ngx mask",
"keywords": [
"ng2-mask",
Expand Down
3 changes: 3 additions & 0 deletions projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida

@HostListener('input', ['$event'])
public onInput(e: CustomKeyboardEvent): void {
this._maskService.isInitialized = true;
// If IME is composing text, we wait for the composed text.
if (this._isComposing()) {
return;
Expand Down Expand Up @@ -1024,8 +1025,10 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida
];
// Let the service know we've finished writing value
this._maskService.writingValue = false;
this._maskService.isInitialized = true;
} else {
this._maskService.formElementProperty = ['value', inputValue];
this._maskService.isInitialized = true;
}
this._inputValue.set(inputValue);
} else {
Expand Down
8 changes: 7 additions & 1 deletion projects/ngx-mask-lib/src/lib/ngx-mask.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class NgxMaskService extends NgxMaskApplierService {
* since writeValue should be a one way only process of writing the DOM value based on the Angular model value.
*/
public writingValue = false;
public isInitialized = false;

private _emitValue = false;
private _start!: number;
Expand Down Expand Up @@ -267,7 +268,6 @@ export class NgxMaskService extends NgxMaskApplierService {

this._emitValue =
this.previousValue !== this.currentValue ||
(newInputValue !== this.currentValue && this.writingValue) ||
(this.previousValue === this.currentValue && justPasted);
}

Expand Down Expand Up @@ -594,8 +594,14 @@ export class NgxMaskService extends NgxMaskApplierService {
const outputTransformFn = this.outputTransformFn
? this.outputTransformFn
: (v: unknown) => v;

this.writingValue = false;
this.maskChanged = false;

if (!this.isInitialized && this._emitValue) {
return;
}

if (Array.isArray(this.dropSpecialCharacters)) {
this.onChange(
outputTransformFn(
Expand Down
94 changes: 33 additions & 61 deletions projects/ngx-mask-lib/src/test/forms.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import type { ComponentFixture } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { FormControl, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { NgxMaskDirective, provideNgxMask } from 'ngx-mask';

@Component({
Expand All @@ -13,79 +13,51 @@ class TestMaskComponent {
public form: FormControl = new FormControl('');
}

@Component({
selector: 'jsdaddy-phone-test',
imports: [FormsModule, NgxMaskDirective],
template: `
<form #phoneForm="ngForm">
<input
name="phoneNumber"
[(ngModel)]="phoneNumber"
[pattern]="phoneValidationPattern"
[dropSpecialCharacters]="false"
[mask]="phoneMask" />
</form>
`,
})
class TestPhoneMaskComponent {
public phoneValidationPattern =
/^\(?([2-9][0-8][0-9])\)?[-. ]*([2-9][0-9]{2})[-. ]*([0-9]{4})$/;
public phoneMask = '(000) 000-0000';
public phoneNumber = '3333333333';
}

describe('Directive: Forms', () => {
let fixture: ComponentFixture<TestMaskComponent>;
let component: TestMaskComponent;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [NgxMaskDirective],
providers: [provideNgxMask()],
});
fixture = TestBed.createComponent(TestMaskComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should propagate masked value to the form control value', () => {
component.form.setValue('A1234Z');
expect(component.form.value).toBe('1234');
});

it('should propagate masked value to the form control valueChanges observable', () => {
component.form.valueChanges.subscribe((newValue) => {
expect(newValue).toEqual('1234');
});

component.form.setValue('A1234Z');
});

it('should mask values when multiple calls to setValue() are made', () => {
component.form.setValue('A1234Z');
expect(component.form.value).toBe('1234');
component.form.setValue('A1234Z');
expect(component.form.value).toBe('1234');
component.form.setValue('A1234Z');
expect(component.form.value).toBe('1234');
});

it('should propagate masked value to the form control valueChanges observable when multiple calls to setValue() are made', () => {
component.form.valueChanges.subscribe((newValue) => {
expect(newValue).toEqual('1234');
});

component.form.setValue('A1234Z');
component.form.setValue('A1234Z');
component.form.setValue('A1234Z');
});

it('should not emit to valueChanges if the masked value has not changed with emitEvent: true', () => {
let emissionsToValueChanges = 0;

component.form.valueChanges.subscribe(() => {
emissionsToValueChanges++;
});

component.form.setValue('1234', { emitEvent: true });
component.form.setValue('1234', { emitEvent: true });

// Expect to emit 3 times, once for the first setValue() call, once by ngx-mask, and once for the second setValue() call.
// There is not fourth emission for when ngx-mask masks the value for a second time.
expect(emissionsToValueChanges).toBe(3);
});

it('should not emit to valueChanges if the masked value has not changed with emitEvent: false', () => {
let emissionsToValueChanges = 0;

component.form.valueChanges.subscribe(() => {
emissionsToValueChanges++;
});
it('should not mark form as dirty on initial load with initial value', () => {
const testBed = TestBed.createComponent(TestPhoneMaskComponent);
const phoneComponent = testBed.componentInstance;
phoneComponent.phoneNumber = '3333333333';
testBed.detectChanges();

component.form.setValue('1234', { emitEvent: false });
component.form.setValue('1234', { emitEvent: false });
// Get the form element and check if it's not dirty
const formElement = testBed.nativeElement.querySelector('form');
const inputElement = testBed.nativeElement.querySelector('input');

// Expect to only have emitted once, only by ngx-mask.
// There is no second emission for when ngx-mask masks the value for a second time.
expect(emissionsToValueChanges).toBe(1);
// Check that the form is not dirty on initial load
expect(formElement.classList.contains('ng-dirty')).toBe(false);
expect(inputElement.classList.contains('ng-dirty')).toBe(false);
});
});
Loading