From a9524d32e3bfb76abdac5773f92971f29aa8465c Mon Sep 17 00:00:00 2001 From: christophercr Date: Wed, 13 Feb 2019 15:12:14 +0100 Subject: [PATCH] feat(stark-ui): implement directives for email, number and timestamp masks ISSUES CLOSED: #681, #682, #683 --- packages/rollup.config.common-data.js | 1 + .../input-mask-directives/directives.ts | 6 + .../directives/email-mask.directive.spec.ts | 329 ++++++++++++++ .../directives/email-mask.directive.ts | 60 +++ .../directives/number-mask-config.intf.ts | 60 +++ .../directives/number-mask.directive.spec.ts | 379 ++++++++++++++++ .../directives/number-mask.directive.ts | 83 ++++ .../directives/timestamp-mask-config.intf.ts | 3 + .../timestamp-mask.directive.spec.ts | 413 ++++++++++++++++++ .../directives/timestamp-mask.directive.ts | 99 +++++ .../directives/timestamp-pipe.fn.spec.ts | 122 ++++++ .../directives/timestamp-pipe.fn.ts | 81 ++++ .../input-mask-directives.module.ts | 6 +- showcase/src/app/demo-ui/demo-ui.module.ts | 6 +- ...-input-mask-directives-page.component.html | 154 ++++++- ...mo-input-mask-directives-page.component.ts | 99 ++++- showcase/src/assets/translations/en.json | 31 +- showcase/src/assets/translations/fr.json | 27 +- showcase/src/assets/translations/nl.json | 27 +- 19 files changed, 1963 insertions(+), 23 deletions(-) create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.spec.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/number-mask-config.intf.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.spec.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask-config.intf.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.spec.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.spec.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.ts diff --git a/packages/rollup.config.common-data.js b/packages/rollup.config.common-data.js index 15ac38fd79..b6ab47c007 100644 --- a/packages/rollup.config.common-data.js +++ b/packages/rollup.config.common-data.js @@ -69,6 +69,7 @@ const globals = { "prismjs/components/prism-css-extras.min.js": "Prism.languages.css.selector", "prismjs/components/prism-scss.min.js": "Prism.languages.scss", "text-mask-core": "textMaskCore", + "text-mask-addons": "textMaskAddons", uuid: "uuid", rxjs: "rxjs", diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives.ts b/packages/stark-ui/src/modules/input-mask-directives/directives.ts index d6e7741585..05fb37aa05 100644 --- a/packages/stark-ui/src/modules/input-mask-directives/directives.ts +++ b/packages/stark-ui/src/modules/input-mask-directives/directives.ts @@ -1,3 +1,9 @@ +export * from "./directives/email-mask.directive"; +export * from "./directives/number-mask-config.intf"; +export * from "./directives/number-mask.directive"; export * from "./directives/text-mask.constants"; export * from "./directives/text-mask.directive"; export * from "./directives/text-mask-config.intf"; +export * from "./directives/timestamp-mask-config.intf"; +export * from "./directives/timestamp-mask.directive"; +export * from "./directives/timestamp-pipe.fn"; diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.spec.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.spec.ts new file mode 100644 index 0000000000..9b66578e3a --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.spec.ts @@ -0,0 +1,329 @@ +/* tslint:disable:completed-docs no-duplicate-string no-identical-functions no-big-function */ +import { Component, DebugElement } from "@angular/core"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing"; +import { Observer } from "rxjs"; +import { StarkEmailMaskDirective } from "./email-mask.directive"; + +describe("EmailMaskDirective", () => { + let fixture: ComponentFixture; + let hostComponent: TestComponent; + let inputElement: DebugElement; + + @Component({ + selector: "test-component", + template: getTemplate("[starkEmailMask]='emailMaskConfig'") + }) + class TestComponent { + public emailMaskConfig: boolean; + public ngModelValue: string = ""; + public formControl: FormControl = new FormControl(""); + } + + function getTemplate(emailMaskDirective: string): string { + return ""; + } + + function initializeComponentFixture(): void { + fixture = TestBed.createComponent(TestComponent); + hostComponent = fixture.componentInstance; + inputElement = fixture.debugElement.query(By.css("input")); + // trigger initial data binding + fixture.detectChanges(); + } + + function changeInputValue(inputDebugElement: DebugElement, value: string, eventType: string = "input"): void { + (inputDebugElement.nativeElement).value = value; + + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const ev: Event = document.createEvent("Event"); + ev.initEvent(eventType, true, true); + (inputDebugElement.nativeElement).dispatchEvent(ev); + } + + // Inject module dependencies + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkEmailMaskDirective, TestComponent], + imports: [FormsModule, ReactiveFormsModule], + providers: [] + }); + }); + + describe("uncontrolled", () => { + beforeEach(fakeAsync(() => { + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkEmailMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email@ ."); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "my-email", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email"); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["@@", "@.a.", " @ .", "what@.ever@."]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe(""); + } + }); + + it("should remove the mask only when the config is set to false", () => { + changeInputValue(inputElement, "my-email@"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email@ ."); + + hostComponent.emailMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email@ ."); // the mask is enabled by default + + hostComponent.emailMaskConfig = false; + fixture.detectChanges(); + + changeInputValue(inputElement, "what@@.ever@."); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("what@@.ever@."); // no mask at all + }); + }); + + describe("with ngModel", () => { + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[(ngModel)]='ngModelValue' [starkEmailMask]='emailMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkEmailMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("my-email@ ."); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + // IMPORTANT: the ngModel is not changed with invalid events, just with "input" events + expect(hostComponent.ngModelValue).toBe(""); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["@@", "@.a.", " @ .", "what@.ever@."]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe(""); + } + }); + + it("should remove the mask only when the config is set to false", () => { + changeInputValue(inputElement, "my-email@"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("my-email@ ."); + + hostComponent.emailMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("my-email@ ."); // the mask is enabled by default + + hostComponent.emailMaskConfig = false; + fixture.detectChanges(); + + changeInputValue(inputElement, "what@@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("what@@.ever@."); // no mask at all + }); + }); + + describe("with FormControl", () => { + let mockValueChangeObserver: jasmine.SpyObj>; + + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[formControl]='formControl' [starkEmailMask]='emailMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + + mockValueChangeObserver = jasmine.createSpyObj>("observerSpy", ["next", "error", "complete"]); + hostComponent.formControl.valueChanges.subscribe(mockValueChangeObserver); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkEmailMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("my-email@ ."); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + + mockValueChangeObserver.next.calls.reset(); + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + // IMPORTANT: the formControl is not changed with invalid events, just with "input" events + expect(hostComponent.formControl.value).toBe(""); // no mask shown + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["@@", "@.a.", " @ .", "what@.ever@."]; + + for (const value of invalidValues) { + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "my-email@"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("my-email@ ."); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.emailMaskConfig = undefined; + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask is enabled by default + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("my-email@ ."); // the mask is enabled by default + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.emailMaskConfig = false; + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask was just disabled + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "what@@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("what@@.ever@."); // no mask at all + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.ts new file mode 100644 index 0000000000..bc7b624257 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.ts @@ -0,0 +1,60 @@ +import { Directive, ElementRef, forwardRef, Inject, Input, OnChanges, Optional, Provider, Renderer2, SimpleChanges } from "@angular/core"; +import { COMPOSITION_BUFFER_MODE, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { CombinedPipeMask } from "text-mask-core"; +import { emailMask } from "text-mask-addons"; +import { MaskedInputDirective, TextMaskConfig as Ng2TextMaskConfig } from "angular2-text-mask"; + +/** + * Name of the directive + */ +const directiveName: string = "[starkEmailMask]"; + +export const STARK_EMAIL_MASK_VALUE_ACCESSOR: Provider = { + provide: NG_VALUE_ACCESSOR, + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => StarkEmailMaskDirective), + multi: true +}; + +@Directive({ + host: { + "(input)": "_handleInput($event.target.value)", + "(blur)": "onTouched()", + "(compositionstart)": "_compositionStart()", + "(compositionend)": "_compositionEnd($event.target.value)" + }, + selector: directiveName, + exportAs: "starkEmailMask", + providers: [STARK_EMAIL_MASK_VALUE_ACCESSOR] +}) +export class StarkEmailMaskDirective extends MaskedInputDirective implements OnChanges { + /* tslint:disable:no-input-rename */ + @Input("starkEmailMask") + public maskConfig: boolean = true; // enabled by default + + public constructor( + _renderer: Renderer2, + _elementRef: ElementRef, + @Optional() @Inject(COMPOSITION_BUFFER_MODE) _compositionMode: boolean + ) { + super(_renderer, _elementRef, _compositionMode); + } + + public ngOnChanges(changes: SimpleChanges): void { + this.textMaskConfig = this.normalizeMaskConfig(this.maskConfig); + + super.ngOnChanges(changes); + } + + public normalizeMaskConfig(maskConfig: boolean = true): Ng2TextMaskConfig { + if (!maskConfig) { + return { mask: false }; // remove the mask + } else { + // TODO: Ng2TextMaskConfig is not the same as Core TextMaskConfig + // even though emailMask is passed as a mask, it is actually made of both a mask and a pipe bundled together for convenience + // https://github.com/text-mask/text-mask/tree/master/addons + const { mask, pipe }: CombinedPipeMask = emailMask; + return { mask: mask, pipe: pipe }; + } + } +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask-config.intf.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask-config.intf.ts new file mode 100644 index 0000000000..7d7f5a181d --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask-config.intf.ts @@ -0,0 +1,60 @@ +/** + * Interface based on the API of the createNumberMask() function from the text-mask-addons library + * See https://github.com/text-mask/text-mask/tree/master/addons#createnumbermask + */ +export interface StarkNumberMaskConfig { + /** + * String to be displayed before the amount. Default: empty string ("") + */ + prefix?: string; + + /** + * String to be displayed after the amount. Default: empty string ("") + */ + suffix?: string; + + /** + * Whether or not to separate thousands. Default: true + */ + includeThousandsSeparator?: boolean; + + /** + * Character to be used as thousands separator. Default: "," + */ + thousandsSeparatorSymbol?: string; + + /** + * Whether or not to allow the user to enter a fraction with the amount. Default: false + */ + allowDecimal?: boolean; + + /** + * Character to be used as decimal point. Default: "." + */ + decimalSymbol?: string; + + /** + * Number of digits to allow in the decimal part of the number. Default: 2 + */ + decimalLimit?: number; + + /** + * Limit the length of the integer number. Default: undefined (unlimited) + */ + integerLimit?: number; + + /** + * Whether or not to always include a decimal point and placeholder for decimal digits after the integer. Default: false + */ + requireDecimal?: boolean; + + /** + * Whether or not to allow negative numbers. Default: true + */ + allowNegative?: boolean; + + /** + * Whether or not to allow leading zeroes. Default: false + */ + allowLeadingZeroes?: boolean; +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.spec.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.spec.ts new file mode 100644 index 0000000000..6df34a7e5e --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.spec.ts @@ -0,0 +1,379 @@ +/* tslint:disable:completed-docs no-duplicate-string no-identical-functions no-big-function */ +import { Component, DebugElement } from "@angular/core"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing"; +import { Observer } from "rxjs"; +import { StarkNumberMaskDirective } from "./number-mask.directive"; +import { StarkNumberMaskConfig } from "./number-mask-config.intf"; + +describe("NumberMaskDirective", () => { + let fixture: ComponentFixture; + let hostComponent: TestComponent; + let inputElement: DebugElement; + + const numberMaskConfig: StarkNumberMaskConfig = { + prefix: "", + suffix: "" + }; + + @Component({ + selector: "test-component", + template: getTemplate("[starkNumberMask]='numberMaskConfig'") + }) + class TestComponent { + public numberMaskConfig: StarkNumberMaskConfig = numberMaskConfig; + public ngModelValue: string = ""; + public formControl: FormControl = new FormControl(""); + } + + function getTemplate(numberMaskDirective: string): string { + return ""; + } + + function initializeComponentFixture(): void { + fixture = TestBed.createComponent(TestComponent); + hostComponent = fixture.componentInstance; + inputElement = fixture.debugElement.query(By.css("input")); + // trigger initial data binding + fixture.detectChanges(); + } + + function changeInputValue(inputDebugElement: DebugElement, value: string, eventType: string = "input"): void { + (inputDebugElement.nativeElement).value = value; + + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const ev: Event = document.createEvent("Event"); + ev.initEvent(eventType, true, true); + (inputDebugElement.nativeElement).dispatchEvent(ev); + } + + // Inject module dependencies + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkNumberMaskDirective, TestComponent], + imports: [FormsModule, ReactiveFormsModule], + providers: [] + }); + }); + + describe("uncontrolled", () => { + beforeEach(fakeAsync(() => { + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkNumberMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12,345"); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12345"); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "/*-+,."]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe(""); + } + + const invalidNumericValues: string[] = ["1-2-3.4.5", "+1*23-4/5", ".1.234,5"]; + + for (const numericValue of invalidNumericValues) { + changeInputValue(inputElement, numericValue); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12,345"); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12,345"); + + hostComponent.numberMaskConfig = { ...numberMaskConfig, prefix: "%", suffix: " percent", thousandsSeparatorSymbol: "-" }; + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("%12-345 percent"); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12,345"); + + hostComponent.numberMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "whatever+1*23-4/5"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("whatever+1*23-4/5"); // no mask at all + }); + }); + + describe("with ngModel", () => { + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[(ngModel)]='ngModelValue' [starkNumberMask]='numberMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkNumberMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12,345"); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + // IMPORTANT: the ngModel is not changed with invalid events, just with "input" events + expect(hostComponent.ngModelValue).toBe(""); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "/*-+,."]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe(""); + } + + const invalidNumericValues: string[] = ["1-2-3.4.5", "+1*23-4/5", ".1.234,5"]; + + for (const numericValue of invalidNumericValues) { + changeInputValue(inputElement, numericValue); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12,345"); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12,345"); + + hostComponent.numberMaskConfig = { ...numberMaskConfig, prefix: "%", suffix: " percent", thousandsSeparatorSymbol: "-" }; + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("%12-345 percent"); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12,345"); + + hostComponent.numberMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "whatever+1*23-4/5"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("whatever+1*23-4/5"); // no mask at all + }); + }); + + describe("with FormControl", () => { + let mockValueChangeObserver: jasmine.SpyObj>; + + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[formControl]='formControl' [starkNumberMask]='numberMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + + mockValueChangeObserver = jasmine.createSpyObj>("observerSpy", ["next", "error", "complete"]); + hostComponent.formControl.valueChanges.subscribe(mockValueChangeObserver); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkNumberMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12,345"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + + mockValueChangeObserver.next.calls.reset(); + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + // IMPORTANT: the formControl is not changed with invalid events, just with "input" events + expect(hostComponent.formControl.value).toBe(""); // no mask shown + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "/*-+,."]; + + for (const value of invalidValues) { + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + + const invalidNumericValues: string[] = ["1-2-3.4.5", "+1*23-4/5", ".1.234,5"]; + + for (const numericValue of invalidNumericValues) { + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, numericValue); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12,345"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12,345"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.numberMaskConfig = { ...numberMaskConfig, prefix: "%", suffix: " percent", thousandsSeparatorSymbol: "-" }; + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("%12-345 percent"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12,345"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.numberMaskConfig = undefined; + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask was just disabled + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "whatever+1*23-4/5"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("whatever+1*23-4/5"); // no mask at all + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.ts new file mode 100644 index 0000000000..738f27548a --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.ts @@ -0,0 +1,83 @@ +import { Directive, ElementRef, forwardRef, Inject, Input, OnChanges, Optional, Provider, Renderer2, SimpleChanges } from "@angular/core"; +import { COMPOSITION_BUFFER_MODE, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { MaskedInputDirective, TextMaskConfig as Ng2TextMaskConfig } from "angular2-text-mask"; +import { createNumberMask } from "text-mask-addons"; +import { StarkNumberMaskConfig } from "./number-mask-config.intf"; + +/** + * Name of the directive + */ +const directiveName: string = "[starkNumberMask]"; + +export const STARK_NUMBER_MASK_VALUE_ACCESSOR: Provider = { + provide: NG_VALUE_ACCESSOR, + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => StarkNumberMaskDirective), + multi: true +}; + +const defaultNumberMaskConfig: StarkNumberMaskConfig = { + prefix: "", + suffix: "", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: false, + decimalSymbol: ".", + decimalLimit: 2, + requireDecimal: false, + allowNegative: true, + allowLeadingZeroes: false +}; + +@Directive({ + host: { + "(input)": "_handleInput($event.target.value)", + "(blur)": "onTouched()", + "(compositionstart)": "_compositionStart()", + "(compositionend)": "_compositionEnd($event.target.value)" + }, + selector: directiveName, + exportAs: "starkNumberMask", + providers: [STARK_NUMBER_MASK_VALUE_ACCESSOR] +}) +export class StarkNumberMaskDirective extends MaskedInputDirective implements OnChanges { + /* tslint:disable:no-input-rename */ + @Input("starkNumberMask") + public maskConfig: StarkNumberMaskConfig; + + public elementRef: ElementRef; + + public constructor( + _renderer: Renderer2, + _elementRef: ElementRef, + @Optional() @Inject(COMPOSITION_BUFFER_MODE) _compositionMode: boolean + ) { + super(_renderer, _elementRef, _compositionMode); + this.elementRef = _elementRef; + } + + public ngOnChanges(changes: SimpleChanges): void { + this.textMaskConfig = this.normalizeMaskConfig(this.maskConfig); + + super.ngOnChanges(changes); + + // TODO: temporary workaround to update the model when the maskConfig changes since this is not yet implemented in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + if (changes["maskConfig"] && !changes["maskConfig"].isFirstChange() && this.textMaskConfig.mask !== false) { + // trigger a dummy "input" event in the input to trigger the changes in the model (only if the mask was not disabled!) + const ev: Event = document.createEvent("Event"); + ev.initEvent("input", true, true); + (this.elementRef.nativeElement).dispatchEvent(ev); + } + } + + public normalizeMaskConfig(maskConfig: StarkNumberMaskConfig): Ng2TextMaskConfig { + if (typeof maskConfig === "undefined") { + return { mask: false }; // remove the mask + } else { + // TODO: Ng2TextMaskConfig is not the same as Core TextMaskConfig + const numberMaskConfig: StarkNumberMaskConfig = { ...defaultNumberMaskConfig, ...maskConfig }; + return { mask: createNumberMask(numberMaskConfig) }; + } + } +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask-config.intf.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask-config.intf.ts new file mode 100644 index 0000000000..a82b6e84f2 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask-config.intf.ts @@ -0,0 +1,3 @@ +export interface StarkTimestampMaskConfig { + format: string; +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.spec.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.spec.ts new file mode 100644 index 0000000000..1f78687c1c --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.spec.ts @@ -0,0 +1,413 @@ +/* tslint:disable:completed-docs no-duplicate-string no-identical-functions no-big-function */ +import { Component, DebugElement } from "@angular/core"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing"; +import { Observer } from "rxjs"; +import { StarkTimestampMaskDirective } from "./timestamp-mask.directive"; +import { StarkTimestampMaskConfig } from "./timestamp-mask-config.intf"; + +describe("TimestampMaskDirective", () => { + let fixture: ComponentFixture; + let hostComponent: TestComponent; + let inputElement: DebugElement; + + const timestampMaskConfig: StarkTimestampMaskConfig = { + format: "DD/MM/YYYY" + }; + + @Component({ + selector: "test-component", + template: getTemplate("[starkTimestampMask]='timestampMaskConfig'") + }) + class TestComponent { + public timestampMaskConfig: StarkTimestampMaskConfig = timestampMaskConfig; + public ngModelValue: string = ""; + public formControl: FormControl = new FormControl(""); + } + + function getTemplate(timestampMaskDirective: string): string { + return ""; + } + + function initializeComponentFixture(): void { + fixture = TestBed.createComponent(TestComponent); + hostComponent = fixture.componentInstance; + inputElement = fixture.debugElement.query(By.css("input")); + // trigger initial data binding + fixture.detectChanges(); + } + + function changeInputValue(inputDebugElement: DebugElement, value: string, eventType: string = "input"): void { + (inputDebugElement.nativeElement).value = value; + + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const ev: Event = document.createEvent("Event"); + ev.initEvent(eventType, true, true); + (inputDebugElement.nativeElement).dispatchEvent(ev); + } + + // Inject module dependencies + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkTimestampMaskDirective, TestComponent], + imports: [FormsModule, ReactiveFormsModule], + providers: [] + }); + }); + + describe("uncontrolled", () => { + beforeEach(fakeAsync(() => { + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkTimestampMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12/3_/____"); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("123"); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "whatever"]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe(""); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12/3_/____"); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "DD-MM" }; + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12-3_"); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12/3_/____"); + + hostComponent.timestampMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "whatever"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("whatever"); // no mask at all + }); + + it("should allow to enter February 29 manually in the input field when a year is foreseen but is not yet entered", () => { + changeInputValue(inputElement, "2902"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("29/02/____"); + + // FIXME: currently the text-mask library throws if the value is not cleared before changing the config + // in fact the model in not changed after changing the config. which is not implemented yet in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + // the error is thrown as long as the entered value is not valid according to the new config + changeInputValue(inputElement, ""); + fixture.detectChanges(); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "MM-DD-YY" }; + fixture.detectChanges(); + + changeInputValue(inputElement, "0229"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("02-29-__"); + }); + }); + + describe("with ngModel", () => { + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[(ngModel)]='ngModelValue' [starkTimestampMask]='timestampMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkTimestampMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12/3_/____"); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + // IMPORTANT: the ngModel is not changed with invalid events, just with "input" events + expect(hostComponent.ngModelValue).toBe(""); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "whatever"]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe(""); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12/3_/____"); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "DD-MM" }; + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12-3_"); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12/3_/____"); + + hostComponent.timestampMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "whatever"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("whatever"); // no mask at all + }); + + it("should allow to enter February 29 manually in the input field when a year is foreseen but is not yet entered", () => { + changeInputValue(inputElement, "2902"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("29/02/____"); + + // FIXME: currently the text-mask library throws if the value is not cleared before changing the config + // in fact the model in not changed after changing the config. which is not implemented yet in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + // the error is thrown as long as the entered value is not valid according to the new config + changeInputValue(inputElement, ""); + fixture.detectChanges(); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "MM-DD-YY" }; + fixture.detectChanges(); + + changeInputValue(inputElement, "0229"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("02-29-__"); + }); + }); + + describe("with FormControl", () => { + let mockValueChangeObserver: jasmine.SpyObj>; + + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[formControl]='formControl' [starkTimestampMask]='timestampMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + + mockValueChangeObserver = jasmine.createSpyObj>("observerSpy", ["next", "error", "complete"]); + hostComponent.formControl.valueChanges.subscribe(mockValueChangeObserver); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkTimestampMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12/3_/____"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + + mockValueChangeObserver.next.calls.reset(); + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + // IMPORTANT: the formControl is not changed with invalid events, just with "input" events + expect(hostComponent.formControl.value).toBe(""); // no mask shown + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "whatever"]; + + for (const value of invalidValues) { + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12/3_/____"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "DD-MM" }; + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12-3_"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12/3_/____"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.timestampMaskConfig = undefined; + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask was just disabled + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "whatever"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("whatever"); // no mask at all + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + + it("should allow to enter February 29 manually in the input field when a year is foreseen but is not yet entered", () => { + changeInputValue(inputElement, "2902"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("29/02/____"); + + // FIXME: currently the text-mask library throws if the value is not cleared before changing the config + // in fact the model in not changed after changing the config. which is not implemented yet in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + // the error is thrown as long as the entered value is not valid according to the new config + changeInputValue(inputElement, ""); + fixture.detectChanges(); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "MM-DD-YY" }; + fixture.detectChanges(); + + changeInputValue(inputElement, "0229"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("02-29-__"); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.ts new file mode 100644 index 0000000000..5bbf0f7229 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.ts @@ -0,0 +1,99 @@ +import { Directive, ElementRef, forwardRef, Inject, Input, OnChanges, Optional, Provider, Renderer2, SimpleChanges } from "@angular/core"; +import { COMPOSITION_BUFFER_MODE, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { MaskedInputDirective, TextMaskConfig as Ng2TextMaskConfig } from "angular2-text-mask"; +import { MaskArray } from "text-mask-core"; +import { StarkTimestampMaskConfig } from "./timestamp-mask-config.intf"; +import { createTimestampPipe } from "./timestamp-pipe.fn"; + +/** + * Name of the directive + */ +const directiveName: string = "[starkTimestampMask]"; + +export const STARK_TIMESTAMP_MASK_VALUE_ACCESSOR: Provider = { + provide: NG_VALUE_ACCESSOR, + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => StarkTimestampMaskDirective), + multi: true +}; + +const defaultTimestampMaskConfig: StarkTimestampMaskConfig = { + format: "DD-MM-YYYY HH:mm:ss" +}; + +@Directive({ + host: { + "(input)": "_handleInput($event.target.value)", + "(blur)": "onTouched()", + "(compositionstart)": "_compositionStart()", + "(compositionend)": "_compositionEnd($event.target.value)" + }, + selector: directiveName, + exportAs: "starkTimestampMask", + providers: [STARK_TIMESTAMP_MASK_VALUE_ACCESSOR] +}) +export class StarkTimestampMaskDirective extends MaskedInputDirective implements OnChanges { + /* tslint:disable:no-input-rename */ + @Input("starkTimestampMask") + public maskConfig: StarkTimestampMaskConfig; + + public elementRef: ElementRef; + + public constructor( + _renderer: Renderer2, + _elementRef: ElementRef, + @Optional() @Inject(COMPOSITION_BUFFER_MODE) _compositionMode: boolean + ) { + super(_renderer, _elementRef, _compositionMode); + this.elementRef = _elementRef; + } + + public ngOnChanges(changes: SimpleChanges): void { + this.textMaskConfig = this.normalizeMaskConfig(this.maskConfig); + + super.ngOnChanges(changes); + + // TODO: temporary workaround to update the model when the maskConfig changes since this is not yet implemented in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + if (changes["maskConfig"] && !changes["maskConfig"].isFirstChange() && this.textMaskConfig.mask !== false) { + // trigger a dummy "input" event in the input to trigger the changes in the model (only if the mask was not disabled!) + const ev: Event = document.createEvent("Event"); + ev.initEvent("input", true, true); + (this.elementRef.nativeElement).dispatchEvent(ev); + } + } + + public normalizeMaskConfig(maskConfig: StarkTimestampMaskConfig): Ng2TextMaskConfig { + if (typeof maskConfig === "undefined") { + return { mask: false }; // remove the mask + } else { + // TODO: Ng2TextMaskConfig is not the same as Core TextMaskConfig + const timestampMaskConfig: StarkTimestampMaskConfig = { ...defaultTimestampMaskConfig, ...maskConfig }; + + return { + pipe: createTimestampPipe(timestampMaskConfig.format), + mask: this.getMaskFromFormat(timestampMaskConfig.format), + placeholderChar: "_" + }; + } + } + + public getMaskFromFormat(format: string): MaskArray { + const mask: MaskArray = []; + for (let i: number = 0; i < format.length; i++) { + if ( + format.charAt(i) === "D" || + format.charAt(i) === "M" || + format.charAt(i) === "Y" || + format.charAt(i) === "H" || + format.charAt(i) === "m" || + format.charAt(i) === "s" + ) { + mask[i] = /\d/; + } else { + mask[i] = format.charAt(i); + } + } + return mask; + } +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.spec.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.spec.ts new file mode 100644 index 0000000000..a17e1e03b1 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.spec.ts @@ -0,0 +1,122 @@ +/* tslint:disable:completed-docs */ +import { createTimestampPipe } from "./timestamp-pipe.fn"; + +describe("createTimestampPipe", () => { + const fullDateTimeLongYearFormat: string = "YYYY-DD-MM HH:mm:ss"; + const fullDateTimeShortYearFormat: string = "DD-MM-YY HH:mm:ss"; + + function assertTimestampsValidity(dateTimeStrings: string[], shouldBeValid: boolean, customFormat?: string): void { + const timestampPipeFn: Function = createTimestampPipe(customFormat); + + for (const dateTimeStr of dateTimeStrings) { + const expectedResult: boolean | string = shouldBeValid ? dateTimeStr : false; + expect(timestampPipeFn(dateTimeStr)).toBe(expectedResult); + } + } + + it("should return a pipe function regardless of whether a custom format is passed or not", () => { + let timestampPipeFn: Function = createTimestampPipe(fullDateTimeLongYearFormat); + expect(typeof timestampPipeFn).toBe("function"); + + timestampPipeFn = createTimestampPipe(fullDateTimeShortYearFormat); + expect(typeof timestampPipeFn).toBe("function"); + + timestampPipeFn = createTimestampPipe(); + expect(typeof timestampPipeFn).toBe("function"); + }); + + describe("with the default format: 'DD-MM-YYYY HH:mm:ss'", () => { + it("should return the same date time string if a part of date time string is correct", () => { + const validDateTimeStrings: string[] = [ + "31", + "29-02", + "29-02-20", + "29-02-200", + "0", // when typing a day starting with 0 + "29-0" // when typing a month starting with 0 + ]; + + assertTimestampsValidity(validDateTimeStrings, true); + }); + + it("should return FALSE if a part of date time string is incorrect", () => { + const invalidDateTimeStrings: string[] = ["02-13", "30-02", "31-04"]; + + assertTimestampsValidity(invalidDateTimeStrings, false); + }); + + it("should return FALSE if the date time string doesn't match the format", () => { + const invalidDateTimeStrings: string[] = [ + "32-12-2000 12:12:12", + "40-12-2000 12:12:12", + "22-13-2000 12:12:12", + "22-20-2000 12:12:12", + "22-11-3000 12:12:12", + "22-11-2000 62:12:12", + "22-11-2000 70:12:12", + "22-11-2000 44:13:12", + "22-11-2000 44:70:12", + "22-11-2000 44:44:62", + "22-11-2000 44:44:70" + ]; + + assertTimestampsValidity(invalidDateTimeStrings, false); + }); + }); + + describe("with a custom format", () => { + it("should return the same date time string if a part of date time string is correct", () => { + let validDateTimeStrings: string[] = [ + "2017-30-12", + "2016-29-02 10:15:20" // leap year + ]; + + assertTimestampsValidity(validDateTimeStrings, true, fullDateTimeLongYearFormat); + + validDateTimeStrings = [ + "22-11-15 12:12:12", + "29-02", + "29-02-16 10:15:20" // leap year + ]; + + assertTimestampsValidity(validDateTimeStrings, true, fullDateTimeShortYearFormat); + }); + + it("should return FALSE if 29 February is given and the format given has no year", () => { + // it is invalid because when there is no year specified, it is assumed to be something recurrent + // and you shouldn't put something recurrent on a Feb 29! + + let validDateTimeStrings: string[] = [ + "29-02" // valid date until no year is entered + ]; + + assertTimestampsValidity(validDateTimeStrings, false, "DD-MM"); + + validDateTimeStrings = [ + "02-29" // valid date until no year is entered + ]; + + assertTimestampsValidity(validDateTimeStrings, false, "MM-DD"); + }); + + it("should return FALSE if the date time string doesn't match the format", () => { + let invalidDateTimeStrings: string[] = [ + "2017/30/12", + "2017/30/12 12:12:12", + "2017-29-02 10:15:20", // non leap year + "31-12-2000" + ]; + + assertTimestampsValidity(invalidDateTimeStrings, false, fullDateTimeLongYearFormat); + + invalidDateTimeStrings = [ + "30/12/17", + "30/12/2017 12:12:12", + "29-02-17 10:15:20", // non leap year + "31-12-2000" + ]; + + assertTimestampsValidity(invalidDateTimeStrings, false, fullDateTimeShortYearFormat); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.ts new file mode 100644 index 0000000000..ea7a776f48 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.ts @@ -0,0 +1,81 @@ +import { starkIsDateTime } from "@nationalbankbelgium/stark-core"; +import { PipeFunction } from "text-mask-core"; + +// TODO: refactor this function to reduce its cognitive complexity +// tslint:disable-next-line:cognitive-complexity +export function createTimestampPipe(timestampFormat: string = "DD-MM-YYYY HH:mm:ss"): PipeFunction { + return (conformedValue: string) => { + const dateFormatArray: string[] = timestampFormat.split(/[^DMYHms]+/); + const maxValue: object = { DD: 31, MM: 12, YYYY: 2999, HH: 24, mm: 60, ss: 60 }; + const minValue: object = { DD: 0, MM: 0, YYYY: 1, HH: 0, mm: 0, ss: 0 }; + + let skipValidation: boolean = false; + + // Check for invalid date + const isInvalid: boolean = dateFormatArray.some((format: string) => { + const position: number = timestampFormat.indexOf(format); + const length: number = format.length; + const textValue: string = conformedValue.substr(position, length).replace(/\D/g, ""); + const value: number = parseInt(textValue, 10); + + // skip the validation if the day starts with 0, but is not 00 + // because if we would validate it would give not valid, because day 0 doesn't exist + // but maybe we want to type for example 02 + // it should not give invalid if we only already have typed the 0 + if (format === "DD" && (value === 0 && textValue !== "00")) { + skipValidation = true; + // same for month + } else if (format === "MM" && (value === 0 && textValue !== "00")) { + skipValidation = true; + } + return value > maxValue[format] || (textValue.length === length && value < minValue[format]); + }); + + // remove all non digits at the end of the conformed value + const inputValue: string = conformedValue.replace(/\D*$/, ""); + const partialFormat: string = timestampFormat.substring(0, inputValue.length); + + // MomentJs gives always false for input 31, but it depends on the month + // so we say it is always true + // if 31 is a month or year or hour than we couldn't even type the 3 + if (inputValue === "31") { + skipValidation = true; + + // 29 february must be checked after we have typed the year if there is a year in the format + } else if (isLeapDay(inputValue, partialFormat, timestampFormat)) { + skipValidation = true; + } + + if (!skipValidation && !isInvalid && inputValue.length > 0 && !starkIsDateTime(inputValue, partialFormat)) { + return false; + } + + skipValidation = false; + + if (isInvalid) { + return false; + } + + return conformedValue; + }; +} + +function isLeapDay(value: string, format: string, fullFormat: string): boolean { + const textValue: string = value.replace(/\D/, ""); // removing all non digits + const dayMonthFormat: string = format.replace(/[^DM]/, ""); // keeping only day and month parts + const leapDays: { format: string; date: string }[] = [{ format: "DDMM", date: "2902" }, { format: "MMDD", date: "0229" }]; + + // is leap day as long as there is no year entered yet and the full format does have a year part + for (const leapDay of leapDays) { + const indexOfDayMonth: number = dayMonthFormat.indexOf(leapDay.format); + if ( + textValue.substr(indexOfDayMonth, 4) === leapDay.date && + dayMonthFormat.substr(indexOfDayMonth, 4) === leapDay.format && + ((fullFormat.indexOf("YYYY") > 0 && value.length < fullFormat.indexOf("YYYY") + 4) || + (fullFormat.indexOf("YY") > 0 && value.length < fullFormat.indexOf("YY") + 2)) + ) { + return true; + } + } + return false; +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/input-mask-directives.module.ts b/packages/stark-ui/src/modules/input-mask-directives/input-mask-directives.module.ts index 8faf26a0c9..f3fb7013a8 100644 --- a/packages/stark-ui/src/modules/input-mask-directives/input-mask-directives.module.ts +++ b/packages/stark-ui/src/modules/input-mask-directives/input-mask-directives.module.ts @@ -1,8 +1,8 @@ import { NgModule } from "@angular/core"; -import { StarkTextMaskDirective } from "./directives"; +import { StarkEmailMaskDirective, StarkNumberMaskDirective, StarkTextMaskDirective, StarkTimestampMaskDirective } from "./directives"; @NgModule({ - declarations: [StarkTextMaskDirective], - exports: [StarkTextMaskDirective] + declarations: [StarkEmailMaskDirective, StarkNumberMaskDirective, StarkTextMaskDirective, StarkTimestampMaskDirective], + exports: [StarkEmailMaskDirective, StarkNumberMaskDirective, StarkTextMaskDirective, StarkTimestampMaskDirective] }) export class StarkInputMaskDirectivesModule {} diff --git a/showcase/src/app/demo-ui/demo-ui.module.ts b/showcase/src/app/demo-ui/demo-ui.module.ts index ca18b837c7..1ef8cf78e6 100644 --- a/showcase/src/app/demo-ui/demo-ui.module.ts +++ b/showcase/src/app/demo-ui/demo-ui.module.ts @@ -33,7 +33,6 @@ import { StarkGenericSearchModule, StarkInputMaskDirectivesModule, StarkKeyboardDirectivesModule, - StarkTransformInputDirectiveModule, StarkLanguageSelectorModule, StarkMinimapModule, StarkPaginationModule, @@ -42,7 +41,8 @@ import { StarkRouteSearchModule, StarkSliderModule, StarkSvgViewBoxModule, - StarkTableModule + StarkTableModule, + StarkTransformInputDirectiveModule } from "@nationalbankbelgium/stark-ui"; import { DemoActionBarPageComponent, @@ -166,7 +166,7 @@ import { TableWithCustomStylingComponent, DemoToastPageComponent, DemoGenericSearchFormComponent, - DemoTransformInputDirectivePageComponent, + DemoTransformInputDirectivePageComponent ], exports: [ DemoActionBarPageComponent, diff --git a/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.html b/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.html index 550532b264..210714e64a 100644 --- a/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.html +++ b/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.html @@ -9,13 +9,7 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

-
+
SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TEXT_MASK.CREDIT_CARD_MASK SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST id="structured-message-input-mask" type="text" [(ngModel)]="structuredMessage" - (input)="logModelChange()" + (input)="logModelChange(structuredMessage)" placeholder="+++###/####/#####/+++" [starkTextMask]="structuredMessageMaskConfig" /> - SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TEXT_MASK.CUSTOM_MASK + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TEXT_MASK.PHONE_NUMBER_MASK SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST
+ + + + + +
+ + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.NUMBER_MASK.EURO_AMOUNT_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.NUMBER_MASK.US_DOLLARS_AMOUNT_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.NUMBER_MASK.PERCENTAGE_MASK + + +
+
+ + + + + +
+ + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.EMAIL_MASK.EMAIL_MASK + + +
+
+ + + + + +
+ + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.TIMESTAMP_DMY_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.TIMESTAMP_MDY_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.FULL_DATE_DMY_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.DAY_MONTH_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.MONTH_DAY_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.TIME_MASK + + +
+
diff --git a/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.ts b/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.ts index 8a59a6317c..e2753bab5d 100644 --- a/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.ts +++ b/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.ts @@ -1,7 +1,7 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormControl } from "@angular/forms"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; -import { StarkTextMasks, StarkTextMaskConfig } from "@nationalbankbelgium/stark-ui"; +import { StarkTextMasks, StarkTextMaskConfig, StarkTimestampMaskConfig, StarkNumberMaskConfig } from "@nationalbankbelgium/stark-ui"; import { ReferenceLink } from "../../../shared/components/reference-block"; @Component({ @@ -13,12 +13,26 @@ export class DemoInputMaskDirectivesPageComponent implements OnInit { public creditCardMaskConfig: StarkTextMaskConfig; public structuredMessageMaskConfig: StarkTextMaskConfig; public customMaskConfig: StarkTextMaskConfig; + public dateMaskConfig: StarkTimestampMaskConfig; + public dollarsMaskConfig: StarkNumberMaskConfig; + public eurosMaskConfig: StarkNumberMaskConfig; + public percentageMaskConfig: StarkNumberMaskConfig; + public timestampDMYMaskConfig: StarkTimestampMaskConfig; + public dayMonthMaskConfig: StarkTimestampMaskConfig; + public monthDayMaskConfig: StarkTimestampMaskConfig; + public timestampMDYMaskConfig: StarkTimestampMaskConfig; + public timeMaskConfig: StarkTimestampMaskConfig; + public structuredMessage: string; + public email: string; + public timestamp: string; public phoneNumberField: FormControl; + public timestampField: FormControl; public referenceList: ReferenceLink[]; public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) { this.phoneNumberField = new FormControl(); + this.timestampField = new FormControl(); this.creditCardMaskConfig = { mask: StarkTextMasks.CREDITCARD_NUMBER @@ -33,6 +47,69 @@ export class DemoInputMaskDirectivesPageComponent implements OnInit { placeholderChar: "#" }; + this.timestampDMYMaskConfig = { + format: "DD-MM-YYYY HH:mm:ss" + }; + + this.timestampMDYMaskConfig = { + format: "MM-DD-YYYY HH:mm:ss" + }; + + this.dateMaskConfig = { + format: "DD-MM-YYYY" + }; + + this.dayMonthMaskConfig = { + format: "DD-MM" + }; + + this.monthDayMaskConfig = { + format: "MM/DD" + }; + + this.timeMaskConfig = { + format: "HH:mm:ss" + }; + + this.eurosMaskConfig = { + prefix: "", + suffix: " €", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 2, + integerLimit: 9, + allowNegative: false, + allowLeadingZeroes: false + }; + + this.dollarsMaskConfig = { + prefix: "$ ", + suffix: "", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 2, + integerLimit: 9, + allowNegative: false, + allowLeadingZeroes: false + }; + + this.percentageMaskConfig = { + prefix: "% ", + suffix: "", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 3, + integerLimit: 3, + allowNegative: true, + allowLeadingZeroes: true + }; + this.referenceList = [ { label: "Stark Text Mask directive", @@ -45,7 +122,23 @@ export class DemoInputMaskDirectivesPageComponent implements OnInit { * Component lifecycle hook */ public ngOnInit(): void { + // combineLatest(this.phoneNumberField.valueChanges, this.timestampField.valueChanges) + // .pipe( + // reduce((acc: string[], values: string[]) => { + // acc.filter((currentValue: string) => values.findIndex((value: string) => value === currentValue)); + // }, []), + // map((values: string[]) => { + // console.log("CCR==========> values after reduce", values); + // return values[0]; + // }), + // tap((changedValue: string) => { + // this.logger.debug("formControl value changed: ", changedValue); + // }) + // ) + // .subscribe(); + this.phoneNumberField.valueChanges.subscribe((value: string) => this.logger.debug("phoneNumberField value changed: ", value)); + this.timestampField.valueChanges.subscribe((value: string) => this.logger.debug("timestampField value changed: ", value)); this.logger.debug("DemoInputMaskDirectivesComponent - initialized"); } @@ -54,7 +147,7 @@ export class DemoInputMaskDirectivesPageComponent implements OnInit { this.logger.debug("creditCard value changed", (event.srcElement).value); } - public logModelChange(): void { - this.logger.debug("structuredMessage value changed", this.structuredMessage); + public logModelChange(model: any): void { + this.logger.debug("model value changed", model); } } diff --git a/showcase/src/assets/translations/en.json b/showcase/src/assets/translations/en.json index f96531122b..51ac0af583 100644 --- a/showcase/src/assets/translations/en.json +++ b/showcase/src/assets/translations/en.json @@ -96,11 +96,36 @@ }, "DIRECTIVES": { "INPUT_MASK": { + "EMAIL_MASK": { + "TITLE": "Email Mask directive", + "EMAIL_MASK": "Email" + }, + "NUMBER_MASK": { + "TITLE": "Number Mask directive", + "US_DOLLARS_AMOUNT_MASK": "Amount in US Dollars ", + "EURO_AMOUNT_MASK": "Amount in Euros", + "PERCENTAGE_MASK": "Percentage" + }, "TEXT_MASK": { "TITLE": "Text Mask directive", - "CREDIT_CARD_MASK": "Credit Card", - "STRUCTURED_MESSAGE_MASK": "Structured Communication Message", - "CUSTOM_MASK": "Custom Mask" + "CREDIT_CARD_MASK": "Credit card", + "STRUCTURED_MESSAGE_MASK": "Structured communication message", + "PHONE_NUMBER_MASK": "Numéro de téléphone" + }, + "TIMESTAMP_MASK": { + "TITLE": "Timestamp Mask directive", + "TIMESTAMP_DMY_MASK": "Timestamp in DMY format", + "TIMESTAMP_DMY_PLACEHOLDER": "DD-MM-YYYY HH:mm:ss", + "TIMESTAMP_MDY_MASK": "Timestamp in MDY format", + "TIMESTAMP_MDY_PLACEHOLDER": "MM-DD-YYYY HH:mm:ss", + "FULL_DATE_DMY_MASK": "Date in DMY format", + "FULL_DATE_DMY_PLACEHOLDER": "DD-MM-YYYY", + "DAY_MONTH_MASK": "Day-Month", + "DAY_MONTH_PLACEHOLDER": "DD-MM", + "MONTH_DAY_MASK": "Month / Day", + "MONTH_DAY_PLACEHOLDER": "MM/DD", + "TIME_MASK": "Time", + "TIME_PLACEHOLDER": "HH:mm:ss" }, "TITLE": "Input Mask directives" }, diff --git a/showcase/src/assets/translations/fr.json b/showcase/src/assets/translations/fr.json index 2eb1ebf4d0..2730188392 100644 --- a/showcase/src/assets/translations/fr.json +++ b/showcase/src/assets/translations/fr.json @@ -96,11 +96,36 @@ }, "DIRECTIVES": { "INPUT_MASK": { + "EMAIL_MASK": { + "TITLE": "Email Mask directive", + "EMAIL_MASK": "Email" + }, + "NUMBER_MASK": { + "TITLE": "Number Mask directive", + "US_DOLLARS_AMOUNT_MASK": "Montant en US Dollars ", + "EURO_AMOUNT_MASK": "Montant en Euros", + "PERCENTAGE_MASK": "Percentage" + }, "TEXT_MASK": { "TITLE": "Text Mask directive", "CREDIT_CARD_MASK": "Carte de crédit", "STRUCTURED_MESSAGE_MASK": "Message de communication structurée", - "CUSTOM_MASK": "Masque personnalisé" + "PHONE_NUMBER_MASK": "Numéro de téléphone" + }, + "TIMESTAMP_MASK": { + "TITLE": "Timestamp Mask directive", + "TIMESTAMP_DMY_MASK": "Horodatage au format JMA", + "TIMESTAMP_DMY_PLACEHOLDER": "DD-MM-YYYY HH:mm:ss", + "TIMESTAMP_MDY_MASK": "Horodatage au format MJA", + "TIMESTAMP_MDY_PLACEHOLDER": "MM-DD-YYYY HH:mm:ss", + "FULL_DATE_DMY_MASK": "Date au format JMA", + "FULL_DATE_DMY_PLACEHOLDER": "DD-MM-YYYY", + "DAY_MONTH_MASK": "Jour-Mois", + "DAY_MONTH_PLACEHOLDER": "DD-MM", + "MONTH_DAY_MASK": "Mois / Jour", + "MONTH_DAY_PLACEHOLDER": "MM/DD", + "TIME_MASK": "Temps", + "TIME_PLACEHOLDER": "HH:mm:ss" }, "TITLE": "Input Mask directives" }, diff --git a/showcase/src/assets/translations/nl.json b/showcase/src/assets/translations/nl.json index 956f8d8041..dc6d457d35 100644 --- a/showcase/src/assets/translations/nl.json +++ b/showcase/src/assets/translations/nl.json @@ -96,11 +96,36 @@ }, "DIRECTIVES": { "INPUT_MASK": { + "EMAIL_MASK": { + "TITLE": "Email Mask directive", + "EMAIL_MASK": "Email" + }, + "NUMBER_MASK": { + "TITLE": "Number Mask directive", + "US_DOLLARS_AMOUNT_MASK": "Bedrag in US Dollars ", + "EURO_AMOUNT_MASK": "Bedrag in Euros", + "PERCENTAGE_MASK": "Percentage" + }, "TEXT_MASK": { "TITLE": "Text Mask directive", "CREDIT_CARD_MASK": "Kredietkaart", "STRUCTURED_MESSAGE_MASK": "Gestructureerde mededeling", - "CUSTOM_MASK": "Aangepast masker" + "PHONE_NUMBER_MASK": "Telefoonnummer" + }, + "TIMESTAMP_MASK": { + "TITLE": "Timestamp Mask directive", + "TIMESTAMP_DMY_MASK": "Timestamp in DMJ format", + "TIMESTAMP_DMY_PLACEHOLDER": "DD-MM-YYYY HH:mm:ss", + "TIMESTAMP_MDY_MASK": "Timestamp in MDJ format", + "TIMESTAMP_MDY_PLACEHOLDER": "MM-DD-YYYY HH:mm:ss", + "FULL_DATE_DMY_MASK": "Datum in DMJ format", + "FULL_DATE_DMY_PLACEHOLDER": "DD-MM-YYYY", + "DAY_MONTH_MASK": "Dag-Maand", + "DAY_MONTH_PLACEHOLDER": "DD-MM", + "MONTH_DAY_MASK": "Maand / Dag", + "MONTH_DAY_PLACEHOLDER": "MM/DD", + "TIME_MASK": "Tijd", + "TIME_PLACEHOLDER": "HH:mm:ss" }, "TITLE": "Input Mask directives" },