src/modules/date-range-picker/components/date-range-picker.component.ts
Component to display the stark date-range-picker
ControlValueAccessor
OnInit
OnDestroy
encapsulation | ViewEncapsulation.None |
host | { |
providers |
{
provide: NG_VALUE_ACCESSOR, multi: true, useExisting: StarkDateRangePickerComponent
}
|
selector | stark-date-range-picker |
templateUrl | ./date-range-picker.component.html |
Properties |
|
Methods |
|
Inputs |
Outputs |
Accessors |
Public
constructor(logger: StarkLoggingService, injector: Injector, renderer: Renderer2, elementRef: ElementRef, cdRef: ChangeDetectorRef)
|
||||||||||||||||||||||||
Class constructor
Parameters :
|
dateFilter | |
Type : StarkDatePickerFilter
|
|
Filter function or a string Will be applied to both date-picker |
dateMask | |
Type : StarkDatePickerMaskConfig | undefined
|
|
Timestamp Mask Configuration to apply on the start/end date-picker.
If |
disabled | |
Type : boolean
|
|
Whether the date pickers are disabled |
endDate | |
Type : Date | undefined
|
|
Source Date to be bound to the end datepicker model |
endDateLabel | |
Type : string
|
|
Default value : "STARK.DATE_RANGE_PICKER.TO"
|
|
Label to be displayed in the end datepicker |
endMaxDate | |
Type : moment.Moment | null
|
|
Maximum date of the end date picker |
endMinDate | |
Type : moment.Moment | null
|
|
Minimum date of the end date picker |
rangeFormGroup | |
Type : UntypedFormGroup
|
|
Input to manage both start date and end date. |
rangePickerId | |
Type : string
|
|
Default value : ""
|
|
HTML "name" attribute of the element. |
rangePickerName | |
Type : string
|
|
Default value : ""
|
|
HTML "name" attribute of the element. |
required | |
Type : boolean
|
|
Whether the date pickers are required |
startDate | |
Type : Date | undefined
|
|
Source Date to be bound to the start datepicker model |
startDateLabel | |
Type : string
|
|
Default value : "STARK.DATE_RANGE_PICKER.FROM"
|
|
Label to be displayed in the end datepicker |
startMaxDate | |
Type : moment.Moment | null
|
|
Maximum date of the start date picker |
startMinDate | |
Type : moment.Moment | null
|
|
Minimum date of the start date picker |
color | |
Type : string
|
|
Inherited from
AbstractStarkUiComponent
|
|
Defined in
AbstractStarkUiComponent:16
|
|
Color theme |
dateRangeChanged | |
Type : EventEmitter
|
|
Output that will emit a specific date range whenever the selection has changed |
Public ngOnDestroy |
ngOnDestroy()
|
Angular lifecycle method
Returns :
void
|
Public ngOnInit |
ngOnInit()
|
Inherited from
AbstractStarkUiComponent
|
Defined in
AbstractStarkUiComponent:484
|
Angular lifecycle method
Returns :
void
|
Public onDateChanged | ||||||||
onDateChanged(dateOrigin: "start" | "end")
|
||||||||
Handle the date changed on the start and end datepicker
Parameters :
Returns :
void
|
Private Optional _dateMask |
Type : StarkDatePickerMaskConfig
|
Public endPicker |
Type : StarkDatePickerComponent
|
Decorators :
@ViewChild('endPicker', {static: true})
|
Reference to the end datepicker embedded in this component |
Public logger |
Type : StarkLoggingService
|
Decorators :
@Inject(STARK_LOGGING_SERVICE)
|
- The `StarkLoggingService` instance of the application.
|
Static ngAcceptInputType_dateMask |
Type : BooleanInput | StarkDatePickerMaskConfig
|
Static ngAcceptInputType_endMaxDate |
Type : StarkDateInput
|
Static ngAcceptInputType_endMinDate |
Type : StarkDateInput
|
Static ngAcceptInputType_required |
Type : BooleanInput
|
Static ngAcceptInputType_startMaxDate |
Type : StarkDateInput
|
Static ngAcceptInputType_startMinDate |
Type : StarkDateInput
|
Public startPicker |
Type : StarkDatePickerComponent
|
Decorators :
@ViewChild('startPicker', {static: true})
|
Reference to the start datepicker embedded in this component |
startDate | ||||||
getstartDate()
|
||||||
Source Date to be bound to the start datepicker model
Returns :
Date | undefined
|
||||||
setstartDate(value: Date | undefined)
|
||||||
Parameters :
Returns :
void
|
startMinDate | ||||||
getstartMinDate()
|
||||||
setstartMinDate(value: moment.Moment | null)
|
||||||
Minimum date of the start date picker
Parameters :
Returns :
void
|
startMaxDate | ||||||
getstartMaxDate()
|
||||||
setstartMaxDate(value: moment.Moment | null)
|
||||||
Maximum date of the start date picker
Parameters :
Returns :
void
|
endDate | ||||||
getendDate()
|
||||||
Source Date to be bound to the end datepicker model
Returns :
Date | undefined
|
||||||
setendDate(value: Date | undefined)
|
||||||
Parameters :
Returns :
void
|
endMinDate | ||||||
getendMinDate()
|
||||||
setendMinDate(value: moment.Moment | null)
|
||||||
Minimum date of the end date picker
Parameters :
Returns :
void
|
endMaxDate | ||||||
getendMaxDate()
|
||||||
setendMaxDate(value: moment.Moment | null)
|
||||||
Maximum date of the end date picker
Parameters :
Returns :
void
|
rangeFormGroup | ||||||
setrangeFormGroup(val: UntypedFormGroup)
|
||||||
Input to manage both start date and end date.
Parameters :
Returns :
void
|
dateMask | ||||||
getdateMask()
|
||||||
Timestamp Mask Configuration to apply on the start/end date-picker.
If
Returns :
StarkDatePickerMaskConfig | undefined
|
||||||
setdateMask(value: StarkDatePickerMaskConfig | undefined)
|
||||||
Parameters :
Returns :
void
|
disabled | ||||||
setdisabled(val: boolean)
|
||||||
Whether the date pickers are disabled
Parameters :
Returns :
void
|
required | ||||||
getrequired()
|
||||||
Whether the date pickers are required
Returns :
boolean
|
||||||
setrequired(value: boolean)
|
||||||
Parameters :
Returns :
void
|
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Inject,
Injector,
Input,
OnDestroy,
OnInit,
Output,
Renderer2,
ViewChild,
ViewEncapsulation
} from "@angular/core";
import {
AbstractControl,
ControlValueAccessor,
UntypedFormControl,
UntypedFormGroup,
NG_VALUE_ACCESSOR,
NgControl,
ValidatorFn,
Validators
} from "@angular/forms";
import { Subscription } from "rxjs";
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 "@nationalbankbelgium/stark-ui/src/internal-common";
import {
StarkDateInput,
StarkDatePickerComponent,
StarkDatePickerFilter,
StarkDatePickerMaskConfig
} from "@nationalbankbelgium/stark-ui/src/modules/date-picker";
import { StarkDateRangePickerEvent } from "./date-range-picker-event.intf";
import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion";
import { isStarkTimestampMaskConfig } from "@nationalbankbelgium/stark-ui/src/modules/input-mask-directives";
import moment from "moment";
/**
* @ignore
*/
const componentName = "stark-date-range-picker";
/**
* Component to display the stark date-range-picker
*/
@Component({
selector: "stark-date-range-picker",
templateUrl: "./date-range-picker.component.html",
encapsulation: ViewEncapsulation.None,
// 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 ControlValueAccessor, OnInit, OnDestroy {
/**
* Source Date to be bound to the start datepicker model
*/
@Input()
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
*
* <stark-date-range-picker required #starkDateRangePicker >
* <ng-container start-date-errors>
* {{starkDateRangePicker.startDateFormControl.hasError('required') ? "Start date is required" : null }}
* </ng-container>
* </stark-date-range-picker>
*/
public get startDateFormControl(): UntypedFormControl {
return this._startDate;
}
/**
* @ignore
*/
private _startDate!: UntypedFormControl; // will be defined by '_setupFormControls()' called in the constructor
/**
* Label to be displayed in the end datepicker
*/
@Input()
public startDateLabel = "STARK.DATE_RANGE_PICKER.FROM";
/**
* Minimum date of the start date picker
*/
@Input()
public set startMinDate(value: moment.Moment | null) {
if (value === undefined) {
// eslint-disable-next-line no-null/no-null
this._startMinDate = null;
} else if (value instanceof Date) {
this._startMinDate = moment(value);
} else {
this._startMinDate = value;
}
}
public get startMinDate(): moment.Moment | null {
return this._startMinDate;
}
// Information about input setter coercion https://angular.io/guide/template-typecheck#input-setter-coercion
public static ngAcceptInputType_startMinDate: StarkDateInput;
/**
* @ignore
* Angular expects a Moment or null value.
*/
// eslint-disable-next-line no-null/no-null
private _startMinDate: moment.Moment | null = null;
/**
* Maximum date of the start date picker
*/
@Input()
public set startMaxDate(value: moment.Moment | null) {
if (value === undefined) {
// eslint-disable-next-line no-null/no-null
this._startMaxDate = null;
} else if (value instanceof Date) {
this._startMaxDate = moment(value);
} else {
this._startMaxDate = value;
}
}
public get startMaxDate(): moment.Moment | null {
return this._startMaxDate;
}
// Information about input setter coercion https://angular.io/guide/template-typecheck#input-setter-coercion
public static ngAcceptInputType_startMaxDate: StarkDateInput;
/**
* @ignore
* Angular expects a Moment or null value.
*/
// eslint-disable-next-line no-null/no-null
private _startMaxDate: moment.Moment | null = null;
/**
* Source Date to be bound to the end datepicker model
*/
@Input()
public get endDate(): Date | undefined {
return this._endDate.value || undefined;
}
public set endDate(value: Date | undefined) {
this._endDate.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
*
* <stark-date-range-picker required #starkDateRangePicker >
* <ng-container end-date-errors>
* {{starkDateRangePicker.endDateFormControl.hasError('required') ? "End date is required" : null }}
* </ng-container>
* </stark-date-range-picker>
*/
public get endDateFormControl(): UntypedFormControl {
return this._endDate;
}
/**
* @ignore
*/
private _endDate!: UntypedFormControl; // will be defined by '_setupFormControls()' called in the constructor
/**
* Label to be displayed in the end datepicker
*/
@Input()
public endDateLabel = "STARK.DATE_RANGE_PICKER.TO";
/**
* Minimum date of the end date picker
*/
@Input()
public set endMinDate(value: moment.Moment | null) {
if (value === undefined) {
// eslint-disable-next-line no-null/no-null
this._endMinDate = null;
} else if (value instanceof Date) {
this._endMinDate = moment(value);
} else {
this._endMinDate = value;
}
}
public get endMinDate(): moment.Moment | null {
// use the startDate when defined to provide better user experience :)
return this.startDate ? moment(this.startDate) : this._endMinDate;
}
// Information about input setter coercion https://angular.io/guide/template-typecheck#input-setter-coercion
public static ngAcceptInputType_endMinDate: StarkDateInput;
/**
* @ignore
* Angular expects a Moment or null value.
*/
// eslint-disable-next-line no-null/no-null
private _endMinDate: moment.Moment | null = null;
/**
* Maximum date of the end date picker
*/
@Input()
public set endMaxDate(value: moment.Moment | null) {
if (value === undefined) {
// eslint-disable-next-line no-null/no-null
this._endMaxDate = null;
} else if (value instanceof Date) {
this._endMaxDate = moment(value);
} else {
this._endMaxDate = value;
}
}
public get endMaxDate(): moment.Moment | null {
return this._endMaxDate;
}
// Information about input setter coercion https://angular.io/guide/template-typecheck#input-setter-coercion
public static ngAcceptInputType_endMaxDate: StarkDateInput;
/**
* @ignore
* Angular expects a Moment or null value.
*/
// eslint-disable-next-line no-null/no-null
private _endMaxDate: moment.Moment | null = null;
/**
* Input to manage both start date and end date.
*/
@Input()
public set rangeFormGroup(val: UntypedFormGroup) {
const { startDate, endDate } = val.controls;
if (!(startDate instanceof UntypedFormControl && endDate instanceof UntypedFormControl)) {
this.logger.error(`[${componentName}]: "formGroup" requires a FormControl for startDate and another one for endDate`);
return;
}
this._formGroup = val;
// overwrite internal formControls and setup again the validators, subscriptions, etc.
this._startDate = startDate;
this._endDate = endDate;
this._setupFormControls();
}
/**
* @ignore
*/
private _formGroup?: UntypedFormGroup;
/**
* Filter function or a string
* Will be applied to both date-picker
*/
@Input()
public dateFilter?: StarkDatePickerFilter;
/**
* 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.
*/
@Input()
public get dateMask(): StarkDatePickerMaskConfig | undefined {
return this._dateMask;
}
public set dateMask(value: StarkDatePickerMaskConfig | undefined) {
this._dateMask = isStarkTimestampMaskConfig(value) ? value : coerceBooleanProperty(value);
}
private _dateMask?: StarkDatePickerMaskConfig;
// Information about boolean coercion https://angular.io/guide/template-typecheck#input-setter-coercion
public static ngAcceptInputType_dateMask: BooleanInput | StarkDatePickerMaskConfig;
/**
* Whether the date pickers 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})
});
`);
}
// enable/disable the controls without emitting a change event since the values did not change (to avoid unnecessary extra calls!)
if (val) {
if (this.startDateFormControl) {
this.startDateFormControl.disable({ emitEvent: false });
}
if (this.endDateFormControl) {
this.endDateFormControl.disable({ emitEvent: false });
}
} else {
if (this.startDateFormControl) {
this.startDateFormControl.enable({ emitEvent: false });
}
if (this.endDateFormControl) {
this.endDateFormControl.enable({ emitEvent: false });
}
}
}
/**
* Whether the date pickers are required
*/
@Input()
public get required(): boolean {
return this._required;
}
public set required(value: boolean) {
this._required = coerceBooleanProperty(value);
}
// Information about boolean coercion https://angular.io/guide/template-typecheck#input-setter-coercion
public static ngAcceptInputType_required: BooleanInput;
/**
* @ignore
* @internal
*/
private _required = false;
/**
* HTML "name" attribute of the element.
*/
@Input()
public rangePickerId = "";
/**
* HTML "name" attribute of the element.
*/
@Input()
public rangePickerName = "";
/**
* Output that will emit a specific date range whenever the selection has changed
*/
@Output()
public readonly dateRangeChanged = new EventEmitter<StarkDateRangePickerEvent>();
/**
* Reference to the start datepicker embedded in this component
*/
@ViewChild("startPicker", { static: true })
public startPicker!: StarkDatePickerComponent;
/**
* Reference to the end datepicker embedded in this component
*/
@ViewChild("endPicker", { static: true })
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 }) =>
value instanceof Date && this.endDate instanceof Date && value.getTime() > this.endDate.getTime() ? { startBeforeEnd: true } : null;
/**
* @ignore
* @internal
*/
private _endAfterStartValidator: ValidatorFn = ({ value }) =>
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);
}
return null;
};
/**
* Class constructor
* @param logger - The `StarkLoggingService` instance of the application.
* @param injector - The Injector of the application
* @param renderer - Angular `Renderer2` wrapper for DOM manipulations.
* @param elementRef - Reference to the DOM element where this component is attached to.
* @param cdRef - Reference to the change detector attached to this component
*/
public constructor(
@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService,
private injector: Injector,
renderer: Renderer2,
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 override ngOnInit(): void {
super.ngOnInit();
this._setupNgControl();
this.logger.debug(componentName + ": component initialized");
}
/**
* Angular lifecycle method
*/
public ngOnDestroy(): void {
for (const subscription of this.subs) {
subscription.unsubscribe();
}
}
/**
* @ignore
* @internal
*/
private _setupFormControls(): void {
// 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 UntypedFormControl(undefined, [this._requiredValidator, this._startBeforeEndValidator]);
}
if (this.endDateFormControl) {
this.endDateFormControl.setValidators(
Validators.compose([this.endDateFormControl.validator, this._requiredValidator, this._endAfterStartValidator])
);
} else {
this._endDate = new UntypedFormControl(undefined, [this._requiredValidator, this._endAfterStartValidator]);
}
for (const subscription of this.subs) {
subscription.unsubscribe();
}
this.subs.push(
this.startDateFormControl.valueChanges.pipe(distinctUntilChanged()).subscribe((_value: Date) => {
this.onDateChanged("start");
}),
this.endDateFormControl.valueChanges.pipe(distinctUntilChanged()).subscribe((_value: Date) => {
this.onDateChanged("end");
})
);
}
/**
* Link the passed control (if one is available) to the internal formControls.
* @ignore
* @internal
*/
private _setupNgControl(): void {
// Get the ngControl from the injector
const ngControl = this.injector.get<NgControl>(NgControl, <any>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.`
);
}
}
/**
* 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(dateOrigin: "start" | "end"): void {
this._onTouched();
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 };
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 });
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 || {};
this.startDateFormControl.setValue(dateRange.startDate, { emitEvent: false });
this.endDateFormControl.setValue(dateRange.endDate, { emitEvent: false });
}
}
<mat-form-field>
<stark-date-picker
#startPicker
[pickerId]="rangePickerId + '-start'"
[pickerName]="rangePickerName + '-start'"
[formControl]="startDateFormControl"
[placeholder]="startDateLabel | translate"
[dateFilter]="dateFilter"
[dateMask]="dateMask"
[min]="startMinDate"
[max]="startMaxDate"
[required]="required"
>
</stark-date-picker>
<!-- <mat-error> should always be a direct child of <mat-form-field> -->
<mat-error>
<ng-content select="[start-date-errors]"></ng-content>
</mat-error>
</mat-form-field>
<mat-form-field>
<stark-date-picker
#endPicker
[pickerId]="rangePickerId + '-end'"
[pickerName]="rangePickerName + '-end'"
[formControl]="endDateFormControl"
[placeholder]="endDateLabel | translate"
[dateFilter]="dateFilter"
[dateMask]="dateMask"
[min]="endMinDate"
[max]="endMaxDate"
[required]="required"
>
</stark-date-picker>
<!-- <mat-error> should always be a direct child of <mat-form-field> -->
<mat-error>
<ng-content select="[end-date-errors]"></ng-content>
</mat-error>
</mat-form-field>