• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 8570372476

05 Apr 2024 01:11PM UTC coverage: 91.809% (-0.02%) from 91.829%
8570372476

push

github

web-flow
refactor(calendar): implement improved design and functionallity (#13650)

12432 of 14504 branches covered (85.71%)

749 of 816 new or added lines in 18 files covered. (91.79%)

18 existing lines in 8 files now uncovered.

25421 of 27689 relevant lines covered (91.81%)

30246.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.38
/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts
1
import {
2
    AfterViewInit, booleanAttribute, ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef,
3
    EventEmitter, HostBinding, HostListener, Inject, Injector, Input, LOCALE_ID,
4
    OnChanges, OnDestroy, OnInit, Optional, Output, QueryList,
5
    SimpleChanges, TemplateRef, ViewChild, ViewContainerRef
6
} from '@angular/core';
7
import { NgTemplateOutlet, NgIf } from '@angular/common';
8
import {
9
    AbstractControl, ControlValueAccessor, NgControl,
10
    NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator
11
} from '@angular/forms';
12

13
import { fromEvent, merge, MonoTypeOperatorFunction, noop, Subscription } from 'rxjs';
14
import { filter, takeUntil } from 'rxjs/operators';
15

16
import { CalendarSelection, IgxCalendarComponent } from '../calendar/public_api';
17
import { DateRangeType } from '../core/dates';
18
import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density';
19
import { DateRangePickerResourceStringsEN, IDateRangePickerResourceStrings } from '../core/i18n/date-range-picker-resources';
20
import { IBaseCancelableBrowserEventArgs, isDate, parseDate, PlatformUtil } from '../core/utils';
21
import { IgxCalendarContainerComponent } from '../date-common/calendar-container/calendar-container.component';
22
import { PickerBaseDirective } from '../date-common/picker-base.directive';
23
import { IgxPickerActionsDirective } from '../date-common/picker-icons.common';
24
import { DateTimeUtil } from '../date-common/util/date-time.util';
25
import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive';
26
import {
27
    IgxInputDirective, IgxInputGroupComponent, IgxInputGroupType, IgxInputState,
28
    IgxLabelDirective, IGX_INPUT_GROUP_TYPE
29
} from '../input-group/public_api';
30
import {
31
    AutoPositionStrategy, IgxOverlayService, OverlayCancelableEventArgs, OverlayEventArgs,
32
    OverlaySettings, PositionSettings
33
} from '../services/public_api';
34
import { DateRange, IgxDateRangeEndComponent, IgxDateRangeInputsBaseComponent, IgxDateRangeSeparatorDirective, IgxDateRangeStartComponent, DateRangePickerFormatPipe } from './date-range-picker-inputs.common';
35
import { IgxPrefixDirective } from '../directives/prefix/prefix.directive';
36
import { IgxIconComponent } from '../icon/icon.component';
37
import { getCurrentResourceStrings } from '../core/i18n/resources';
38
import { fadeIn, fadeOut } from 'igniteui-angular/animations';
39

40
const SingleInputDatesConcatenationString = ' - ';
2✔
41

42
/**
43
 * Provides the ability to select a range of dates from a calendar UI or editable inputs.
44
 *
45
 * @igxModule IgxDateRangeModule
46
 *
47
 * @igxTheme igx-input-group-theme, igx-calendar-theme, igx-date-range-picker-theme
48
 *
49
 * @igxKeywords date, range, date range, date picker
50
 *
51
 * @igxGroup scheduling
52
 *
53
 * @remarks
54
 * It displays the range selection in a single or two input fields.
55
 * The default template displays a single *readonly* input field
56
 * while projecting `igx-date-range-start` and `igx-date-range-end`
57
 * displays two *editable* input fields.
58
 *
59
 * @example
60
 * ```html
61
 * <igx-date-range-picker mode="dropdown"></igx-date-range-picker>
62
 * ```
63
 */
64
@Component({
65
    selector: 'igx-date-range-picker',
66
    templateUrl: './date-range-picker.component.html',
67
    providers: [
68
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxDateRangePickerComponent, multi: true },
69
        { provide: NG_VALIDATORS, useExisting: IgxDateRangePickerComponent, multi: true }
70
    ],
71
    standalone: true,
72
    imports: [
73
        NgIf,
74
        NgTemplateOutlet,
75
        IgxIconComponent,
76
        IgxInputGroupComponent,
77
        IgxInputDirective,
78
        IgxPrefixDirective,
79
        DateRangePickerFormatPipe
80
    ]
81
})
82
export class IgxDateRangePickerComponent extends PickerBaseDirective
2✔
83
    implements OnChanges, OnInit, AfterViewInit, OnDestroy, ControlValueAccessor, Validator {
84

85
    /**
86
     * The number of displayed month views.
87
     *
88
     * @remarks
89
     * Default is `2`.
90
     *
91
     * @example
92
     * ```html
93
     * <igx-date-range-picker [displayMonthsCount]="3"></igx-date-range-picker>
94
     * ```
95
     */
96
    @Input()
97
    public displayMonthsCount = 2;
69✔
98

99
    /**
100
     * Gets/Sets whether dates that are not part of the current month will be displayed.
101
     *
102
     * @remarks
103
     * Default value is `false`.
104
     *
105
     * @example
106
     * ```html
107
     * <igx-date-range-picker [hideOutsideDays]="true"></igx-date-range-picker>
108
     * ```
109
     */
110
    @Input({ transform: booleanAttribute })
111
    public hideOutsideDays: boolean;
112

113
    /**
114
     * A custom formatter function, applied on the selected or passed in date.
115
     *
116
     * @example
117
     * ```typescript
118
     * private dayFormatter = new Intl.DateTimeFormat("en", { weekday: "long" });
119
     * private monthFormatter = new Intl.DateTimeFormat("en", { month: "long" });
120
     *
121
     * public formatter(date: Date): string {
122
     *  return `${this.dayFormatter.format(date)} - ${this.monthFormatter.format(date)} - ${date.getFullYear()}`;
123
     * }
124
     * ```
125
     * ```html
126
     * <igx-date-range-picker [formatter]="formatter"></igx-date-range-picker>
127
     * ```
128
     */
129
    @Input()
130
    public formatter: (val: DateRange) => string;
131

132
    /**
133
     * Overrides the default text of the calendar dialog **Done** button.
134
     *
135
     * @remarks
136
     * Defaults to the value from resource strings, `"Done"` for the built-in EN.
137
     * The button will only show up in `dialog` mode.
138
     *
139
     * @example
140
     * ```html
141
     * <igx-date-range-picker doneButtonText="完了"></igx-date-range-picker>
142
     * ```
143
     */
144
    @Input()
145
    public set doneButtonText(value: string) {
146
        this._doneButtonText = value;
1✔
147
    }
148

149
    public get doneButtonText(): string {
150
        if (this._doneButtonText === null) {
6✔
151
            return this.resourceStrings.igx_date_range_picker_done_button;
5✔
152
        }
153
        return this._doneButtonText;
1✔
154
    }
155
    /**
156
     * Custom overlay settings that should be used to display the calendar.
157
     *
158
     * @example
159
     * ```html
160
     * <igx-date-range-picker [overlaySettings]="customOverlaySettings"></igx-date-range-picker>
161
     * ```
162
     */
163
    @Input()
164
    public override overlaySettings: OverlaySettings;
165

166
    /**
167
     * The format used when editable inputs are not focused.
168
     *
169
     * @remarks
170
     * Uses Angular's DatePipe.
171
     *
172
     * @example
173
     * ```html
174
     * <igx-date-range-picker displayFormat="EE/M/yy"></igx-date-range-picker>
175
     * ```
176
     *
177
     */
178
    @Input()
179
    public override displayFormat: string;
180

181
    /**
182
     * The expected user input format and placeholder.
183
     *
184
     * @remarks
185
     * Default is `"'MM/dd/yyyy'"`
186
     *
187
     * @example
188
     * ```html
189
     * <igx-date-range-picker inputFormat="dd/MM/yy"></igx-date-range-picker>
190
     * ```
191
     */
192
    @Input()
193
    public override inputFormat: string;
194

195
    /**
196
     * The minimum value in a valid range.
197
     *
198
     * @example
199
     * <igx-date-range-picker [minValue]="minDate"></igx-date-range-picker>
200
     */
201
    @Input()
202
    public set minValue(value: Date | string) {
203
        this._minValue = value;
27✔
204
        this.onValidatorChange();
27✔
205
    }
206

207
    public get minValue(): Date | string {
208
        return this._minValue;
99✔
209
    }
210

211
    /**
212
     * The maximum value in a valid range.
213
     *
214
     * @example
215
     * <igx-date-range-picker [maxValue]="maxDate"></igx-date-range-picker>
216
     */
217
    @Input()
218
    public set maxValue(value: Date | string) {
219
        this._maxValue = value;
27✔
220
        this.onValidatorChange();
27✔
221
    }
222

223
    public get maxValue(): Date | string {
224
        return this._maxValue;
99✔
225
    }
226

227
    /**
228
     * An accessor that sets the resource strings.
229
     * By default it uses EN resources.
230
     */
231
    @Input()
232
    public set resourceStrings(value: IDateRangePickerResourceStrings) {
233
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
×
234
    }
235

236
    /**
237
     * An accessor that returns the resource strings.
238
     */
239
    public get resourceStrings(): IDateRangePickerResourceStrings {
240
        return this._resourceStrings;
45✔
241
    }
242

243
    /**
244
     * Sets the `placeholder` for single-input `IgxDateRangePickerComponent`.
245
     *
246
     *   @example
247
     * ```html
248
     * <igx-date-range-picker [placeholder]="'Choose your dates'"></igx-date-range-picker>
249
     * ```
250
     */
251
    @Input()
252
    public override placeholder = '';
69✔
253

254
    /**
255
     * Gets/Sets the container used for the popup element.
256
     *
257
     * @remarks
258
     *  `outlet` is an instance of `IgxOverlayOutletDirective` or an `ElementRef`.
259
     * @example
260
     * ```html
261
     * <div igxOverlayOutlet #outlet="overlay-outlet"></div>
262
     * //..
263
     * <igx-date-range-picker [outlet]="outlet"></igx-date-range-picker>
264
     * //..
265
     * ```
266
     */
267
    @Input()
268
    public override outlet: IgxOverlayOutletDirective | ElementRef<any>;
269

270
    /**
271
     * Show/hide week numbers
272
     *
273
     * @remarks
274
     * Default is `false`.
275
     *
276
     * @example
277
     * ```html
278
     * <igx-date-range-picker [showWeekNumbers]="true"></igx-date-range-picker>
279
     * ``
280
     */
281
    @Input({ transform: booleanAttribute })
282
    public showWeekNumbers = false;
69✔
283

284
    /**
285
     * Emitted when the picker's value changes. Used for two-way binding.
286
     *
287
     * @example
288
     * ```html
289
     * <igx-date-range-picker [(value)]="date"></igx-date-range-picker>
290
     * ```
291
     */
292
    @Output()
293
    public valueChange = new EventEmitter<DateRange>();
69✔
294

295
    /** @hidden @internal */
296
    @HostBinding('class.igx-date-range-picker')
297
    public cssClass = 'igx-date-range-picker';
69✔
298

299
    @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef })
300
    private viewContainerRef: ViewContainerRef;
301

302
    /** @hidden @internal */
303
    @ViewChild(IgxInputDirective)
304
    public inputDirective: IgxInputDirective;
305

306
    /** @hidden @internal */
307
    @ContentChildren(IgxDateRangeInputsBaseComponent)
308
    public projectedInputs: QueryList<IgxDateRangeInputsBaseComponent>;
309

310
    @ContentChild(IgxLabelDirective)
311
    public label: IgxLabelDirective;
312

313
    @ContentChild(IgxPickerActionsDirective)
314
    public pickerActions: IgxPickerActionsDirective;
315

316
    /** @hidden @internal */
317
    @ContentChild(IgxDateRangeSeparatorDirective, { read: TemplateRef })
318
    public dateSeparatorTemplate: TemplateRef<any>;
319

320
    /** @hidden @internal */
321
    public get dateSeparator(): string {
322
        if (this._dateSeparator === null) {
40✔
323
            return this.resourceStrings.igx_date_range_picker_date_separator;
40✔
324
        }
325
        return this._dateSeparator;
×
326
    }
327

328
    /** @hidden @internal */
329
    public get appliedFormat(): string {
330
        return DateTimeUtil.getLocaleDateFormat(this.locale, this.displayFormat)
506✔
331
            || DateTimeUtil.DEFAULT_INPUT_FORMAT;
332
    }
333

334
    /** @hidden @internal */
335
    public get singleInputFormat(): string {
336
        if (this.placeholder !== '') {
236✔
337
            return this.placeholder;
2✔
338
        }
339

340
        const format = this.appliedFormat;
234✔
341
        return `${format}${SingleInputDatesConcatenationString}${format}`;
234✔
342
    }
343

344
    /**
345
     * Gets calendar state.
346
     *
347
     * ```typescript
348
     * let state = this.dateRange.collapsed;
349
     * ```
350
     */
351
    public override get collapsed(): boolean {
352
        return this._collapsed;
404✔
353
    }
354

355
    /**
356
     * The currently selected value / range from the calendar
357
     *
358
     * @remarks
359
     * The current value is of type `DateRange`
360
     *
361
     * @example
362
     * ```typescript
363
     * const newValue: DateRange = { start: new Date("2/2/2012"), end: new Date("3/3/2013")};
364
     * this.dateRangePicker.value = newValue;
365
     * ```
366
     */
367
    public get value(): DateRange | null {
368
        return this._value;
900✔
369
    }
370

371
    @Input()
372
    public set value(value: DateRange | null) {
373
        this.updateValue(value);
70✔
374
        this.onChangeCallback(value);
70✔
375
        this.valueChange.emit(value);
70✔
376
    }
377

378
    /** @hidden @internal */
379
    public get hasProjectedInputs(): boolean {
380
        return this.projectedInputs?.length > 0;
1,317✔
381
    }
382

383
    /** @hidden @internal */
384
    public get separatorClass(): string {
385
        return 'igx-date-range-picker__label';
240✔
386
    }
387

388
    private get required(): boolean {
389
        if (this._ngControl && this._ngControl.control && this._ngControl.control.validator) {
135✔
390
            const error = this._ngControl.control.validator({} as AbstractControl);
98✔
391
            return (error && error.required) ? true : false;
98!
392
        }
393

394
        return false;
37✔
395
    }
396

397
    private get calendar(): IgxCalendarComponent {
398
        return this._calendar;
573✔
399
    }
400

401
    private get dropdownOverlaySettings(): OverlaySettings {
402
        return Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
27✔
403
    }
404

405
    private get dialogOverlaySettings(): OverlaySettings {
406
        return Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
6✔
407
    }
408

409
    private _resourceStrings = getCurrentResourceStrings(DateRangePickerResourceStringsEN);
69✔
410
    private _doneButtonText = null;
69✔
411
    private _dateSeparator = null;
69✔
412
    private _value: DateRange | null;
413
    private _overlayId: string;
414
    private _ngControl: NgControl;
415
    private _statusChanges$: Subscription;
416
    private _calendar: IgxCalendarComponent;
417
    private _positionSettings: PositionSettings;
418
    private _focusedInput: IgxDateRangeInputsBaseComponent;
419
    private _overlaySubFilter:
69✔
420
        [MonoTypeOperatorFunction<OverlayEventArgs>, MonoTypeOperatorFunction<OverlayEventArgs | OverlayCancelableEventArgs>] = [
421
            filter(x => x.id === this._overlayId),
104✔
422
            takeUntil(merge(this._destroy$, this.closed))
423
        ];
424
    private _dialogOverlaySettings: OverlaySettings = {
69✔
425
        closeOnOutsideClick: true,
426
        modal: true,
427
        closeOnEscape: true
428
    };
429
    private _dropDownOverlaySettings: OverlaySettings = {
69✔
430
        closeOnOutsideClick: true,
431
        modal: false,
432
        closeOnEscape: true
433
    };
434
    private onChangeCallback: (dateRange: DateRange) => void = noop;
69✔
435
    private onTouchCallback: () => void = noop;
69✔
436
    private onValidatorChange: () => void = noop;
69✔
437

438
    constructor(element: ElementRef,
439
        @Inject(LOCALE_ID) _localeId: string,
440
        protected platform: PlatformUtil,
69✔
441
        private _injector: Injector,
69✔
442
        private _cdr: ChangeDetectorRef,
69✔
443
        @Inject(IgxOverlayService) private _overlayService: IgxOverlayService,
69✔
444
        @Optional() @Inject(DisplayDensityToken) _displayDensityOptions?: IDisplayDensityOptions,
445
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType?: IgxInputGroupType) {
446
        super(element, _localeId, _displayDensityOptions, _inputGroupType);
69✔
447
        this.locale = this.locale || this._localeId;
69!
448
    }
449

450
    /** @hidden @internal */
451
    @HostListener('keydown', ['$event'])
452
    /** @hidden @internal */
453
    public onKeyDown(event: KeyboardEvent): void {
454
        switch (event.key) {
4!
455
            case this.platform.KEYMAP.ARROW_UP:
456
                if (event.altKey) {
×
457
                    this.close();
×
458
                }
459
                break;
×
460
            case this.platform.KEYMAP.ARROW_DOWN:
461
                if (event.altKey) {
4✔
462
                    this.open();
4✔
463
                }
464
                break;
4✔
465
        }
466
    }
467

468
    /**
469
     * Opens the date range picker's dropdown or dialog.
470
     *
471
     * @example
472
     * ```html
473
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
474
     *
475
     * <button type="button" igxButton (click)="dateRange.open()">Open Dialog</button
476
     * ```
477
     */
478
    public open(overlaySettings?: OverlaySettings): void {
479
        if (!this.collapsed || this.disabled) {
37✔
480
            return;
4✔
481
        }
482

483
        const settings = Object.assign({}, this.isDropdown
33✔
484
            ? this.dropdownOverlaySettings
485
            : this.dialogOverlaySettings
486
            , overlaySettings);
487

488
        this._overlayId = this._overlayService
33✔
489
            .attach(IgxCalendarContainerComponent, this.viewContainerRef, settings);
490
        this.subscribeToOverlayEvents();
33✔
491
        this._overlayService.show(this._overlayId);
33✔
492
    }
493

494
    /**
495
     * Closes the date range picker's dropdown or dialog.
496
     *
497
     * @example
498
     * ```html
499
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
500
     *
501
     * <button type="button" igxButton (click)="dateRange.close()">Close Dialog</button>
502
     * ```
503
     */
504
    public close(): void {
505
        if (!this.collapsed) {
46✔
506
            this._overlayService.hide(this._overlayId);
29✔
507
        }
508
    }
509

510
    /**
511
     * Toggles the date range picker's dropdown or dialog
512
     *
513
     * @example
514
     * ```html
515
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
516
     *
517
     * <button type="button" igxButton (click)="dateRange.toggle()">Toggle Dialog</button>
518
     * ```
519
     */
520
    public toggle(overlaySettings?: OverlaySettings): void {
521
        if (!this.collapsed) {
7✔
522
            this.close();
2✔
523
        } else {
524
            this.open(overlaySettings);
5✔
525
        }
526
    }
527

528
    /**
529
     * Selects a range of dates. If no `endDate` is passed, range is 1 day (only `startDate`)
530
     *
531
     * @example
532
     * ```typescript
533
     * public selectFiveDayRange() {
534
     *  const today = new Date();
535
     *  const inFiveDays = new Date(new Date().setDate(today.getDate() + 5));
536
     *  this.dateRange.select(today, inFiveDays);
537
     * }
538
     * ```
539
     */
540
    public select(startDate: Date, endDate?: Date): void {
541
        endDate = endDate ?? startDate;
19✔
542
        const dateRange = [startDate, endDate];
19✔
543
        this.handleSelection(dateRange);
19✔
544
    }
545

546
    /** @hidden @internal */
547
    public writeValue(value: DateRange): void {
548
        this.updateValue(value);
52✔
549
    }
550

551
    /** @hidden @internal */
552
    public registerOnChange(fn: any): void {
553
        this.onChangeCallback = fn;
36✔
554
    }
555

556
    /** @hidden @internal */
557
    public registerOnTouched(fn: any): void {
558
        this.onTouchCallback = fn;
35✔
559
    }
560

561
    /** @hidden @internal */
562
    public validate(control: AbstractControl): ValidationErrors | null {
563
        const value: DateRange = control.value;
181✔
564
        const errors = {};
181✔
565
        if (value) {
181✔
566
            if (this.hasProjectedInputs) {
41✔
567
                const startInput = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
38✔
568
                const endInput = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
76✔
569
                if (!startInput.dateTimeEditor.value) {
38!
570
                    Object.assign(errors, { startValue: true });
×
571
                }
572
                if (!endInput.dateTimeEditor.value) {
38✔
573
                    Object.assign(errors, { endValue: true });
1✔
574
                }
575
            }
576

577
            const min = parseDate(this.minValue);
41✔
578
            const max = parseDate(this.maxValue);
41✔
579
            const start = parseDate(value.start);
41✔
580
            const end = parseDate(value.end);
41✔
581
            if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false))
41✔
582
                || (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) {
583
                Object.assign(errors, { minValue: true });
2✔
584
            }
585
            if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false))
41✔
586
                || (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) {
587
                Object.assign(errors, { maxValue: true });
1✔
588
            }
589
        }
590

591
        return Object.keys(errors).length > 0 ? errors : null;
181✔
592
    }
593

594
    /** @hidden @internal */
595
    public registerOnValidatorChange?(fn: any): void {
596
        this.onValidatorChange = fn;
35✔
597
    }
598

599
    /** @hidden @internal */
600
    public setDisabledState?(isDisabled: boolean): void {
601
        this.disabled = isDisabled;
36✔
602
    }
603

604
    /** @hidden */
605
    public override ngOnInit(): void {
606
        this._ngControl = this._injector.get<NgControl>(NgControl, null);
66✔
607

608
        this.locale = this.locale || this._localeId;
66!
609
        super.ngOnInit();
66✔
610
    }
611

612
    /** @hidden */
613
    public override ngAfterViewInit(): void {
614
        super.ngAfterViewInit();
63✔
615
        this.subscribeToDateEditorEvents();
63✔
616
        this.configPositionStrategy();
63✔
617
        this.configOverlaySettings();
63✔
618
        this.cacheFocusedInput();
63✔
619
        this.attachOnTouched();
63✔
620

621
        this.setRequiredToInputs();
63✔
622

623
        if (this._ngControl) {
63✔
624
            this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this));
28✔
625
        }
626

627
        // delay invocations until the current change detection cycle has completed
628
        Promise.resolve().then(() => {
63✔
629
            this.updateDisabledState();
63✔
630
            this.initialSetValue();
63✔
631
            this.updateInputs();
63✔
632
            // B.P. 07 July 2021 - IgxDateRangePicker not showing initial disabled state with ChangeDetectionStrategy.OnPush #9776
633
            /**
634
             * if disabled is placed on the range picker element and there are projected inputs
635
             * run change detection since igxInput will initially set the projected inputs' disabled to false
636
             */
637
            if (this.hasProjectedInputs && this.disabled) {
63✔
638
                this._cdr.markForCheck();
3✔
639
            }
640
        });
641
        this.updateDisplayFormat();
63✔
642
        this.updateInputFormat();
63✔
643
    }
644

645
    /** @hidden @internal */
646
    public ngOnChanges(changes: SimpleChanges): void {
647
        if (changes['displayFormat'] && this.hasProjectedInputs) {
76✔
648
            this.updateDisplayFormat();
7✔
649
        }
650
        if (changes['inputFormat'] && this.hasProjectedInputs) {
76✔
651
            this.updateInputFormat();
4✔
652
        }
653
        if (changes['disabled']) {
76✔
654
            this.updateDisabledState();
57✔
655
        }
656
    }
657

658
    /** @hidden @internal */
659
    public override ngOnDestroy(): void {
660
        super.ngOnDestroy();
53✔
661
        if (this._statusChanges$) {
53✔
662
            this._statusChanges$.unsubscribe();
28✔
663
        }
664
        if (this._overlayId) {
53✔
665
            this._overlayService.detach(this._overlayId);
8✔
666
        }
667
    }
668

669
    /** @hidden @internal */
670
    public getEditElement() {
671
        return this.inputDirective.nativeElement;
×
672
    }
673

674
    protected onStatusChanged = () => {
69✔
675
        if (this.inputGroup) {
71✔
676
            this.setValidityState(this.inputDirective, this.inputGroup.isFocused);
7✔
677
        } else if (this.hasProjectedInputs) {
64✔
678
            this.projectedInputs
64✔
679
                .forEach((i) => {
680
                    this.setValidityState(i.inputDirective, i.isFocused);
128✔
681
                });
682
        }
683
        this.setRequiredToInputs();
71✔
684
    };
685

686
    private setValidityState(inputDirective: IgxInputDirective, isFocused: boolean) {
687
        if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) {
135✔
688
            if (this.hasValidators && isFocused) {
125✔
689
                inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
2✔
690
            } else {
691
                inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
123✔
692
            }
693
        } else {
694
            inputDirective.valid = IgxInputState.INITIAL;
10✔
695
        }
696
    }
697

698
    private get isTouchedOrDirty(): boolean {
699
        return (this._ngControl.control.touched || this._ngControl.control.dirty);
125✔
700
    }
701

702
    private get hasValidators(): boolean {
703
        return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
125✔
704
    }
705

706
    private handleSelection(selectionData: Date[]): void {
707
        let newValue = this.extractRange(selectionData);
37✔
708
        if (!newValue.start && !newValue.end) {
37!
709
            newValue = null;
×
710
        }
711
        this.value = newValue;
37✔
712
        if (this.isDropdown && selectionData?.length > 1) {
37✔
713
            this.close();
26✔
714
        }
715
    }
716

717
    private handleClosing(e: IBaseCancelableBrowserEventArgs): void {
718
        const args = { owner: this, cancel: e?.cancel, event: e?.event };
32✔
719
        this.closing.emit(args);
32✔
720
        e.cancel = args.cancel;
32✔
721
        if (args.cancel) {
32✔
722
            return;
2✔
723
        }
724

725
        if (this.isDropdown && e?.event && !this.element.nativeElement.contains(e.event.target)) {
30✔
726
            // outside click
727
            this.updateValidityOnBlur();
3✔
728
        } else {
729
            this.onTouchCallback();
27✔
730
            // input click
731
            if (this.hasProjectedInputs && this._focusedInput) {
27✔
732
                this._focusedInput.setFocus();
2✔
733
                this._focusedInput = null;
2✔
734
            }
735
            if (this.inputDirective) {
27✔
736
                this.inputDirective.focus();
14✔
737
            }
738
        }
739
    }
740

741
    private subscribeToOverlayEvents() {
742
        this._overlayService.opening.pipe(...this._overlaySubFilter).subscribe((e) => {
33✔
743
            const overlayEvent = e as OverlayCancelableEventArgs;
33✔
744
            const args = { owner: this, cancel: overlayEvent?.cancel, event: e.event };
33✔
745
            this.opening.emit(args);
33✔
746
            if (args.cancel) {
33!
747
                this._overlayService.detach(this._overlayId);
×
748
                overlayEvent.cancel = true;
×
749
                return;
×
750
            }
751

752
            this._initializeCalendarContainer(e.componentRef.instance);
33✔
753
            this._collapsed = false;
33✔
754
            this.updateCalendar();
33✔
755
        });
756

757
        this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => {
33✔
758
            this.opened.emit({ owner: this });
25✔
759
        });
760

761
        this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e) => {
33✔
762
            this.handleClosing(e as OverlayCancelableEventArgs);
32✔
763
        });
764

765
        this._overlayService.closed.pipe(...this._overlaySubFilter).subscribe(() => {
33✔
766
            this._overlayService.detach(this._overlayId);
14✔
767
            this._collapsed = true;
14✔
768
            this._overlayId = null;
14✔
769
            this.closed.emit({ owner: this });
14✔
770
        });
771
    }
772

773
    private updateValue(value: DateRange) {
774
        this._value = value ? value : null;
122✔
775
        this.updateInputs();
122✔
776
        this.updateCalendar();
122✔
777
    }
778

779
    private updateValidityOnBlur() {
780
        this.onTouchCallback();
5✔
781
        if (this._ngControl) {
5✔
782
            if (this.hasProjectedInputs) {
3✔
783
                this.projectedInputs.forEach(i => {
3✔
784
                    if (!this._ngControl.valid) {
6!
785
                        i.updateInputValidity(IgxInputState.INVALID);
6✔
786
                    } else {
787
                        i.updateInputValidity(IgxInputState.INITIAL);
×
788
                    }
789
                });
790
            }
791

792
            if (this.inputDirective) {
3!
793
                if (!this._ngControl.valid) {
×
794
                    this.inputDirective.valid = IgxInputState.INVALID;
×
795
                } else {
796
                    this.inputDirective.valid = IgxInputState.INITIAL;
×
797
                }
798
            }
799
        }
800
    }
801

802
    private updateDisabledState() {
803
        if (this.hasProjectedInputs) {
120✔
804
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
32✔
805
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
64✔
806
            start.inputDirective.disabled = this.disabled;
32✔
807
            end.inputDirective.disabled = this.disabled;
32✔
808
            return;
32✔
809
        }
810
    }
811

812
    private setRequiredToInputs(): void {
813
        // workaround for igxInput setting required
814
        Promise.resolve().then(() => {
134✔
815
            const isRequired = this.required;
134✔
816
            if (this.inputGroup && this.inputGroup.isRequired !== isRequired) {
134✔
817
                this.inputGroup.isRequired = isRequired;
4✔
818
            } else if (this.hasProjectedInputs && this._ngControl) {
130✔
819
                this.projectedInputs.forEach(i => i.isRequired = isRequired);
178✔
820
            }
821
        });
822
    }
823

824
    private parseMinValue(value: string | Date): Date | null {
825
        let minValue: Date = parseDate(value);
56✔
826
        if (!minValue && this.hasProjectedInputs) {
56✔
827
            const start = this.projectedInputs.filter(i => i instanceof IgxDateRangeStartComponent)[0];
50✔
828
            if (start) {
25✔
829
                minValue = parseDate(start.dateTimeEditor.minValue);
25✔
830
            }
831
        }
832

833
        return minValue;
56✔
834
    }
835

836
    private parseMaxValue(value: string | Date): Date | null {
837
        let maxValue: Date = parseDate(value);
56✔
838
        if (!maxValue && this.projectedInputs) {
56✔
839
            const end = this.projectedInputs.filter(i => i instanceof IgxDateRangeEndComponent)[0];
53✔
840
            if (end) {
53✔
841
                maxValue = parseDate(end.dateTimeEditor.maxValue);
25✔
842
            }
843
        }
844

845
        return maxValue;
56✔
846
    }
847

848
    private updateCalendar(): void {
849
        if (!this.calendar) {
157✔
850
            return;
101✔
851
        }
852
        this.calendar.disabledDates = [];
56✔
853
        const minValue = this.parseMinValue(this.minValue);
56✔
854
        if (minValue) {
56✔
855
            this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] });
3✔
856
        }
857
        const maxValue = this.parseMaxValue(this.maxValue);
56✔
858
        if (maxValue) {
56✔
859
            this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] });
3✔
860
        }
861

862
        const range: Date[] = [];
56✔
863
        if (this.value?.start && this.value?.end) {
56✔
864
            const _value = this.toRangeOfDates(this.value);
22✔
865
            if (DateTimeUtil.greaterThanMaxValue(_value.start, _value.end)) {
22!
866
                this.swapEditorDates();
×
867
            }
868
            if (this.valueInRange(this.value, minValue, maxValue)) {
22✔
869
                range.push(_value.start, _value.end);
22✔
870
            }
871
        }
872

873
        if (range.length > 0) {
56✔
874
            this.calendar.selectDate(range);
22✔
875
        } else if (range.length === 0 && this.calendar.monthViews) {
34!
876
            this.calendar.deselectDate();
×
877
        }
878
        this.calendar.viewDate = range[0] || new Date();
56✔
879
    }
880

881
    private swapEditorDates(): void {
882
        if (this.hasProjectedInputs) {
×
883
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
884
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
885
            [start.dateTimeEditor.value, end.dateTimeEditor.value] = [end.dateTimeEditor.value, start.dateTimeEditor.value];
×
886
            [this.value.start, this.value.end] = [this.value.end, this.value.start];
×
887
        }
888
    }
889

890
    private valueInRange(value: DateRange, minValue?: Date, maxValue?: Date): boolean {
891
        const _value = this.toRangeOfDates(value);
22✔
892
        if (minValue && DateTimeUtil.lessThanMinValue(_value.start, minValue, false)) {
22!
893
            return false;
×
894
        }
895
        if (maxValue && DateTimeUtil.greaterThanMaxValue(_value.end, maxValue, false)) {
22!
896
            return false;
×
897
        }
898

899
        return true;
22✔
900
    }
901

902
    private extractRange(selection: Date[]): DateRange {
903
        return {
37✔
904
            start: selection[0] || null,
37!
905
            end: selection.length > 0 ? selection[selection.length - 1] : null
37!
906
        };
907
    }
908

909
    private toRangeOfDates(range: DateRange): { start: Date; end: Date } {
910
        let start;
911
        let end;
912
        if (!isDate(range.start)) {
84✔
913
            start = DateTimeUtil.parseIsoDate(range.start);
1✔
914
        }
915
        if (!isDate(range.end)) {
84✔
916
            end = DateTimeUtil.parseIsoDate(range.end);
2✔
917
        }
918

919
        if (start || end) {
84!
920
            return { start, end };
×
921
        }
922

923
        return { start: range.start as Date, end: range.end as Date };
84✔
924
    }
925

926
    private subscribeToDateEditorEvents(): void {
927
        if (this.hasProjectedInputs) {
63✔
928
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
27✔
929
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
54✔
930
            if (start && end) {
27✔
931
                start.dateTimeEditor.valueChange
27✔
932
                    .pipe(takeUntil(this._destroy$))
933
                    .subscribe(value => {
934
                        if (this.value) {
1!
935
                            this.value = { start: value, end: this.value.end };
×
936
                        } else {
937
                            this.value = { start: value, end: null };
1✔
938
                        }
939
                    });
940
                end.dateTimeEditor.valueChange
27✔
941
                    .pipe(takeUntil(this._destroy$))
942
                    .subscribe(value => {
943
                        if (this.value) {
1!
944
                            this.value = { start: this.value.start, end: value as Date };
1✔
945
                        } else {
946
                            this.value = { start: null, end: value as Date };
×
947
                        }
948
                    });
949
            }
950
        }
951
    }
952

953
    private attachOnTouched(): void {
954
        if (this.hasProjectedInputs) {
63✔
955
            this.projectedInputs.forEach(i => {
27✔
956
                fromEvent(i.dateTimeEditor.nativeElement, 'blur')
54✔
957
                    .pipe(takeUntil(this._destroy$))
958
                    .subscribe(() => {
959
                        if (this.collapsed) {
1✔
960
                            this.updateValidityOnBlur();
1✔
961
                        }
962
                    });
963
            });
964
        } else {
965
            fromEvent(this.inputDirective.nativeElement, 'blur')
36✔
966
                .pipe(takeUntil(this._destroy$))
967
                .subscribe(() => {
968
                    if (this.collapsed) {
10!
UNCOV
969
                        this.updateValidityOnBlur();
×
970
                    }
971
                });
972
        }
973
    }
974

975
    private cacheFocusedInput(): void {
976
        if (this.hasProjectedInputs) {
63✔
977
            this.projectedInputs.forEach(i => {
27✔
978
                fromEvent(i.dateTimeEditor.nativeElement, 'focus')
54✔
979
                    .pipe(takeUntil(this._destroy$))
980
                    .subscribe(() => this._focusedInput = i);
4✔
981
            });
982
        }
983
    }
984

985
    private configPositionStrategy(): void {
986
        this._positionSettings = {
63✔
987
            openAnimation: fadeIn,
988
            closeAnimation: fadeOut
989
        };
990
        this._dropDownOverlaySettings.positionStrategy = new AutoPositionStrategy(this._positionSettings);
63✔
991
        this._dropDownOverlaySettings.target = this.element.nativeElement;
63✔
992
    }
993

994
    private configOverlaySettings(): void {
995
        if (this.overlaySettings !== null) {
63✔
996
            this._dropDownOverlaySettings = Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
63✔
997
            this._dialogOverlaySettings = Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
63✔
998
        }
999
    }
1000

1001
    private initialSetValue() {
1002
        // if there is no value and no ngControl on the picker but we have inputs we may have value set through
1003
        // their ngModels - we should generate our initial control value
1004
        if ((!this.value || (!this.value.start && !this.value.end)) && this.hasProjectedInputs && !this._ngControl) {
63!
1005
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent);
2✔
1006
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent);
4✔
1007
            this._value = {
2✔
1008
                start: start.dateTimeEditor.value as Date,
1009
                end: end.dateTimeEditor.value as Date
1010
            };
1011
        }
1012
    }
1013

1014
    private updateInputs(): void {
1015
        const start = this.projectedInputs?.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
185✔
1016
        const end = this.projectedInputs?.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
185✔
1017
        if (start && end) {
185✔
1018
            const _value = this.value ? this.toRangeOfDates(this.value) : null;
87✔
1019
            start.updateInputValue(_value?.start || null);
87✔
1020
            end.updateInputValue(_value?.end || null);
87✔
1021
        }
1022
    }
1023

1024
    private updateDisplayFormat(): void {
1025
        this.projectedInputs.forEach(i => {
70✔
1026
            const input = i as IgxDateRangeInputsBaseComponent;
68✔
1027
            input.dateTimeEditor.displayFormat = this.displayFormat;
68✔
1028
        });
1029
    }
1030

1031
    private updateInputFormat(): void {
1032
        this.projectedInputs.forEach(i => {
67✔
1033
            const input = i as IgxDateRangeInputsBaseComponent;
62✔
1034
            if (input.dateTimeEditor.inputFormat !== this.inputFormat) {
62✔
1035
                input.dateTimeEditor.inputFormat = this.inputFormat;
62✔
1036
            }
1037
        });
1038
    }
1039

1040
    private _initializeCalendarContainer(componentInstance: IgxCalendarContainerComponent) {
1041
        this._calendar = componentInstance.calendar;
33✔
1042
        this.calendar.hasHeader = false;
33✔
1043
        this.calendar.locale = this.locale;
33✔
1044
        this.calendar.selection = CalendarSelection.RANGE;
33✔
1045
        this.calendar.weekStart = this.weekStart;
33✔
1046
        this.calendar.hideOutsideDays = this.hideOutsideDays;
33✔
1047
        this.calendar.monthsViewNumber = this.displayMonthsCount;
33✔
1048
        this.calendar.showWeekNumbers = this.showWeekNumbers;
33✔
1049
        this.calendar.selected.pipe(takeUntil(this._destroy$)).subscribe((ev: Date[]) => this.handleSelection(ev));
33✔
1050

1051
        componentInstance.mode = this.mode;
33✔
1052
        componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null;
33✔
1053
        componentInstance.pickerActions = this.pickerActions;
33✔
1054
        componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close());
33✔
1055
    }
1056
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc