diff --git a/packages/stark-ui/src/modules/date-range-picker/components/_date-range-picker.component.scss b/packages/stark-ui/src/modules/date-range-picker/components/_date-range-picker.component.scss index 5b01e5a749..09801c64ee 100644 --- a/packages/stark-ui/src/modules/date-range-picker/components/_date-range-picker.component.scss +++ b/packages/stark-ui/src/modules/date-range-picker/components/_date-range-picker.component.scss @@ -3,7 +3,7 @@ /* ============================================================================== */ /* stark-ui: src/modules/date-range-picker/components/date-range-picker/_date-range-picker.component.scss */ -.stark-date-range-picker stark-date-picker:first-child { +.stark-date-range-picker mat-form-field { margin-right: 16px; } diff --git a/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.html b/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.html index 6462b964e1..f0b979ab4d 100644 --- a/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.html +++ b/packages/stark-ui/src/modules/date-range-picker/components/date-range-picker.component.html @@ -3,30 +3,37 @@ #startPicker [pickerId]="rangePickerId + '-start'" [pickerName]="rangePickerName + '-start'" - [value]="startDate" - [dateFilter]="dateFilter" - [disabled]="isDisabled" + [formControl]="startDateFormControl" [placeholder]="startDateLabel | translate" + [dateFilter]="dateFilter" + [dateMask]="dateMask" [min]="startMinDate" [max]="startMaxDate" - [dateMask]="dateMask" - (dateChange)="onDateStartChanged($event)" + [required]="required" > + + + + + + + + + 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 7b92d1c9cb..f4d5d671d1 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 @@ -1,7 +1,8 @@ +/* tslint:disable:no-null-keyword completed-docs max-inline-declarations no-big-function */ import { NoopAnimationsModule } from "@angular/platform-browser/animations"; -import { EventEmitter } from "@angular/core"; -import { async, ComponentFixture, TestBed } from "@angular/core/testing"; -import { ReactiveFormsModule } from "@angular/forms"; +import { Component, EventEmitter, ViewChild } from "@angular/core"; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, ValidationErrors } from "@angular/forms"; import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from "@angular/material/core"; import { MAT_MOMENT_DATE_FORMATS, MomentDateAdapter } from "@angular/material-moment-adapter"; import { MatDatepickerModule } from "@angular/material/datepicker"; @@ -16,160 +17,324 @@ import { StarkTimestampMaskDirective } from "../../input-mask-directives"; import moment from "moment"; describe("DateRangePickerComponent", () => { - let fixture: ComponentFixture; - let component: StarkDateRangePickerComponent; - - beforeEach(async(() => { - return TestBed.configureTestingModule({ - declarations: [StarkTimestampMaskDirective, StarkDatePickerComponent, StarkDateRangePickerComponent], - imports: [NoopAnimationsModule, MatDatepickerModule, MatFormFieldModule, TranslateModule.forRoot(), ReactiveFormsModule], - 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; - fixture.detectChanges(); - }); + describe("uncontrolled", () => { + let fixture: ComponentFixture; + let component: StarkDateRangePickerComponent; - describe("on initialization", () => { - it("should set internal component properties", () => { - expect(fixture).toBeDefined(); - expect(component).toBeDefined(); + 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(); + })); - expect(component.logger).not.toBeNull(); - expect(component.logger).toBeDefined(); + beforeEach(() => { + fixture = TestBed.createComponent(StarkDateRangePickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it("should NOT have any inputs set", () => { - expect(component.dateFilter).toBeUndefined(); - expect(component.dateMask).toBeUndefined(); - expect(component.endDate).toBeUndefined(); - expect(component.endDateLabel).toBeDefined(); - expect(component.endDateLabel).toEqual("STARK.DATE_RANGE_PICKER.TO"); - expect(component.endMaxDate).toBeUndefined(); - expect(component.endMinDate).toBeUndefined(); - expect(component.isDisabled).toBeUndefined(); - expect(component.rangePickerId).toBeDefined(); - expect(component.rangePickerId).toEqual(""); - expect(component.rangePickerName).toBeDefined(); - expect(component.rangePickerName).toEqual(""); - expect(component.startDate).toBeUndefined(); - expect(component.startDateLabel).toBeDefined(); - expect(component.startDateLabel).toEqual("STARK.DATE_RANGE_PICKER.FROM"); - expect(component.startMaxDate).toBeUndefined(); - expect(component.startMinDate).toBeUndefined(); - expect(component.dateRangeChanged).toBeDefined(); - expect(component.dateRangeChanged).toEqual(new EventEmitter()); - }); - }); + describe("on initialization", () => { + it("should set internal component properties", () => { + expect(fixture).toBeDefined(); + expect(component).toBeDefined(); - 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(); - }); + expect(component.logger).not.toBeNull(); + expect(component.logger).toBeDefined(); + }); - 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(); + it("should NOT have any inputs set", () => { + expect(component.dateFilter).toBeUndefined(); + expect(component.dateMask).toBeUndefined(); + expect(component.endDate).toBeUndefined(); + expect(component.endDateLabel).toBeDefined(); + expect(component.endDateLabel).toEqual("STARK.DATE_RANGE_PICKER.TO"); + expect(component.endMaxDate).toBeUndefined(); + expect(component.endMinDate).toBeUndefined(); + expect(component.rangePickerId).toBeDefined(); + expect(component.rangePickerId).toEqual(""); + expect(component.rangePickerName).toBeDefined(); + expect(component.rangePickerName).toEqual(""); + expect(component.startDate).toBeUndefined(); + expect(component.startDateLabel).toBeDefined(); + expect(component.startDateLabel).toEqual("STARK.DATE_RANGE_PICKER.FROM"); + expect(component.startMaxDate).toBeUndefined(); + expect(component.startMinDate).toBeUndefined(); + expect(component.dateRangeChanged).toBeDefined(); + expect(component.dateRangeChanged).toEqual(new EventEmitter()); + }); }); - it("the datepickers should be disabled when isDisabled is true", () => { - component.isDisabled = true; - fixture.detectChanges(); - expect(component.startPicker.pickerInput.disabled).toBe(true); - expect(component.endPicker.pickerInput.disabled).toBe(true); - }); + 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(); + }); - it("the placeholders of the datepickers should be set correctly", () => { - 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 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(); + }); + + 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("the placeholders of the datepickers should be set correctly", () => { + 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; + fixture.detectChanges(); + 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); + }); + + it("the datepickers value should be set correctly", fakeAsync(() => { + 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).toEqual(date); + expect(component.endPicker.value).not.toBeNull("The value of the endDate date-picker should be set."); + expect(component.endPicker.value).toEqual(date); + })); }); - it("the datepickers min date should be set correctly", () => { - const minDate = new Date(2018, 6, 1); - component.startMinDate = minDate; - component.endMinDate = minDate; - fixture.detectChanges(); - 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); + 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(); + + expect(component.endDateFormControl.status).toBe("INVALID"); + }); + + it("the end date should be correctly set if after the start date", () => { + const endDate = new Date(2018, 6, 7); + component.startDate = new Date(2018, 6, 6); + component.endDate = endDate; + fixture.detectChanges(); + + expect(component.endDate).toEqual(endDate); + }); + + it("the end date should be correctly set if after the start date is undefined", () => { + const endDate = new Date(2018, 6, 8); + component.startDate = undefined; + component.endDate = endDate; + fixture.detectChanges(); + + expect(component.endDate).toEqual(endDate); + }); }); + }); - 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); + @Component({ + selector: "test-model", + template: ` + + ` + }) + class TestModelComponent { + @ViewChild(StarkDateRangePickerComponent) + public dateRangePicker!: StarkDateRangePickerComponent; + + public dateRange = {}; + } + + describe("with ngModel", () => { + let fixture: 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())); + + beforeEach(() => { + fixture = TestBed.createComponent(TestModelComponent); + hostComponent = fixture.componentInstance; + component = hostComponent.dateRangePicker; }); - it("the datepickers value should be set correctly", () => { - const date = new Date(2018, 6, 3); - component.startDate = date; - component.endDate = date; + it("should update when model is updated", fakeAsync(() => { + const expected = { startDate: new Date(2019, 0, 1), endDate: new Date(2019, 0, 2) }; + + hostComponent.dateRange = expected; fixture.detectChanges(); - expect(component.startPicker.pickerInput.value).not.toBeNull(); - expect((component.startPicker.pickerInput.value).toDate()).toEqual(date); - expect(component.endPicker.pickerInput.value).not.toBeNull(); - expect((component.endPicker.pickerInput.value).toDate()).toEqual(date); - }); + + tick(); + + expect(component.startDate).toBeDefined(); + expect(component.endDate).toBeDefined(); + + expect(component.startDate).toEqual(expected.startDate); + expect(component.endDate).toEqual(expected.endDate); + })); }); - describe("dates selection", () => { - it("the end date should be undefined before the start date", () => { - component.startDate = new Date(2018, 6, 5); - component.endDate = new Date(2018, 6, 4); - fixture.detectChanges(); - component.checkDates(); - expect(component.endDate).toBeUndefined(); + @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 hostComponent: TestFormGroupComponent; + let component: StarkDateRangePickerComponent; - it("the end date should be correctly set if after the start date", () => { - const endDate = new Date(2018, 6, 7); - component.startDate = new Date(2018, 6, 6); - component.endDate = endDate; + 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; + component = hostComponent.dateRangePicker; + }); + + it("should update when form group is updated", () => { + const expected = { startDate: new Date(2019, 0, 1), endDate: new Date(2019, 0, 2) }; + + hostComponent.formGroup.setValue(expected); fixture.detectChanges(); - component.checkDates(); - expect(component.endDate).toEqual(endDate); + + expect(component.startDate).toBeDefined(); + expect(component.endDate).toBeDefined(); + + expect(component.startDate).toEqual(expected.startDate); + expect(component.endDate).toEqual(expected.endDate); }); - it("the end date should be correctly set if after the start date is undefined", () => { - const endDate = new Date(2018, 6, 8); - component.startDate = undefined; - component.endDate = endDate; + it("should show errors at the correct input", () => { + const { startDate: startDateFC, endDate: endDateFC } = hostComponent.formGroup.controls; + const alwaysFail = (): ValidationErrors => ({ alwaysFail: "error" }); + + startDateFC.setValidators(alwaysFail); + startDateFC.setValue(new Date()); + startDateFC.markAsTouched(); + startDateFC.markAsDirty(); + + endDateFC.setValidators(alwaysFail); + endDateFC.setValue(new Date()); + endDateFC.markAsTouched(); + endDateFC.markAsDirty(); + fixture.detectChanges(); - component.checkDates(); - 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"); + + const endDateError = fixture.nativeElement.querySelectorAll("mat-form-field mat-error").item(1); + expect(endDateError).not.toBeNull(); + expect(endDateError.textContent).toEqual("END-ERROR"); }); }); }); 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 5a191d7572..3062410773 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,12 +1,27 @@ -import { Component, ElementRef, EventEmitter, Inject, Input, OnInit, Output, Renderer2, ViewChild, ViewEncapsulation } from "@angular/core"; -import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; -import moment from "moment"; +/* tslint:disable:no-null-keyword */ import { - StarkDatePickerComponent, - StarkDatePickerFilter, - StarkDatePickerMaskConfig -} from "../../date-picker/components/date-picker.component"; + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Inject, + Injector, + Input, + OnDestroy, + OnInit, + Output, + Renderer2, + Type, + ViewChild, + ViewEncapsulation +} from "@angular/core"; +import { ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR, NgControl, ValidatorFn, Validators } from "@angular/forms"; +import { Subscription } from "rxjs"; +import noop from "lodash-es/noop"; +import get from "lodash-es/get"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; import { AbstractStarkUiComponent } from "../../../common/classes/abstract-component"; +import { StarkDatePickerComponent, StarkDatePickerFilter, StarkDatePickerMaskConfig } from "../../date-picker"; import { StarkDateRangePickerEvent } from "./date-range-picker-event.intf"; /** @@ -24,91 +39,197 @@ const componentName = "stark-date-range-picker"; // We need to use host instead of @HostBinding: https://github.com/NationalBankBelgium/stark/issues/664 host: { class: componentName - } + }, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: StarkDateRangePickerComponent + } + ] }) -export class StarkDateRangePickerComponent extends AbstractStarkUiComponent implements OnInit { +export class StarkDateRangePickerComponent extends AbstractStarkUiComponent + implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { /** - * Filter function or a string - * Will be applied to both date-picker + * ControlValueAccessor listener + * @ignore */ - @Input() - public dateFilter?: StarkDatePickerFilter; + private _onTouched: () => void = noop; /** - * Timestamp Mask Configuration to apply on the start/end date-picker. - * If `true` is passed, the default mask config is applied: {DEFAULT_DATE_MASK_CONFIG|DEFAULT_DATE_MASK_CONFIG} - * If `false` is passed or if `dateMask` is not present, the directive is disabled. - * If a `StarkTimestampMaskConfig` is passed, it is set as the date mask config. + * ControlValueAccessor listener + * @ignore */ - @Input() - public dateMask?: StarkDatePickerMaskConfig; + private _onChange: (_dateRange: StarkDateRangePickerEvent) => void = noop; /** - * Source Date to be bound to the end datepicker model + * 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; + }; + + /** + * Source Date to be bound to the start datepicker model */ @Input() - public endDate?: Date; + public get startDate(): Date | undefined { + return this._startDate.value || undefined; + } + + public set startDate(value: Date | undefined) { + this._startDate.setValue(value); + } + + /** + * The internal formControl used to manage the state of the starkDatePicker for the start date. + * This can be used to retrieve internal errors. + * + * @example + * + * + * + * {{starkDateRangePicker.startDateFormControl.hasError('required') ? "Start date is required" : null }} + * + * + */ + public get startDateFormControl(): FormControl { + return this._startDate; + } + + /** + * @ignore + */ + private _startDate = new FormControl(); /** * Label to be displayed in the end datepicker */ @Input() - public endDateLabel = "STARK.DATE_RANGE_PICKER.TO"; + public startDateLabel = "STARK.DATE_RANGE_PICKER.FROM"; /** - * Maximum date of the end date picker + * Minimum date of the start date picker */ @Input() - public endMaxDate?: Date; + public startMinDate?: Date; /** - * Minimum date of the end date picker + * Maximum date of the start date picker */ @Input() - public endMinDate?: Date; + public startMaxDate?: Date; + + /*--- END-DATE CONFIGURATIONS ---*/ /** - * Whether the datepickers are disabled + * @ignore + * @internal */ - @Input() - public isDisabled?: boolean; + private _endAfterStartValidator: ValidatorFn = ({ value }) => { + return value instanceof Date && this.startDate instanceof Date && value.getTime() < this.startDate.getTime() + ? { endAfterStart: true } + : null; + }; /** - * HTML "name" attribute of the element. + * Source Date to be bound to the end datepicker model */ @Input() - public rangePickerId = ""; + public get endDate(): Date | undefined { + return this._endDate.value || undefined; + } + + public set endDate(value: Date | undefined) { + this._endDate.setValue(value || null); + } /** - * HTML "name" attribute of the element. + * The internal formControl used to manage the state of the starkDatePicker for the start date. + * This can be used to retrieve internal errors. + * + * @example + * + * + * + * {{starkDateRangePicker.endDateFormControl.hasError('required') ? "End date is required" : null }} + * + * */ - @Input() - public rangePickerName = ""; + public get endDateFormControl(): FormControl { + return this._endDate; + } /** - * Source Date to be bound to the start datepicker model + * @ignore */ + private _endDate = new FormControl(); + /** + * Label to be displayed in the end datepicker + */ @Input() - public startDate?: Date; + public endDateLabel = "STARK.DATE_RANGE_PICKER.TO"; /** - * Label to be displayed in the start datepicker + * Minimum date of the end date picker */ @Input() - public startDateLabel = "STARK.DATE_RANGE_PICKER.FROM"; + public endMinDate?: Date; /** - * Maximum date of the start date picker + * Maximum date of the end date picker */ @Input() - public startMaxDate?: Date; + public endMaxDate?: Date; + + /*--- SHARED CONFIGURATIONS ---*/ /** - * Minimum date of the start date picker + * Input to manage both start date and end date. */ @Input() - public startMinDate?: Date; + 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`); + return; + } + + this._formGroup = val; + // overwrite internal formControls + this._startDate = startDate; + this._endDate = endDate; + } + + /** + * @ignore + */ + private _formGroup?: FormGroup; /** * Output that will emit a specific date whenever the selection has changed @@ -117,25 +238,75 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent impl public readonly dateRangeChanged = new EventEmitter(); /** - * Reference to the start datepicker embedded in this component + * Filter function or a string + * Will be applied to both date-picker */ - @ViewChild("startPicker") - public startPicker!: StarkDatePickerComponent; + @Input() + public dateFilter?: StarkDatePickerFilter; /** - * Reference to the end datepicker embedded in this component + * Timestamp Mask Configuration to apply on the start/end date-picker. + * If `true` is passed, the default mask config is applied: {DEFAULT_DATE_MASK_CONFIG|DEFAULT_DATE_MASK_CONFIG} + * If `false` is passed or if `dateMask` is not present, the directive is disabled. + * If a `StarkTimestampMaskConfig` is passed, it is set as the date mask config. */ - @ViewChild("endPicker") - public endPicker!: StarkDatePickerComponent; + @Input() + public dateMask?: StarkDatePickerMaskConfig; + + /** + * Whether the datepickers are disabled + */ + @Input() + public set disabled(val: boolean) { + if (this._formGroup) { + this.logger.warn(`[${componentName}]: + It looks like you're using the "disabled" attribute with a "formGroup". We recommend using following approach. + + Example: + dateRangeFormGroup = new FormGroup({ + startDate: new FormControl({value: null, disabled: true}), + endDate: new FormControl({value: null, disabled: true}) + }); + `); + } + + if (val) { + this.startDateFormControl.disable(); + this.endDateFormControl.disable(); + } else { + this.startDateFormControl.enable(); + this.endDateFormControl.enable(); + } + } + + /** + * Whether the datepickers are required + */ + @Input() + public required = false; + + /** + * HTML "name" attribute of the element. + */ + @Input() + public rangePickerId = ""; + + /** + * HTML "name" attribute of the element. + */ + @Input() + public rangePickerName = ""; /** * 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. */ public constructor( @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService, + private injector: Injector, protected renderer: Renderer2, protected elementRef: ElementRef ) { @@ -143,41 +314,115 @@ export class StarkDateRangePickerComponent extends AbstractStarkUiComponent impl } /** - * Component lifecycle hook + * Angular lifecycle method */ public ngOnInit(): void { this.logger.debug(componentName + ": component initialized"); + this._setupFormControls(); + } + + /** + * Angular lifecycle method + */ + public ngAfterViewInit(): void { + this._setupNgControl(); } /** - * Handle the date changed on the start datepicker + * @ignore + * @internal */ - public onDateStartChanged(date: Date): void { - this.startDate = date; - this.checkDates(); + 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])); + + 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.endDateFormControl.valueChanges.subscribe(() => { + this.startDateFormControl.updateValueAndValidity({ emitEvent: false, onlySelf: true }); + this.onDateChanged(); + }) + ); } /** - * Handle the date changed on the end datepicker + * Link the passed control (if one is available) to the internal formControls. + * @ignore + * @internal */ - public onDateEndChanged(date: Date): void { - this.endDate = date; - this.checkDates(); + private _setupNgControl(): void { + // Get the ngControl from the injector + const ngControl = this.injector.get(>NgControl, null); + if (!ngControl) { + return; + } + + ngControl.valueAccessor = this; + + if (typeof get(ngControl, "control.validator") === "function") { + this.logger.warn( + `[${componentName}]: validators set on the control will not be used, use the "formGroup" attribute to manage your own validations.` + ); + } } /** - * Validate the dates and emit the dateRangeChanged event + * Angular lifecycle method */ - public checkDates(): void { - if (moment.isDate(this.startDate) && moment.isDate(this.endDate) && moment(this.endDate).isBefore(this.startDate)) { - this.logger.error("StarkDateRangePicker: Start Date cannot be lower than End Date. End Date will be cleared"); - this.endDate = undefined; - // tslint:disable-next-line:no-null-keyword - this.endPicker.pickerInput.value = null; + public ngOnDestroy(): void { + for (const subscription of this.subs) { + subscription.unsubscribe(); } - this.dateRangeChanged.emit({ - startDate: this.startDate, - endDate: this.endDate - }); + } + + /** + * Handle the date changed on the start and end datepicker + */ + public onDateChanged(): void { + this._onTouched(); + + const dateRange: StarkDateRangePickerEvent = { startDate: this.startDate, endDate: this.endDate }; + + this._onChange(dateRange); + this.dateRangeChanged.emit(dateRange); + } + + /*--- Control Value Accessor methods---*/ + + /** + * @ignore + */ + public registerOnChange(fn: (_dateRange: StarkDateRangePickerEvent) => void): void { + this._onChange = fn; + } + + /** + * @ignore + */ + public registerOnTouched(fn: () => void): void { + this._onTouched = fn; + } + + /** + * @ignore + */ + public setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + /** + * @ignore + */ + public writeValue(dateRange: StarkDateRangePickerEvent): void { + dateRange = dateRange || {}; + this.startDateFormControl.setValue(dateRange.startDate, { emitEvent: false }); + this.endDateFormControl.setValue(dateRange.endDate, { emitEvent: false }); } } 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 c7cce95a21..1b4b1b7b35 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 @@ -1,80 +1,76 @@

SHOWCASE.DEMO.DATE_RANGE_PICKER.TITLE

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

- -
- - -
- isDisabled + + + + + {{ error | translate }} +
+
+ + + {{ error | translate }} +
+
+
+

+ + {{ (modelDisabled ? "SHOWCASE.COMMON.DISABLED" : "SHOWCASE.COMMON.ENABLED") | translate }} + +
+ + + + + {{ error | translate }} +
+
+ + + {{ error | translate }} +
+
+
+

+ + {{ (dateRangeFormGroup.disabled ? "SHOWCASE.COMMON.DISABLED" : "SHOWCASE.COMMON.ENABLED") | translate }} +
-
- - -
- isDisabled +
-
- - -
- isDisabled +
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 6eb975edb2..952df3c7ef 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,30 +1,68 @@ -import { Component, Inject } from "@angular/core"; +/* tslint:disable:no-null-keyword trackBy-function */ +import { Component, Inject, OnDestroy } from "@angular/core"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; -import { StarkDatePickerFilter, StarkDateRangePickerEvent, StarkTimestampMaskConfig } 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 DAY_IN_MILLISECONDS = 86400000; +const MONTH_IN_MILLI = 2592000000; @Component({ selector: "demo-date-range-picker", templateUrl: "./demo-date-range-picker-page.component.html" }) -export class DemoDateRangePickerPageComponent { - public startDate?: Date; - public endDate?: Date; +export class DemoDateRangePickerPageComponent implements OnDestroy { + public static noFebruaryValidator(control: AbstractControl): ValidationErrors | null { + const { value } = control; + return value instanceof Date && value.getMonth() === 1 ? { inFebruary: true } : null; // date counts months from 0 + } + + public today = new Date(); + public inOneMonth = new Date(this.today.getTime() + MONTH_IN_MILLI); - public minDate = new Date(); - public maxDate = new Date(Date.now() + 30 * DAY_IN_MILLISECONDS); + public dateRangeModel = { startDate: this.today, endDate: this.inOneMonth }; + public modelDisabled = false; - public customDateFilter: StarkDatePickerFilter = (date: Date): boolean => date.getDay() !== 0; + public dateRangeFormGroup = new FormGroup({ + startDate: new FormControl(null, Validators.compose([DemoDateRangePickerPageComponent.noFebruaryValidator])), + endDate: new FormControl(null, Validators.compose([DemoDateRangePickerPageComponent.noFebruaryValidator])) + }); - public customDateMask: StarkTimestampMaskConfig = { - format: "DD-MM-YYYY" - }; + public getErrorMessages: (control: AbstractControl) => string[] = () => []; - public disabled = false; - public isDisabledDateMask = false; - public isDisabledCustomDateMask = false; + 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 + */ + private _subs: Subscription[] = []; public referenceList: ReferenceLink[] = [ { @@ -33,12 +71,33 @@ export class DemoDateRangePickerPageComponent { } ]; - public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) {} + public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) { + this._subs.push(this.dateRangeFormGroup.valueChanges.subscribe((v: any) => this.logger.debug("formGroup:", v))); + + // 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 onDateModelChange(): void { + this.logger.debug("ngModel", this.dateRangeModel); + } + + public onDateRangeFormGroupDisableCheckboxChange(event: MatCheckboxChange): void { + if (event.checked) { + this.dateRangeFormGroup.disable(); + } else { + this.dateRangeFormGroup.enable(); + } + } - public onDateChanged(event: StarkDateRangePickerEvent): void { - this.logger.debug(event); + public onDateChange(dateRange: StarkDateRangePickerEvent): void { + this.logger.debug("onChange:", dateRange); + } - this.startDate = event.startDate; - this.endDate = event.endDate; + public ngOnDestroy(): void { + for (const subscription of this._subs) { + subscription.unsubscribe() + } } } diff --git a/showcase/src/assets/examples/date-range-picker/custom-date-mask.html b/showcase/src/assets/examples/date-range-picker/custom-date-mask.html index 26443be59a..9d1118a8ec 100644 --- a/showcase/src/assets/examples/date-range-picker/custom-date-mask.html +++ b/showcase/src/assets/examples/date-range-picker/custom-date-mask.html @@ -1,20 +1 @@ -
- - -
-isDisabled + diff --git a/showcase/src/assets/examples/date-range-picker/custom-date-mask.ts b/showcase/src/assets/examples/date-range-picker/custom-date-mask.ts index f462aa9319..120f9866fa 100644 --- a/showcase/src/assets/examples/date-range-picker/custom-date-mask.ts +++ b/showcase/src/assets/examples/date-range-picker/custom-date-mask.ts @@ -1,34 +1,15 @@ -import { Component, Inject, OnInit } from "@angular/core"; +import { Component, Inject } from "@angular/core"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; -import { StarkDatePickerFilter, StarkDateRangePickerEvent, StarkTimestampMaskConfig } from "@nationalbankbelgium/stark-ui"; - -const DAY_IN_MILLISECONDS = 86400000; +import { StarkDateRangePickerEvent } from "@nationalbankbelgium/stark-ui"; @Component({ selector: "demo-date-range-picker", templateUrl: "./demo-date-range-picker.component.html" }) -export class DemoDateRangePickerComponent implements OnInit { - public startDate?: Date; - public endDate?: Date; - - public minDate = new Date(); - public maxDate = new Date(Date.now() + 30 * DAY_IN_MILLISECONDS); - - public customDateFilter: StarkDatePickerFilter = (date: Date): boolean => date.getDay() !== 0; - - public customDateMask: StarkTimestampMaskConfig = { - format: "DD-MM-YYYY" - }; - - public disabled = false; - +export class DemoDateRangePickerComponent { public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) {} - public onDateChanged(event: StarkDateRangePickerEvent): void { - this.logger.debug(event); - - this.startDate = event.startDate; - this.endDate = event.endDate; + public onDateChange(dateRange: StarkDateRangePickerEvent): void { + this.logger.debug("onChange:", dateRange); } } diff --git a/showcase/src/assets/examples/date-range-picker/date-mask.html b/showcase/src/assets/examples/date-range-picker/date-mask.html index d648945bad..22ed205613 100644 --- a/showcase/src/assets/examples/date-range-picker/date-mask.html +++ b/showcase/src/assets/examples/date-range-picker/date-mask.html @@ -1,20 +1 @@ -
- - - isDisabled -
+ diff --git a/showcase/src/assets/examples/date-range-picker/date-mask.ts b/showcase/src/assets/examples/date-range-picker/date-mask.ts index e6924415f0..120f9866fa 100644 --- a/showcase/src/assets/examples/date-range-picker/date-mask.ts +++ b/showcase/src/assets/examples/date-range-picker/date-mask.ts @@ -1,30 +1,15 @@ -import { Component, Inject, OnInit } from "@angular/core"; +import { Component, Inject } from "@angular/core"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; -import { StarkDatePickerFilter, StarkDateRangePickerEvent, StarkTimestampMaskConfig } from "@nationalbankbelgium/stark-ui"; - -const DAY_IN_MILLISECONDS = 86400000; +import { StarkDateRangePickerEvent } from "@nationalbankbelgium/stark-ui"; @Component({ selector: "demo-date-range-picker", templateUrl: "./demo-date-range-picker.component.html" }) -export class DemoDateRangePickerComponent implements OnInit { - public startDate?: Date; - public endDate?: Date; - - public minDate = new Date(); - public maxDate = new Date(Date.now() + 30 * DAY_IN_MILLISECONDS); - - public customDateFilter: StarkDatePickerFilter = (date: Date): boolean => date.getDay() !== 0; - - public disabled = false; - +export class DemoDateRangePickerComponent { public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) {} - public onDateChanged(event: StarkDateRangePickerEvent): void { - this.logger.debug(event); - - this.startDate = event.startDate; - this.endDate = event.endDate; + public onDateChange(dateRange: StarkDateRangePickerEvent): void { + this.logger.debug("onChange:", dateRange); } } diff --git a/showcase/src/assets/examples/date-range-picker/form-group.html b/showcase/src/assets/examples/date-range-picker/form-group.html new file mode 100644 index 0000000000..a176567650 --- /dev/null +++ b/showcase/src/assets/examples/date-range-picker/form-group.html @@ -0,0 +1,15 @@ + + + {{ error }} +
+
+ + + {{ error }} +
+
+
+

+ + {{ dateRangeFormGroup.disabled ? "Disabled" : "Enabled" }} + diff --git a/showcase/src/assets/examples/date-range-picker/form-group.ts b/showcase/src/assets/examples/date-range-picker/form-group.ts new file mode 100644 index 0000000000..c174adf6d3 --- /dev/null +++ b/showcase/src/assets/examples/date-range-picker/form-group.ts @@ -0,0 +1,66 @@ +/* tslint:disable:no-null-keyword */ +import { Component, Inject, OnDestroy } from "@angular/core"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +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", + templateUrl: "./demo-date-range-picker.component.html" +}) +export class DemoDateRangePickerComponent implements OnDestroy { + public static noFebruaryValidator(control: AbstractControl): ValidationErrors | null { + const { value } = control; + return value instanceof Date && value.getMonth() === 1 ? { inFebruary: true } : null; // date counts months from 0 + } + + public dateRangeFormGroup = new FormGroup({ + startDate: new FormControl(null, Validators.compose([DemoDateRangePickerComponent.noFebruaryValidator])), + endDate: new FormControl(null, Validators.compose([DemoDateRangePickerComponent.noFebruaryValidator])) + }); + + /** + * List of subscriptions to be unsubscribed when component is destroyed + */ + private _subs: Subscription[] = []; + + public getErrorMessages(control: AbstractControl): string[] { + return map( + control.errors || [], + (_value: any, key: string): string => { + switch (key) { + case "required": + return "Date is required"; + case "startBeforeEnd": + return "Start date should be before end date"; + case "endAfterStart": + return "End date should be after start date"; + case "inFebruary": + return "Date should not be in February"; + default: + return ""; + } + } + ); + } + + public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) { + this._subs.push(this.dateRangeFormGroup.valueChanges.subscribe((v: any) => this.logger.debug("formGroup:", v))); + } + + public onDateRangeFormGroupDisableCheckboxChange(event: MatCheckboxChange): void { + if (event.checked) { + this.dateRangeFormGroup.disable(); + } else { + this.dateRangeFormGroup.enable(); + } + } + + public ngOnDestroy(): void { + for (const subscription of this._subs) { + subscription.unsubscribe() + } + } +} diff --git a/showcase/src/assets/examples/date-range-picker/full.html b/showcase/src/assets/examples/date-range-picker/full.html deleted file mode 100644 index 1bbb69e1eb..0000000000 --- a/showcase/src/assets/examples/date-range-picker/full.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - -
-isDisabled diff --git a/showcase/src/assets/examples/date-range-picker/full.ts b/showcase/src/assets/examples/date-range-picker/full.ts deleted file mode 100644 index e279da8ba6..0000000000 --- a/showcase/src/assets/examples/date-range-picker/full.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Component, Inject } from "@angular/core"; -import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; -import { StarkDatePickerFilter, StarkDateRangePickerEvent } from "@nationalbankbelgium/stark-ui"; - -const DAY_IN_MILLISECONDS = 86400000; - -@Component({ - selector: "demo-date-range-picker", - templateUrl: "./demo-date-range-picker.component.html" -}) -export class DemoDateRangePickerComponent { - public startDate?: Date; - public endDate?: Date; - - public minDate = new Date(); - public maxDate = new Date(Date.now() + 30 * DAY_IN_MILLISECONDS); - - public customDateFilter: StarkDatePickerFilter = (date: Date): boolean => date.getDay() !== 0; - - public disabled = false; - - public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) {} - - public onDateChanged(event: StarkDateRangePickerEvent): void { - this.logger.debug(event); - - this.startDate = event.startDate; - this.endDate = event.endDate; - } -} diff --git a/showcase/src/assets/examples/date-range-picker/model.html b/showcase/src/assets/examples/date-range-picker/model.html new file mode 100644 index 0000000000..fb42c720bc --- /dev/null +++ b/showcase/src/assets/examples/date-range-picker/model.html @@ -0,0 +1,23 @@ + + + {{ error }} +
+
+ + + {{ error }} +
+
+
+

+{{ modelDisabled ? "Disabled" : "Enabled" }} diff --git a/showcase/src/assets/examples/date-range-picker/model.ts b/showcase/src/assets/examples/date-range-picker/model.ts new file mode 100644 index 0000000000..cf38f1804d --- /dev/null +++ b/showcase/src/assets/examples/date-range-picker/model.ts @@ -0,0 +1,56 @@ +/* tslint:disable:no-null-keyword */ +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; + +@Component({ + selector: "demo-date-range-picker", + templateUrl: "./demo-date-range-picker.component.html" +}) +export class DemoDateRangePickerComponent implements OnDestroy { + public today = new Date(); + public inOneMonth = new Date(this.today.getTime() + MONTH_IN_MILLI); + + 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) { + // 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 onDateModelChange(): void { + this.logger.debug("ngModel", this.dateRangeModel); + } +} diff --git a/showcase/src/assets/translations/en.json b/showcase/src/assets/translations/en.json index 6f40d4dcdf..27ad49b24a 100644 --- a/showcase/src/assets/translations/en.json +++ b/showcase/src/assets/translations/en.json @@ -52,9 +52,20 @@ "TITLE": "Date picker" }, "DATE_RANGE_PICKER": { - "CUSTOM_MASK": "Date range picker - Custom date mask ({{ mask }})", - "DEFAULT_MASK": "Date range picker - Default date mask", - "TITLE": "Date range picker" + "TITLE": "Date range picker", + "WITH_MODEL": "ngModel", + "WITH_FORM_GROUP": "Form group", + "WITH_MASK": "Default date mask", + "WITH_CUSTOM_MASK": "Custom date mask (DD-MM-YYYY)", + "ERROR_MESSAGES": { + "MIN_TODAY": "Date should be after today", + "MAX_MONTH": "Date should be in less than 1 month", + "WEEKDAY": "Date should be a weekday", + "START_BEFORE_END": "Start date should be before end date", + "END_AFTER_START": "End date should be after start date", + "IN_FEBRUARY": "Date should not be in February", + "REQUIRED": "Date is required" + } }, "DIALOGS": { "ALERT": { diff --git a/showcase/src/assets/translations/fr.json b/showcase/src/assets/translations/fr.json index dd526312d4..454466b63a 100644 --- a/showcase/src/assets/translations/fr.json +++ b/showcase/src/assets/translations/fr.json @@ -52,9 +52,20 @@ "TITLE": "Date picker" }, "DATE_RANGE_PICKER": { - "CUSTOM_MASK": "Date range picker - Masque de date personnalisé ({{ mask }})", - "DEFAULT_MASK": "Date range picker - Masque de date par défaut", - "TITLE": "Date range picker" + "TITLE": "Date range picker", + "WITH_MODEL": "ngModel", + "WITH_FORM_GROUP": "Form group", + "WITH_MASK": "Masque date par défaut", + "WITH_CUSTOM_MASK": "Masque date personnalisé (DD-MM-YYYY)", + "ERROR_MESSAGES": { + "MIN_TODAY": "La date devrait être postérieure à aujourd'hui", + "MAX_MONTH": "La date devrait être dans moins d'un mois", + "WEEKDAY": "La date doit être un jour de semaine", + "START_BEFORE_END": "La date de début doit être antérieure à la date de fin", + "END_AFTER_START": "La date de fin doit être postérieure à la date de début", + "IN_FEBRUARY": "La date ne devrait pas être en février", + "REQUIRED": "La date est requise" + } }, "DIALOGS": { "ALERT": { diff --git a/showcase/src/assets/translations/nl.json b/showcase/src/assets/translations/nl.json index 22495df35b..b249d4a60f 100644 --- a/showcase/src/assets/translations/nl.json +++ b/showcase/src/assets/translations/nl.json @@ -52,9 +52,20 @@ "TITLE": "Date picker" }, "DATE_RANGE_PICKER": { - "CUSTOM_MASK": "Date range picker - Aangepast datummasker ({{ mask }})", - "DEFAULT_MASK": "Date range picker - Standaard datummasker", - "TITLE": "Date range picker" + "TITLE": "Date range picker", + "WITH_MODEL": "ngModel", + "WITH_FORM_GROUP": "Form group", + "WITH_MASK": "Standaard datummasker", + "WITH_CUSTOM_MASK": "Aangepast datummasker (DD-MM-YYYY)", + "ERROR_MESSAGES": { + "MIN_TODAY": "Datum moet na vandaag zijn", + "MAX_MONTH": "Datum moet binnen minder dan 1 maand zijn", + "WEEKDAY": "Datum moet een weekdag zijn", + "START_BEFORE_END": "Startdatum moet voor einddatum zijn", + "END_AFTER_START": "Einddatum moet na startdatum zijn", + "IN_FEBRUARY": "Datum mag niet in februari zijn", + "REQUIRED": "Datum is verplicht" + } }, "DIALOGS": { "ALERT": {