From 6f7fb72dc7042717e82a9d825651d4f7743f7271 Mon Sep 17 00:00:00 2001 From: christophercr Date: Fri, 24 May 2019 18:22:48 +0200 Subject: [PATCH] chore(stark-ui): more changes for the Stark Date Range picker --- .../date-range-picker.component.spec.ts | 620 ++++++++++++------ .../components/date-range-picker.component.ts | 307 ++++++--- ...demo-date-range-picker-page.component.html | 21 +- .../demo-date-range-picker-page.component.ts | 98 +-- .../date-range-picker/form-group.html | 8 +- .../examples/date-range-picker/form-group.ts | 66 +- .../examples/date-range-picker/model.html | 6 +- .../examples/date-range-picker/model.ts | 66 +- 8 files changed, 775 insertions(+), 417 deletions(-) diff --git a/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.spec.ts b/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.spec.ts index f4d5d671d1..cb05c7663e 100644 --- a/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.spec.ts +++ b/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.spec.ts @@ -8,40 +8,81 @@ import { MAT_MOMENT_DATE_FORMATS, MomentDateAdapter } from "@angular/material-mo import { MatDatepickerModule } from "@angular/material/datepicker"; import { MatFormFieldModule } from "@angular/material/form-field"; import { TranslateModule } from "@ngx-translate/core"; -import { STARK_LOGGING_SERVICE, STARK_ROUTING_SERVICE } from "@nationalbankbelgium/stark-core"; -import { MockStarkLoggingService, MockStarkRoutingService } from "@nationalbankbelgium/stark-core/testing"; -import { StarkDatePickerComponent } from "../../date-picker"; +import { STARK_LOGGING_SERVICE } from "@nationalbankbelgium/stark-core"; +import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing"; +import { StarkDatePickerComponent } from "../../date-picker/components"; +import { StarkTimestampMaskDirective } from "../../input-mask-directives/directives"; import { StarkDateRangePickerComponent } from "./date-range-picker.component"; import { StarkDateRangePickerEvent } from "./date-range-picker-event.intf"; -import { StarkTimestampMaskDirective } from "../../input-mask-directives"; +import { Observer } from "rxjs"; import moment from "moment"; +import Spy = jasmine.Spy; +import SpyObj = jasmine.SpyObj; +import createSpyObj = jasmine.createSpyObj; describe("DateRangePickerComponent", () => { + @Component({ + selector: "test-model", + template: ` + + ` + }) + class TestModelComponent { + @ViewChild(StarkDateRangePickerComponent) + public dateRangePicker!: StarkDateRangePickerComponent; + + public dateRange = {}; + } + + @Component({ + selector: "test-form-group", + template: ` + + START-ERROR + END-ERROR + + ` + }) + class TestFormGroupComponent { + @ViewChild(StarkDateRangePickerComponent) + public dateRangePicker!: StarkDateRangePickerComponent; + + public formGroup = new FormGroup({ + startDate: new FormControl(), + endDate: new FormControl() + }); + } + + beforeEach(async(() => { + return TestBed.configureTestingModule({ + declarations: [ + StarkTimestampMaskDirective, + StarkDatePickerComponent, + StarkDateRangePickerComponent, + TestModelComponent, + TestFormGroupComponent + ], + imports: [ + NoopAnimationsModule, + MatDatepickerModule, + MatFormFieldModule, + FormsModule, + ReactiveFormsModule, + TranslateModule.forRoot() + ], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }, + { provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS }, + { provide: MAT_DATE_LOCALE, useValue: "en-us" }, + { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] } + ] + }).compileComponents(); + })); + describe("uncontrolled", () => { let fixture: ComponentFixture; let component: StarkDateRangePickerComponent; - beforeEach(async(() => { - return TestBed.configureTestingModule({ - declarations: [StarkTimestampMaskDirective, StarkDatePickerComponent, StarkDateRangePickerComponent], - imports: [ - NoopAnimationsModule, - MatDatepickerModule, - MatFormFieldModule, - FormsModule, - ReactiveFormsModule, - TranslateModule.forRoot() - ], - providers: [ - { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }, - { provide: STARK_ROUTING_SERVICE, useClass: MockStarkRoutingService }, - { provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS }, - { provide: MAT_DATE_LOCALE, useValue: "en-us" }, - { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] } - ] - }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(StarkDateRangePickerComponent); component = fixture.componentInstance; @@ -79,262 +120,443 @@ describe("DateRangePickerComponent", () => { }); }); - describe("datepickers properties binding", () => { - it("the ids of the datepickers should be set correctly", () => { - component.rangePickerId = "test-id"; - fixture.detectChanges(); - let input: HTMLElement = fixture.nativeElement.querySelector("#test-id-start-input"); - expect(input).not.toBeNull(); - input = fixture.nativeElement.querySelector("#test-id-end-input"); - expect(input).not.toBeNull(); - let picker: HTMLElement = fixture.nativeElement.querySelector("#test-id-start"); - expect(picker).not.toBeNull(); - picker = fixture.nativeElement.querySelector("#test-id-end"); - expect(picker).not.toBeNull(); - }); + describe("date pickers properties", () => { + let mockObserver: SpyObj>; - it("the names of the datepickers should be set correctly", () => { - component.rangePickerName = "test-name"; - fixture.detectChanges(); - let input: HTMLElement = fixture.nativeElement.querySelector('[name="test-name-start"]'); - expect(input).not.toBeNull(); - input = fixture.nativeElement.querySelector('[name="test-name-end"]'); - expect(input).not.toBeNull(); + beforeEach(() => { + mockObserver = createSpyObj>("observerSpy", ["next", "error", "complete"]); }); - it("the datepickers should be disabled when isDisabled is true", () => { - component.disabled = true; - fixture.detectChanges(); - expect(component.startPicker.pickerInput.disabled).toBe(true); - expect(component.endPicker.pickerInput.disabled).toBe(true); - }); + it("should be set correctly according to the given inputs and WITHOUT triggering a 'dateRangeChanged' event", () => { + component.dateRangeChanged.subscribe(mockObserver); - it("the placeholders of the datepickers should be set correctly", () => { + component.rangePickerId = "test-id"; + component.rangePickerName = "test-name"; component.startDateLabel = "startDateLabel"; component.endDateLabel = "endDateLabel"; - fixture.detectChanges(); - let input: HTMLElement = fixture.nativeElement.querySelector('[ng-reflect-placeholder="startDateLabel"]'); - expect(input).not.toBeNull(); - input = fixture.nativeElement.querySelector('[ng-reflect-placeholder="endDateLabel"]'); - expect(input).not.toBeNull(); - }); - - it("the datepickers min date should be set correctly", () => { const minDate = new Date(2018, 6, 1); component.startMinDate = minDate; component.endMinDate = minDate; + const maxDate = new Date(2018, 6, 2); + component.startMaxDate = maxDate; + component.endMaxDate = maxDate; fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector("#test-id-start-input")).toBeTruthy(); + expect(fixture.nativeElement.querySelector("#test-id-end-input")).toBeTruthy(); + expect(fixture.nativeElement.querySelector("#test-id-start")).toBeTruthy(); + expect(fixture.nativeElement.querySelector("#test-id-end")).toBeTruthy(); + expect(fixture.nativeElement.querySelector('[name="test-name-start"]')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('[name="test-name-end"]')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('[ng-reflect-placeholder="startDateLabel"]')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('[ng-reflect-placeholder="endDateLabel"]')).toBeTruthy(); expect(component.startPicker.pickerInput.min).not.toBeNull(); expect((component.startPicker.pickerInput.min).toDate()).toEqual(minDate); expect(component.endPicker.pickerInput.min).not.toBeNull(); expect((component.endPicker.pickerInput.min).toDate()).toEqual(minDate); - }); - - it("the datepickers max date should be set correctly", () => { - const maxDate = new Date(2018, 6, 2); - component.startMaxDate = maxDate; - component.endMaxDate = maxDate; - fixture.detectChanges(); expect(component.startPicker.pickerInput.max).not.toBeNull(); expect((component.startPicker.pickerInput.max).toDate()).toEqual(maxDate); expect(component.endPicker.pickerInput.max).not.toBeNull(); expect((component.endPicker.pickerInput.max).toDate()).toEqual(maxDate); + + expect(mockObserver.next).not.toHaveBeenCalled(); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); }); - it("the datepickers value should be set correctly", fakeAsync(() => { + it("the date pickers should be disabled when 'disabled' is true and it should NOT emit a 'dateRangeChanged' event", () => { + component.dateRangeChanged.subscribe(mockObserver); + + component.disabled = true; + fixture.detectChanges(); + expect(component.startPicker.pickerInput.disabled).toBe(true); + expect(component.endPicker.pickerInput.disabled).toBe(true); + + expect(mockObserver.next).not.toHaveBeenCalled(); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); + + it("the date pickers value should be set correctly and they should emit a 'dateRangeChanged' event", () => { + component.dateRangeChanged.subscribe(mockObserver); + const date = new Date(2018, 6, 3); component.startDate = date; - component.endDate = date; fixture.detectChanges(); - tick(); - expect(component.startPicker.value).not.toBeNull("The value of the startDate date-picker should be set."); + expect(component.startPicker.value).not.toBeNull(); expect(component.startPicker.value).toEqual(date); - expect(component.endPicker.value).not.toBeNull("The value of the endDate date-picker should be set."); + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ startDate: date, endDate: undefined }); + + mockObserver.next.calls.reset(); + component.endDate = date; + fixture.detectChanges(); + + expect(component.endPicker.value).not.toBeNull(); expect(component.endPicker.value).toEqual(date); - })); + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ startDate: date, endDate: date }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); }); describe("dates selection", () => { - it("the end date should be invalid if before startdate", () => { - component.startDate = new Date(2018, 6, 5); - component.endDate = new Date(2018, 6, 4); - fixture.detectChanges(); + let mockObserver: SpyObj>; - expect(component.endDateFormControl.status).toBe("INVALID"); + beforeEach(() => { + mockObserver = createSpyObj>("observerSpy", ["next", "error", "complete"]); }); - it("the end date should be correctly set if after the start date", () => { + it("the end date should be correctly set if after the start date and emit the new value in the 'dateRangeChanged' output", () => { + // initialize start date + const startDate = new Date(2018, 6, 6); + component.startPicker.picker.select(moment(startDate)); // select a date in the internal date picker + fixture.detectChanges(); + component.dateRangeChanged.subscribe(mockObserver); + + expect(component.startDate).toEqual(startDate); const endDate = new Date(2018, 6, 7); - component.startDate = new Date(2018, 6, 6); - component.endDate = endDate; + component.endPicker.picker.select(moment(endDate)); // select a date in the internal date picker fixture.detectChanges(); expect(component.endDate).toEqual(endDate); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ + startDate: startDate, + endDate: endDate + }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); }); - it("the end date should be correctly set if after the start date is undefined", () => { + it("the end date should be correctly set if start date is undefined and emit the new value in the 'dateRangeChanged' output", () => { + // initialize start date + component.startPicker.picker.select(undefined); // select a date in the internal date picker + fixture.detectChanges(); + component.dateRangeChanged.subscribe(mockObserver); + + expect(component.startDate).toBeUndefined(); const endDate = new Date(2018, 6, 8); - component.startDate = undefined; - component.endDate = endDate; + component.endPicker.picker.select(moment(endDate)); // select a date in the internal date picker fixture.detectChanges(); expect(component.endDate).toEqual(endDate); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ + startDate: undefined, + endDate: endDate + }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); }); - }); - }); - @Component({ - selector: "test-model", - template: ` - - ` - }) - class TestModelComponent { - @ViewChild(StarkDateRangePickerComponent) - public dateRangePicker!: StarkDateRangePickerComponent; + it("the end date should be still valid if it is before the startDate BUT the startDate should be set to undefined and it should emit the new value in the 'dateRangeChanged' output", () => { + // initialize start date + const startDate = new Date(2018, 6, 5); + component.startPicker.picker.select(moment(startDate)); // select a date in the internal date picker + fixture.detectChanges(); + component.dateRangeChanged.subscribe(mockObserver); - public dateRange = {}; - } + expect(component.startDate).toEqual(startDate); + const endDate = new Date(2018, 6, 4); + component.endPicker.picker.select(moment(endDate)); // select a date in the internal date picker + fixture.detectChanges(); + + expect(component.startDate).toBeUndefined(); + expect(component.endDate).toEqual(endDate); + expect(component.endDateFormControl.status).toBe("VALID"); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ + startDate: undefined, + endDate: endDate + }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); + + it("the start date should be still valid if it is after the endDate BUT the endDate should be set to undefined and it should emit the new value in the 'dateRangeChanged' output", () => { + // initialize end date + const endDate = new Date(2018, 6, 5); + component.endPicker.picker.select(moment(endDate)); // select a date in the internal date picker + fixture.detectChanges(); + component.dateRangeChanged.subscribe(mockObserver); + + expect(component.endDate).toEqual(endDate); + const startDate = new Date(2018, 6, 6); + component.startPicker.picker.select(moment(startDate)); // select a date in the internal date picker + fixture.detectChanges(); + + expect(component.endDate).toBeUndefined(); + expect(component.startDate).toEqual(startDate); + expect(component.startDateFormControl.status).toBe("VALID"); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ + startDate: startDate, + endDate: undefined + }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); + }); + }); describe("with ngModel", () => { - let fixture: ComponentFixture; + let hostFixture: ComponentFixture; let hostComponent: TestModelComponent; let component: StarkDateRangePickerComponent; - - beforeEach(async(() => - TestBed.configureTestingModule({ - declarations: [StarkTimestampMaskDirective, StarkDatePickerComponent, StarkDateRangePickerComponent, TestModelComponent], - imports: [ - NoopAnimationsModule, - MatDatepickerModule, - MatFormFieldModule, - FormsModule, - ReactiveFormsModule, - TranslateModule.forRoot() - ], - providers: [ - { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }, - { provide: STARK_ROUTING_SERVICE, useClass: MockStarkRoutingService }, - { provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS }, - { provide: MAT_DATE_LOCALE, useValue: "en-us" }, - { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] } - ] - }).compileComponents())); + let mockObserver: SpyObj>; beforeEach(() => { - fixture = TestBed.createComponent(TestModelComponent); - hostComponent = fixture.componentInstance; + hostFixture = TestBed.createComponent(TestModelComponent); + hostComponent = hostFixture.componentInstance; component = hostComponent.dateRangePicker; + hostFixture.detectChanges(); + + mockObserver = createSpyObj>("observerSpy", ["next", "error", "complete"]); }); - it("should update when model is updated", fakeAsync(() => { - const expected = { startDate: new Date(2019, 0, 1), endDate: new Date(2019, 0, 2) }; + it("should update when model is updated and it should not emit a 'dateRangeChanged' event", fakeAsync(() => { + component.dateRangeChanged.subscribe(mockObserver); + const dateRange = { startDate: new Date(2019, 0, 1), endDate: new Date(2019, 0, 2) }; - hostComponent.dateRange = expected; - fixture.detectChanges(); + hostComponent.dateRange = dateRange; + hostFixture.detectChanges(); tick(); expect(component.startDate).toBeDefined(); + expect(component.startDate).toEqual(dateRange.startDate); expect(component.endDate).toBeDefined(); + expect(component.endDate).toEqual(dateRange.endDate); - expect(component.startDate).toEqual(expected.startDate); - expect(component.endDate).toEqual(expected.endDate); + expect(mockObserver.next).not.toHaveBeenCalled(); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); })); }); - @Component({ - selector: "test-form-group", - template: ` - - START-ERROR - END-ERROR - - ` - }) - class TestFormGroupComponent { - @ViewChild(StarkDateRangePickerComponent) - public dateRangePicker!: StarkDateRangePickerComponent; - - public formGroup = new FormGroup({ - startDate: new FormControl(), - endDate: new FormControl() - }); - } - describe("with formGroup", () => { - let fixture: ComponentFixture; + let hostFixture: ComponentFixture; let hostComponent: TestFormGroupComponent; let component: StarkDateRangePickerComponent; - beforeEach(async(() => - TestBed.configureTestingModule({ - declarations: [ - StarkTimestampMaskDirective, - StarkDatePickerComponent, - StarkDateRangePickerComponent, - TestFormGroupComponent - ], - imports: [ - NoopAnimationsModule, - MatDatepickerModule, - MatFormFieldModule, - FormsModule, - ReactiveFormsModule, - TranslateModule.forRoot() - ], - providers: [ - { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }, - { provide: STARK_ROUTING_SERVICE, useClass: MockStarkRoutingService }, - { provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS }, - { provide: MAT_DATE_LOCALE, useValue: "en-us" }, - { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] } - ] - }).compileComponents())); - beforeEach(() => { - fixture = TestBed.createComponent(TestFormGroupComponent); - hostComponent = fixture.componentInstance; + hostFixture = TestBed.createComponent(TestFormGroupComponent); + hostComponent = hostFixture.componentInstance; component = hostComponent.dateRangePicker; + hostFixture.detectChanges(); }); - it("should update when form group is updated", () => { - const expected = { startDate: new Date(2019, 0, 1), endDate: new Date(2019, 0, 2) }; + describe("date pickers properties", () => { + let mockObserver: SpyObj>; - hostComponent.formGroup.setValue(expected); - fixture.detectChanges(); + beforeEach(() => { + mockObserver = createSpyObj>("observerSpy", ["next", "error", "complete"]); + }); - expect(component.startDate).toBeDefined(); - expect(component.endDate).toBeDefined(); + it("the date pickers should be disabled when the form controls are disabled AND a 'valueChange' event should be triggered ONLY IF the 'emitEvent' option is enabled", () => { + hostComponent.formGroup.valueChanges.subscribe(mockObserver); + + hostComponent.formGroup.disable({ emitEvent: false }); + hostFixture.detectChanges(); + + expect(component.startPicker.pickerInput.disabled).toBe(true); + expect(component.endPicker.pickerInput.disabled).toBe(true); + + hostComponent.formGroup.enable({ emitEvent: false }); + hostFixture.detectChanges(); + + expect(component.startPicker.pickerInput.disabled).toBe(false); + expect(component.endPicker.pickerInput.disabled).toBe(false); + expect(mockObserver.next).not.toHaveBeenCalled(); // because the 'emitEvent' is false + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + + hostComponent.formGroup.disable(); // 'emitEvent' true by default + hostFixture.detectChanges(); - expect(component.startDate).toEqual(expected.startDate); - expect(component.endDate).toEqual(expected.endDate); + expect(component.startPicker.pickerInput.disabled).toBe(true); + expect(component.endPicker.pickerInput.disabled).toBe(true); + expect(mockObserver.next).toHaveBeenCalledTimes(1); + mockObserver.next.calls.reset(); + + hostComponent.formGroup.enable(); // 'emitEvent' true by default + hostFixture.detectChanges(); + + expect(component.startPicker.pickerInput.disabled).toBe(false); + expect(component.endPicker.pickerInput.disabled).toBe(false); + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); + + it("should update start and end dates when values in 'rangeFormGroup' are updated", () => { + const startDate = new Date(2019, 0, 1); + const endDate = new Date(2019, 0, 2); + + hostComponent.formGroup.setValue({ startDate, endDate }); + hostFixture.detectChanges(); + + expect(component.startDate).toBeDefined(); + expect(component.startDate).toEqual(startDate); + expect(component.endDate).toBeDefined(); + expect(component.endDate).toEqual(endDate); + }); + + it("the start and end dates should be the same as the values set on the form controls of the 'rangeFormGroup'", () => { + const startDate = new Date(2018, 6, 3); + const endDate = new Date(2018, 7, 3); + hostComponent.formGroup.controls["startDate"].setValue(startDate); + hostComponent.formGroup.controls["endDate"].setValue(endDate); + hostFixture.detectChanges(); + + expect(component.startDate).toBe(startDate); + expect(component.endDate).toBe(endDate); + }); + + it("should log an error when the given 'rangeFormGroup' does not contain expected 'startDate' and 'endDate' controls", () => { + hostComponent.formGroup = new FormGroup({ + start: new FormControl(new Date(2019, 0, 1)), + end: new FormControl(new Date(2019, 0, 2)) + }); + hostFixture.detectChanges(); + + expect(component.logger.error).toHaveBeenCalledTimes(1); + const errorMessage: string = (component.logger.error).calls.argsFor(0)[0]; + expect(errorMessage).toMatch(/formGroup.*startDate.*endDate/); + }); + + it("should show errors at the correct input", () => { + const { startDate: startDateFormControl, endDate: endDateFormControl } = hostComponent.formGroup.controls; + const alwaysFail = (): ValidationErrors => ({ alwaysFail: "error" }); + + startDateFormControl.setValidators(alwaysFail); + startDateFormControl.setValue(new Date()); + startDateFormControl.markAsTouched(); + + endDateFormControl.setValidators(alwaysFail); + endDateFormControl.setValue(new Date()); + endDateFormControl.markAsTouched(); + + hostFixture.detectChanges(); + + const startDateError = hostFixture.nativeElement.querySelectorAll("mat-form-field mat-error").item(0); + expect(startDateError).not.toBeNull(); + expect(startDateError.textContent).toEqual("START-ERROR"); + + const endDateError = hostFixture.nativeElement.querySelectorAll("mat-form-field mat-error").item(1); + expect(endDateError).not.toBeNull(); + expect(endDateError.textContent).toEqual("END-ERROR"); + }); }); - it("should show errors at the correct input", () => { - const { startDate: startDateFC, endDate: endDateFC } = hostComponent.formGroup.controls; - const alwaysFail = (): ValidationErrors => ({ alwaysFail: "error" }); + describe("dates selection", () => { + let mockObserver: SpyObj>; - startDateFC.setValidators(alwaysFail); - startDateFC.setValue(new Date()); - startDateFC.markAsTouched(); - startDateFC.markAsDirty(); + beforeEach(() => { + mockObserver = createSpyObj>("observerSpy", ["next", "error", "complete"]); + }); - endDateFC.setValidators(alwaysFail); - endDateFC.setValue(new Date()); - endDateFC.markAsTouched(); - endDateFC.markAsDirty(); + it("the end date should be correctly set if after the start date and emit the new value in the form control's 'valueChange' observable", () => { + // initialize start date + const startDate = new Date(2018, 6, 6); + component.startPicker.picker.select(moment(startDate)); // select a date in the internal date picker + hostFixture.detectChanges(); + hostComponent.formGroup.valueChanges.subscribe(mockObserver); - fixture.detectChanges(); + expect(component.startDate).toEqual(startDate); + const endDate = new Date(2018, 6, 7); + component.endPicker.picker.select(moment(endDate)); // select a date in the internal date picker + hostFixture.detectChanges(); + + expect(component.endDate).toEqual(endDate); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ + startDate: startDate, + endDate: endDate + }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); + + it("the end date should be correctly set if start date is undefined and emit the new value in the form control's 'valueChange' observable", () => { + // initialize start date + component.startPicker.picker.select(undefined); // select a date in the internal date picker + hostFixture.detectChanges(); + hostComponent.formGroup.valueChanges.subscribe(mockObserver); + + expect(component.startDate).toBeUndefined(); + const endDate = new Date(2018, 6, 8); + component.endPicker.picker.select(moment(endDate)); // select a date in the internal date picker + hostFixture.detectChanges(); + + expect(component.endDate).toEqual(endDate); - const startDateError = fixture.nativeElement.querySelectorAll("mat-form-field mat-error").item(0); - expect(startDateError).not.toBeNull(); - expect(startDateError.textContent).toEqual("START-ERROR"); + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ + startDate: null, // TODO: null is emitted instead of undefined because it seems Angular Forms work internally with null initial values rather than undefined + endDate: endDate + }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); + + it("the end date should be still valid if it is before the startDate BUT the startDate should be set to undefined and it should emit the new value in the form control's 'valueChange' observable", () => { + // initialize start date + const startDate = new Date(2018, 6, 5); + component.startPicker.picker.select(moment(startDate)); // select a date in the internal date picker + hostFixture.detectChanges(); + hostComponent.formGroup.valueChanges.subscribe(mockObserver); + + expect(component.startDate).toEqual(startDate); + const endDate = new Date(2018, 6, 4); + component.endPicker.picker.select(moment(endDate)); // select a date in the internal date picker + hostFixture.detectChanges(); + + expect(component.startDate).toBeUndefined(); + expect(component.endDate).toEqual(endDate); + expect(component.endDateFormControl.status).toBe("VALID"); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ + startDate: undefined, + endDate: endDate + }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); + + it("the start date should be still valid if it is after the endDate BUT the endDate should be set to undefined and it should emit the new value in the form control's 'valueChange' observable", () => { + // initialize end date + const endDate = new Date(2018, 6, 5); + component.endPicker.picker.select(moment(endDate)); // select a date in the internal date picker + hostFixture.detectChanges(); + hostComponent.formGroup.valueChanges.subscribe(mockObserver); + + expect(component.endDate).toEqual(endDate); + const startDate = new Date(2018, 6, 6); + component.startPicker.picker.select(moment(startDate)); // select a date in the internal date picker + hostFixture.detectChanges(); - const endDateError = fixture.nativeElement.querySelectorAll("mat-form-field mat-error").item(1); - expect(endDateError).not.toBeNull(); - expect(endDateError.textContent).toEqual("END-ERROR"); + expect(component.endDate).toBeUndefined(); + expect(component.startDate).toEqual(startDate); + expect(component.startDateFormControl.status).toBe("VALID"); + + expect(mockObserver.next).toHaveBeenCalledTimes(1); + expect(mockObserver.next).toHaveBeenCalledWith({ + startDate: startDate, + endDate: undefined + }); + expect(mockObserver.error).not.toHaveBeenCalled(); + expect(mockObserver.complete).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.ts b/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.ts index 3062410773..5923ad7bc9 100644 --- a/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.ts +++ b/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.ts @@ -1,6 +1,6 @@ /* tslint:disable:no-null-keyword */ import { - AfterViewInit, + ChangeDetectorRef, Component, ElementRef, EventEmitter, @@ -15,13 +15,23 @@ import { ViewChild, ViewEncapsulation } from "@angular/core"; -import { ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR, NgControl, ValidatorFn, Validators } from "@angular/forms"; +import { + AbstractControl, + ControlValueAccessor, + FormControl, + FormGroup, + NG_VALUE_ACCESSOR, + NgControl, + ValidatorFn, + Validators +} from "@angular/forms"; import { Subscription } from "rxjs"; -import noop from "lodash-es/noop"; +import { distinctUntilChanged } from "rxjs/operators"; import get from "lodash-es/get"; +import isEqual from "lodash-es/isEqual"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; import { AbstractStarkUiComponent } from "../../../common/classes/abstract-component"; -import { StarkDatePickerComponent, StarkDatePickerFilter, StarkDatePickerMaskConfig } from "../../date-picker"; +import { StarkDatePickerComponent, StarkDatePickerFilter, StarkDatePickerMaskConfig } from "../../date-picker/components"; import { StarkDateRangePickerEvent } from "./date-range-picker-event.intf"; /** @@ -48,51 +58,7 @@ const componentName = "stark-date-range-picker"; } ] }) -export class StarkDateRangePickerComponent extends AbstractStarkUiComponent - implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { - /** - * ControlValueAccessor listener - * @ignore - */ - private _onTouched: () => void = noop; - - /** - * ControlValueAccessor listener - * @ignore - */ - private _onChange: (_dateRange: StarkDateRangePickerEvent) => void = noop; - - /** - * Subscriptions to be removed at end of component lifecycle - * @ignore - */ - private subs: Subscription[] = []; - - /*--- VIEW CHILDREN ---*/ - /** - * Reference to the start datepicker embedded in this component - */ - @ViewChild("startPicker") - public startPicker!: StarkDatePickerComponent; - - /** - * Reference to the end datepicker embedded in this component - */ - @ViewChild("endPicker") - public endPicker!: StarkDatePickerComponent; - - /*--- START-DATE CONFIGURATIONS ---*/ - - /** - * @ignore - * @internal - */ - private _startBeforeEndValidator: ValidatorFn = ({ value }) => { - return value instanceof Date && this.endDate instanceof Date && value.getTime() > this.endDate.getTime() - ? { startBeforeEnd: true } - : null; - }; - +export class StarkDateRangePickerComponent extends AbstractStarkUiComponent implements ControlValueAccessor, OnInit, OnDestroy { /** * Source Date to be bound to the start datepicker model */ @@ -124,7 +90,7 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent /** * @ignore */ - private _startDate = new FormControl(); + private _startDate!: FormControl; // will be defined by '_setupFormControls()' called in the constructor /** * Label to be displayed in the end datepicker @@ -144,18 +110,6 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent @Input() public startMaxDate?: Date; - /*--- END-DATE CONFIGURATIONS ---*/ - - /** - * @ignore - * @internal - */ - private _endAfterStartValidator: ValidatorFn = ({ value }) => { - return value instanceof Date && this.startDate instanceof Date && value.getTime() < this.startDate.getTime() - ? { endAfterStart: true } - : null; - }; - /** * Source Date to be bound to the end datepicker model */ @@ -165,7 +119,7 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent } public set endDate(value: Date | undefined) { - this._endDate.setValue(value || null); + this._endDate.setValue(value); } /** @@ -187,7 +141,7 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent /** * @ignore */ - private _endDate = new FormControl(); + private _endDate!: FormControl; // will be defined by '_setupFormControls()' called in the constructor /** * Label to be displayed in the end datepicker @@ -199,7 +153,16 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent * Minimum date of the end date picker */ @Input() - public endMinDate?: Date; + public set endMinDate(date: Date | undefined) { + this._endMinDate = date; + } + + public get endMinDate(): Date | undefined { + // use the startDate when defined to provide better user experience :) + return this.startDate || this._endMinDate; + } + + private _endMinDate?: Date; /** * Maximum date of the end date picker @@ -207,8 +170,6 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent @Input() public endMaxDate?: Date; - /*--- SHARED CONFIGURATIONS ---*/ - /** * Input to manage both start date and end date. */ @@ -216,14 +177,15 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent public set rangeFormGroup(val: FormGroup) { const { startDate, endDate } = val.controls; if (!(startDate instanceof FormControl && endDate instanceof FormControl)) { - this.logger.error(`[${componentName}]: "formGroup" requires a FormControl for startDate and endDate`); + this.logger.error(`[${componentName}]: "formGroup" requires a FormControl for startDate and another one for endDate`); return; } this._formGroup = val; - // overwrite internal formControls + // overwrite internal formControls and setup again the validators, subscriptions, etc. this._startDate = startDate; this._endDate = endDate; + this._setupFormControls(); } /** @@ -231,12 +193,6 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent */ private _formGroup?: FormGroup; - /** - * Output that will emit a specific date whenever the selection has changed - */ - @Output() - public readonly dateRangeChanged = new EventEmitter(); - /** * Filter function or a string * Will be applied to both date-picker @@ -254,7 +210,7 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent public dateMask?: StarkDatePickerMaskConfig; /** - * Whether the datepickers are disabled + * Whether the date pickers are disabled */ @Input() public set disabled(val: boolean) { @@ -270,17 +226,26 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent `); } + // enable/disable the controls without emitting a change event since the values did not change (to avoid unnecessary extra calls!) if (val) { - this.startDateFormControl.disable(); - this.endDateFormControl.disable(); + if (this.startDateFormControl) { + this.startDateFormControl.disable({ emitEvent: false }); + } + if (this.endDateFormControl) { + this.endDateFormControl.disable({ emitEvent: false }); + } } else { - this.startDateFormControl.enable(); - this.endDateFormControl.enable(); + if (this.startDateFormControl) { + this.startDateFormControl.enable({ emitEvent: false }); + } + if (this.endDateFormControl) { + this.endDateFormControl.enable({ emitEvent: false }); + } } } /** - * Whether the datepickers are required + * Whether the date pickers are required */ @Input() public required = false; @@ -297,35 +262,123 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent @Input() public rangePickerName = ""; + /** + * Output that will emit a specific date range whenever the selection has changed + */ + @Output() + public readonly dateRangeChanged = new EventEmitter(); + + /** + * Reference to the start datepicker embedded in this component + */ + @ViewChild("startPicker") + public startPicker!: StarkDatePickerComponent; + + /** + * Reference to the end datepicker embedded in this component + */ + @ViewChild("endPicker") + public endPicker!: StarkDatePickerComponent; + + /** + * @ignore + * @internal + * The registered callback function called when a blur event occurs on the input element. + */ + private _onTouched: () => void = () => { + /*noop*/ + }; + + /** + * @ignore + * @internal + * The registered callback function called when an input event occurs on the input element. + */ + private _onChange: (_dateRange: StarkDateRangePickerEvent) => void = (_: any) => { + /*noop*/ + }; + + /** + * Subscriptions to be removed at end of component lifecycle + * @ignore + */ + private subs: Subscription[] = []; + + /** + * @ignore + * @internal + */ + public currentRange?: StarkDateRangePickerEvent; + + /** + * @ignore + * @internal + */ + private _startBeforeEndValidator: ValidatorFn = ({ value }) => { + return value instanceof Date && this.endDate instanceof Date && value.getTime() > this.endDate.getTime() + ? { startBeforeEnd: true } + : null; + }; + + /** + * @ignore + * @internal + */ + private _endAfterStartValidator: ValidatorFn = ({ value }) => { + return value instanceof Date && this.startDate instanceof Date && value.getTime() < this.startDate.getTime() + ? { endAfterStart: true } + : null; + }; + + /** + * @ignore + * @internal + * Validator that will perform the 'required' validation only if the 'required' input is enabled + * IMPORTANT: this should be always added to the internal form controls for the start and end date pickers + */ + private _requiredValidator: ValidatorFn = (control: AbstractControl) => { + if (this.required) { + return Validators.required(control); + } else { + return null; + } + }; + /** * Class constructor * @param logger - The logger of the application * @param injector - The Injector of the application * @param renderer - Angular Renderer wrapper for DOM manipulations. * @param elementRef - Reference to the DOM element where this directive is applied to. + * @param cdRef - Reference to the change detector attached to this component */ public constructor( @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, private injector: Injector, protected renderer: Renderer2, - protected elementRef: ElementRef + protected elementRef: ElementRef, + protected cdRef: ChangeDetectorRef ) { super(renderer, elementRef); + // IMPORTANT: the form controls should be initialized here because they should be available before the developer passes his own form controls + this._setupFormControls(); } /** * Angular lifecycle method */ public ngOnInit(): void { + this._setupNgControl(); this.logger.debug(componentName + ": component initialized"); - this._setupFormControls(); } /** * Angular lifecycle method */ - public ngAfterViewInit(): void { - this._setupNgControl(); + public ngOnDestroy(): void { + for (const subscription of this.subs) { + subscription.unsubscribe(); + } } /** @@ -333,21 +386,37 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent * @internal */ private _setupFormControls(): void { - // Merge the original validators with ones from the start/end date form controls - this.startDateFormControl.setValidators(Validators.compose([this.startDateFormControl.validator, this._startBeforeEndValidator])); - this.endDateFormControl.setValidators(Validators.compose([this.endDateFormControl.validator, this._endAfterStartValidator])); + // merge the original validators with ones from the start/end date form controls if already available + // or create the form controls with such validators otherwise + // IMPORTANT: the '_requiredValidator' should ALWAYS be added. Internally it checks whether the control is actually required or not. + // By doing this, the formControl will be marked as 'required' since the beginning (if it is marked as required). + // This prevents the 'ExpressionChangedAfterItHasBeenCheckedError' in the template when the developer uses an NgModel and retrieves the internal errors + // However, the error will NOT prevent the error from happening when FormControl is used (via the 'rangeFormGroup' input). + if (this.startDateFormControl) { + this.startDateFormControl.setValidators( + Validators.compose([this.startDateFormControl.validator, this._requiredValidator, this._startBeforeEndValidator]) + ); + } else { + this._startDate = new FormControl(undefined, [this._requiredValidator, this._startBeforeEndValidator]); + } + if (this.endDateFormControl) { + this.endDateFormControl.setValidators( + Validators.compose([this.endDateFormControl.validator, this._requiredValidator, this._endAfterStartValidator]) + ); + } else { + this._endDate = new FormControl(undefined, [this._requiredValidator, this._endAfterStartValidator]); + } for (const subscription of this.subs) { subscription.unsubscribe(); } + this.subs.push( - this.startDateFormControl.valueChanges.subscribe(() => { - this.endDateFormControl.updateValueAndValidity({ emitEvent: false, onlySelf: true }); - this.onDateChanged(); + this.startDateFormControl.valueChanges.pipe(distinctUntilChanged()).subscribe((_value: Date) => { + this.onDateChanged("start"); }), - this.endDateFormControl.valueChanges.subscribe(() => { - this.startDateFormControl.updateValueAndValidity({ emitEvent: false, onlySelf: true }); - this.onDateChanged(); + this.endDateFormControl.valueChanges.pipe(distinctUntilChanged()).subscribe((_value: Date) => { + this.onDateChanged("end"); }) ); } @@ -373,52 +442,72 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent } } - /** - * Angular lifecycle method - */ - public ngOnDestroy(): void { - for (const subscription of this.subs) { - subscription.unsubscribe(); - } - } - /** * Handle the date changed on the start and end datepicker + * @param dateOrigin - Whether the change was triggered by the start or end date picker */ - public onDateChanged(): void { + public onDateChanged(dateOrigin: "start" | "end"): void { this._onTouched(); - const dateRange: StarkDateRangePickerEvent = { startDate: this.startDate, endDate: this.endDate }; + if (this.startDate && this.endDate && this.endDate.getTime() < this.startDate.getTime()) { + // clear the value of one of the date pickers and make the change to affect ONLY that control + // this is because at the end both controls will be validated once the final value is emitted (see the 'else' block below) + if (dateOrigin === "start") { + this.endDateFormControl.setValue(undefined, { onlySelf: true }); + } else { + this.startDateFormControl.setValue(undefined, { onlySelf: true }); + } + this.cdRef.detectChanges(); // to force a refresh of the validation errors + } else { + const dateRange: StarkDateRangePickerEvent = { startDate: this.startDate, endDate: this.endDate }; - this._onChange(dateRange); - this.dateRangeChanged.emit(dateRange); - } + if (!isEqual(dateRange, this.currentRange)) { + // calling 'updateValueAndValidity()' manually on both form controls without emitting the valueChanges event (to avoid unnecessary extra calls in the end user's code!) + this.startDateFormControl.updateValueAndValidity({ emitEvent: false }); + this.endDateFormControl.updateValueAndValidity({ emitEvent: false }); - /*--- Control Value Accessor methods---*/ + this.currentRange = dateRange; + this._onChange(dateRange); + this.dateRangeChanged.emit(dateRange); + } + } + } /** + * Part of {@link ControlValueAccessor} API + * Registers a function to be called when the control value changes. * @ignore + * @internal */ public registerOnChange(fn: (_dateRange: StarkDateRangePickerEvent) => void): void { this._onChange = fn; } /** + * Part of {@link ControlValueAccessor} API + * Registers a function to be called when the control is touched. * @ignore + * @internal */ public registerOnTouched(fn: () => void): void { this._onTouched = fn; } /** + * Part of {@link ControlValueAccessor} API + * Sets the "disabled" property on the input element. * @ignore + * @internal */ public setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } /** + * Part of {@link ControlValueAccessor} API + * Sets the "value" property on the input element. * @ignore + * @internal */ public writeValue(dateRange: StarkDateRangePickerEvent): void { dateRange = dateRange || {}; diff --git a/showcase/src/app/demo-ui/pages/date-range-picker/demo-date-range-picker-page.component.html b/showcase/src/app/demo-ui/pages/date-range-picker/demo-date-range-picker-page.component.html index 1b4b1b7b35..5374bab966 100644 --- a/showcase/src/app/demo-ui/pages/date-range-picker/demo-date-range-picker-page.component.html +++ b/showcase/src/app/demo-ui/pages/date-range-picker/demo-date-range-picker-page.component.html @@ -19,19 +19,25 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

[required]="true" #dateRangeComponent > - + {{ error | translate }}
- + {{ error | translate }}


- {{ (modelDisabled ? "SHOWCASE.COMMON.DISABLED" : "SHOWCASE.COMMON.ENABLED") | translate }} + {{ "SHOWCASE.COMMON.DISABLED" | translate }} @@ -41,19 +47,22 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

exampleTitle="SHOWCASE.DEMO.DATE_RANGE_PICKER.WITH_FORM_GROUP" > - + {{ error | translate }}
- + {{ error | translate }}


- {{ (dateRangeFormGroup.disabled ? "SHOWCASE.COMMON.DISABLED" : "SHOWCASE.COMMON.ENABLED") | translate }} + {{ "SHOWCASE.COMMON.DISABLED" | translate }} diff --git a/showcase/src/app/demo-ui/pages/date-range-picker/demo-date-range-picker-page.component.ts b/showcase/src/app/demo-ui/pages/date-range-picker/demo-date-range-picker-page.component.ts index 952df3c7ef..b0d61c136b 100644 --- a/showcase/src/app/demo-ui/pages/date-range-picker/demo-date-range-picker-page.component.ts +++ b/showcase/src/app/demo-ui/pages/date-range-picker/demo-date-range-picker-page.component.ts @@ -1,12 +1,11 @@ -/* tslint:disable:no-null-keyword trackBy-function */ +/* tslint:disable:no-null-keyword */ import { Component, Inject, OnDestroy } from "@angular/core"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { StarkDateRangePickerEvent } from "@nationalbankbelgium/stark-ui"; import { AbstractControl, FormControl, FormGroup, ValidationErrors, Validators } from "@angular/forms"; import { MatCheckboxChange } from "@angular/material/checkbox"; import { Subscription } from "rxjs"; import { ReferenceLink } from "../../../shared/components"; -import { StarkDateRangePickerEvent } from "@nationalbankbelgium/stark-ui"; -import map from "lodash-es/map"; const MONTH_IN_MILLI = 2592000000; @@ -26,39 +25,15 @@ export class DemoDateRangePickerPageComponent implements OnDestroy { public dateRangeModel = { startDate: this.today, endDate: this.inOneMonth }; public modelDisabled = false; + // IMPORTANT: if the DateRangePicker should be required, then add the 'required' validator to both form controls too! public dateRangeFormGroup = new FormGroup({ - startDate: new FormControl(null, Validators.compose([DemoDateRangePickerPageComponent.noFebruaryValidator])), - endDate: new FormControl(null, Validators.compose([DemoDateRangePickerPageComponent.noFebruaryValidator])) + startDate: new FormControl( + undefined, + Validators.compose([Validators.required, DemoDateRangePickerPageComponent.noFebruaryValidator]) + ), + endDate: new FormControl(undefined, Validators.compose([Validators.required, DemoDateRangePickerPageComponent.noFebruaryValidator])) }); - public getErrorMessages: (control: AbstractControl) => string[] = () => []; - - private _activateGetErrorMessages(): void { - this.getErrorMessages = (control: AbstractControl): string[] => - map( - control.errors || {}, - (_value: any, key: string): string => { - switch (key) { - case "required": - return "SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.REQUIRED"; - case "matDatepickerMin": - return "SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.MIN_TODAY"; - case "matDatepickerMax": - return "SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.MAX_MONTH"; - case "matDatepickerFilter": - return "SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.WEEKDAY"; - case "startBeforeEnd": - return "SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.START_BEFORE_END"; - case "endAfterStart": - return "SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.END_AFTER_START"; - case "inFebruary": - return "SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.IN_FEBRUARY"; - default: - return ""; - } - } - ); - } /** * List of subscriptions to be unsubscribed when component is destroyed */ @@ -72,11 +47,49 @@ export class DemoDateRangePickerPageComponent implements OnDestroy { ]; public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) { - this._subs.push(this.dateRangeFormGroup.valueChanges.subscribe((v: any) => this.logger.debug("formGroup:", v))); + this._subs.push(this.dateRangeFormGroup.valueChanges.subscribe((value: any) => this.logger.debug("formGroup:", value))); + } - // FIXME: For some reason validation is run on the internal formControls before the value is set. - // this results in a ExpressionChangedAfterItHasBeenCheckedError on the usage of getErrorMessages. - setTimeout(() => this._activateGetErrorMessages()); + public ngOnDestroy(): void { + for (const subscription of this._subs) { + subscription.unsubscribe(); + } + } + + public getErrorMessages(control?: AbstractControl): string[] { + const errors: string[] = []; + + if (control && control.errors) { + for (const key of Object.keys(control.errors)) { + switch (key) { + case "required": + errors.push("SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.REQUIRED"); + break; + case "matDatepickerMin": + errors.push("SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.MIN_TODAY"); + break; + case "matDatepickerMax": + errors.push("SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.MAX_MONTH"); + break; + case "matDatepickerFilter": + errors.push("SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.WEEKDAY"); + break; + case "startBeforeEnd": + errors.push("SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.START_BEFORE_END"); + break; + case "endAfterStart": + errors.push("SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.END_AFTER_START"); + break; + case "inFebruary": + errors.push("SHOWCASE.DEMO.DATE_RANGE_PICKER.ERROR_MESSAGES.IN_FEBRUARY"); + break; + default: + errors.push(key); + } + } + } + + return errors; } public onDateModelChange(): void { @@ -84,10 +97,11 @@ export class DemoDateRangePickerPageComponent implements OnDestroy { } public onDateRangeFormGroupDisableCheckboxChange(event: MatCheckboxChange): void { + // enable/disable the control without emitting a change event since the value did not change (to avoid unnecessary extra calls!) if (event.checked) { - this.dateRangeFormGroup.disable(); + this.dateRangeFormGroup.disable({ emitEvent: false }); } else { - this.dateRangeFormGroup.enable(); + this.dateRangeFormGroup.enable({ emitEvent: false }); } } @@ -95,9 +109,7 @@ export class DemoDateRangePickerPageComponent implements OnDestroy { this.logger.debug("onChange:", dateRange); } - public ngOnDestroy(): void { - for (const subscription of this._subs) { - subscription.unsubscribe() - } + public trackItemFn(item: string): string { + return item; } } diff --git a/showcase/src/assets/examples/date-range-picker/form-group.html b/showcase/src/assets/examples/date-range-picker/form-group.html index a176567650..e31b25e058 100644 --- a/showcase/src/assets/examples/date-range-picker/form-group.html +++ b/showcase/src/assets/examples/date-range-picker/form-group.html @@ -1,15 +1,13 @@ - + {{ error }}
- + {{ error }}


- - {{ dateRangeFormGroup.disabled ? "Disabled" : "Enabled" }} - +Disabled diff --git a/showcase/src/assets/examples/date-range-picker/form-group.ts b/showcase/src/assets/examples/date-range-picker/form-group.ts index c174adf6d3..f48e218982 100644 --- a/showcase/src/assets/examples/date-range-picker/form-group.ts +++ b/showcase/src/assets/examples/date-range-picker/form-group.ts @@ -4,7 +4,6 @@ import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium import { AbstractControl, FormControl, FormGroup, ValidationErrors, Validators } from "@angular/forms"; import { MatCheckboxChange } from "@angular/material/checkbox"; import { Subscription } from "rxjs"; -import map from "lodash-es/map"; @Component({ selector: "demo-date-range-picker", @@ -16,9 +15,10 @@ export class DemoDateRangePickerComponent implements OnDestroy { return value instanceof Date && value.getMonth() === 1 ? { inFebruary: true } : null; // date counts months from 0 } + // IMPORTANT: if the DateRangePicker should be required, then add the 'required' validator to both form controls too! public dateRangeFormGroup = new FormGroup({ - startDate: new FormControl(null, Validators.compose([DemoDateRangePickerComponent.noFebruaryValidator])), - endDate: new FormControl(null, Validators.compose([DemoDateRangePickerComponent.noFebruaryValidator])) + startDate: new FormControl(undefined, Validators.compose([Validators.required, DemoDateRangePickerComponent.noFebruaryValidator])), + endDate: new FormControl(undefined, Validators.compose([Validators.required, DemoDateRangePickerComponent.noFebruaryValidator])) }); /** @@ -26,41 +26,63 @@ export class DemoDateRangePickerComponent implements OnDestroy { */ private _subs: Subscription[] = []; - public getErrorMessages(control: AbstractControl): string[] { - return map( - control.errors || [], - (_value: any, key: string): string => { + public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) { + this._subs.push(this.dateRangeFormGroup.valueChanges.subscribe((v: any) => this.logger.debug("formGroup:", v))); + } + + public ngOnDestroy(): void { + for (const subscription of this._subs) { + subscription.unsubscribe(); + } + } + + public getErrorMessages(control?: AbstractControl): string[] { + const errors: string[] = []; + + if (control && control.errors) { + for (const key of Object.keys(control.errors)) { switch (key) { case "required": - return "Date is required"; + errors.push("Date is required"); + break; + case "matDatepickerMin": + errors.push("Date should be after today"); + break; + case "matDatepickerMax": + errors.push("Date should be in less than 1 month"); + break; + case "matDatepickerFilter": + errors.push("Date should be a weekday"); + break; case "startBeforeEnd": - return "Start date should be before end date"; + errors.push("Start date should be before end date"); + break; case "endAfterStart": - return "End date should be after start date"; + errors.push("End date should be after start date"); + break; case "inFebruary": - return "Date should not be in February"; + errors.push("Date should not be in February"); + break; default: - return ""; + errors.push(key); + break; } } - ); - } + } - public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) { - this._subs.push(this.dateRangeFormGroup.valueChanges.subscribe((v: any) => this.logger.debug("formGroup:", v))); + return errors; } public onDateRangeFormGroupDisableCheckboxChange(event: MatCheckboxChange): void { + // enable/disable the control without emitting a change event since the value did not change (to avoid unnecessary extra calls!) if (event.checked) { - this.dateRangeFormGroup.disable(); + this.dateRangeFormGroup.disable({ emitEvent: false }); } else { - this.dateRangeFormGroup.enable(); + this.dateRangeFormGroup.enable({ emitEvent: false }); } } - public ngOnDestroy(): void { - for (const subscription of this._subs) { - subscription.unsubscribe() - } + public trackItemFn(item: string): string { + return item; } } diff --git a/showcase/src/assets/examples/date-range-picker/model.html b/showcase/src/assets/examples/date-range-picker/model.html index fb42c720bc..a106266720 100644 --- a/showcase/src/assets/examples/date-range-picker/model.html +++ b/showcase/src/assets/examples/date-range-picker/model.html @@ -9,15 +9,15 @@ [disabled]="modelDisabled" #dateRangeComponent > - + {{ error }}
- + {{ error }}


-{{ modelDisabled ? "Disabled" : "Enabled" }} +Disabled diff --git a/showcase/src/assets/examples/date-range-picker/model.ts b/showcase/src/assets/examples/date-range-picker/model.ts index cf38f1804d..5e366f017c 100644 --- a/showcase/src/assets/examples/date-range-picker/model.ts +++ b/showcase/src/assets/examples/date-range-picker/model.ts @@ -2,7 +2,6 @@ import { Component, Inject, OnDestroy } from "@angular/core"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; import { AbstractControl } from "@angular/forms"; -import map from "lodash-es/map"; const MONTH_IN_MILLI = 2592000000; @@ -17,40 +16,47 @@ export class DemoDateRangePickerComponent implements OnDestroy { public dateRangeModel = { startDate: this.today, endDate: this.inOneMonth }; public modelDisabled = false; - public getErrorMessages: (control: AbstractControl) => string[] = () => []; - - private _activateGetErrorMessages(): void { - this.getErrorMessages = (control: AbstractControl): string[] => - map( - control.errors || {}, - (_value: any, key: string): string => { - switch (key) { - case "required": - return "Date is required"; - case "matDatepickerMin": - return "Date should be after today"; - case "matDatepickerMax": - return "Date should be in less than 1 month"; - case "matDatepickerFilter": - return "Date should be a weekday"; - case "startBeforeEnd": - return "Start date should be before end date"; - case "endAfterStart": - return "End date should be after start date"; - default: - return ""; - } + public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) {} + + public getErrorMessages(control?: AbstractControl): string[] { + const errors: string[] = []; + + if (control && control.errors) { + for (const key of Object.keys(control.errors)) { + switch (key) { + case "required": + errors.push("Date is required"); + break; + case "matDatepickerMin": + errors.push("Date should be after today"); + break; + case "matDatepickerMax": + errors.push("Date should be in less than 1 month"); + break; + case "matDatepickerFilter": + errors.push("Date should be a weekday"); + break; + case "startBeforeEnd": + errors.push("Start date should be before end date"); + break; + case "endAfterStart": + errors.push("End date should be after start date"); + break; + default: + errors.push(key); + break; } - ); - } + } + } - public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) { - // FIXME: For some reason validation is run on the internal formControls before the value is set. - // this results in a ExpressionChangedAfterItHasBeenCheckedError on the usage of getErrorMessages. - setTimeout(() => this._activateGetErrorMessages()); + return errors; } public onDateModelChange(): void { this.logger.debug("ngModel", this.dateRangeModel); } + + public trackItemFn(item: string): string { + return item; + } }