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

IgniteUI / igniteui-angular / 17121302453

21 Aug 2025 08:20AM UTC coverage: 91.578% (+0.06%) from 91.52%
17121302453

push

github

web-flow
chore(schematics): use node version that works for ts-node (#16147)

13577 of 15915 branches covered (85.31%)

27403 of 29923 relevant lines covered (91.58%)

34514.28 hits per line

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

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

39
const SingleInputDatesConcatenationString = ' - ';
3✔
40

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

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

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

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

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

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

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

178
    /**
179
     * The expected user input format and placeholder.
180
     *
181
     * @example
182
     * ```html
183
     * <igx-date-range-picker inputFormat="dd/MM/yy"></igx-date-range-picker>
184
     * ```
185
     */
186
    @Input()
187
    public override inputFormat: string;
188

189
    /**
190
     * The minimum value in a valid range.
191
     *
192
     * @example
193
     * <igx-date-range-picker [minValue]="minDate"></igx-date-range-picker>
194
     */
195
    @Input()
196
    public set minValue(value: Date | string) {
197
        this._minValue = value;
27✔
198
        this.onValidatorChange();
27✔
199
    }
200

201
    public get minValue(): Date | string {
202
        return this._minValue;
134✔
203
    }
204

205
    /**
206
     * The maximum value in a valid range.
207
     *
208
     * @example
209
     * <igx-date-range-picker [maxValue]="maxDate"></igx-date-range-picker>
210
     */
211
    @Input()
212
    public set maxValue(value: Date | string) {
213
        this._maxValue = value;
27✔
214
        this.onValidatorChange();
27✔
215
    }
216

217
    public get maxValue(): Date | string {
218
        return this._maxValue;
134✔
219
    }
220

221
    /**
222
     * An accessor that sets the resource strings.
223
     * By default it uses EN resources.
224
     */
225
    @Input()
226
    public set resourceStrings(value: IDateRangePickerResourceStrings) {
227
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
1✔
228
    }
229

230
    /**
231
     * An accessor that returns the resource strings.
232
     */
233
    public get resourceStrings(): IDateRangePickerResourceStrings {
234
        return this._resourceStrings;
86✔
235
    }
236

237
    /**
238
     * Sets the `placeholder` for single-input `IgxDateRangePickerComponent`.
239
     *
240
     *   @example
241
     * ```html
242
     * <igx-date-range-picker [placeholder]="'Choose your dates'"></igx-date-range-picker>
243
     * ```
244
     */
245
    @Input()
246
    public override placeholder = '';
85✔
247

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

264
    /**
265
     * Show/hide week numbers
266
     *
267
     * @remarks
268
     * Default is `false`.
269
     *
270
     * @example
271
     * ```html
272
     * <igx-date-range-picker [showWeekNumbers]="true"></igx-date-range-picker>
273
     * ``
274
     */
275
    @Input({ transform: booleanAttribute })
276
    public showWeekNumbers = false;
85✔
277

278
    /**
279
     * Emitted when the picker's value changes. Used for two-way binding.
280
     *
281
     * @example
282
     * ```html
283
     * <igx-date-range-picker [(value)]="date"></igx-date-range-picker>
284
     * ```
285
     */
286

287
     /**
288
      * Whether to render built-in predefined ranges.
289
      *
290
      * @example
291
      * ```html
292
      * <igx-date-range-picker [(usePredefinedRanges)]="true"></igx-date-range-picker>
293
      * ``
294
      *  */
295
    @Input() public usePredefinedRanges = false;
85✔
296

297
    /**
298
     *  Custom ranges rendered as chips.
299
     *
300
     * @example
301
     * ```html
302
     * <igx-date-range-picker [(usePredefinedRanges)]="true"></igx-date-range-picker>
303
     * ``
304
    */
305
    @Input() public customRanges: CustomDateRange[] = [];
85✔
306

307
    @Output()
308
    public valueChange = new EventEmitter<DateRange>();
85✔
309

310
    /** @hidden @internal */
311
    @HostBinding('class.igx-date-range-picker')
312
    public cssClass = 'igx-date-range-picker';
85✔
313

314
    @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef })
315
    private viewContainerRef: ViewContainerRef;
316

317
    /** @hidden @internal */
318
    @ViewChild(IgxInputDirective)
319
    public inputDirective: IgxInputDirective;
320

321
    /** @hidden @internal */
322
    @ContentChildren(IgxDateRangeInputsBaseComponent)
323
    public projectedInputs: QueryList<IgxDateRangeInputsBaseComponent>;
324

325
    @ContentChild(IgxLabelDirective)
326
    public label: IgxLabelDirective;
327

328
    @ContentChild(IgxPickerActionsDirective)
329
    public pickerActions: IgxPickerActionsDirective;
330

331
    /** @hidden @internal */
332
    @ContentChild(IgxDateRangeSeparatorDirective, { read: TemplateRef })
333
    public dateSeparatorTemplate: TemplateRef<any>;
334

335
    /** @hidden @internal */
336
    public get dateSeparator(): string {
337
        if (this._dateSeparator === null) {
40✔
338
            return this.resourceStrings.igx_date_range_picker_date_separator;
40✔
339
        }
340
        return this._dateSeparator;
×
341
    }
342

343
    /** @hidden @internal */
344
    public get appliedFormat(): string {
345
        return DateTimeUtil.getLocaleDateFormat(this.locale, this.displayFormat)
506✔
346
            || DateTimeUtil.DEFAULT_INPUT_FORMAT;
347
    }
348

349
    /**
350
     * @example
351
     * ```html
352
     * <igx-date-range-picker locale="jp"></igx-date-range-picker>
353
     * ```
354
     */
355
    /**
356
     * Gets the `locale` of the date-range-picker.
357
     * If not set, defaults to application's locale.
358
     */
359
    @Input()
360
    public override get locale(): string {
361
        return this._locale;
1,080✔
362
    }
363

364
    /**
365
     * Sets the `locale` of the date-picker.
366
     * Expects a valid BCP 47 language tag.
367
     */
368
    public override set locale(value: string) {
369
        this._locale = value;
259✔
370
        // if value is invalid, set it back to _localeId
371
        try {
259✔
372
            getLocaleFirstDayOfWeek(this._locale);
259✔
373
        } catch (e) {
374
            this._locale = this._localeId;
1✔
375
        }
376
        if (this.hasProjectedInputs) {
259✔
377
            this.updateInputLocale();
3✔
378
            this.updateDisplayFormat();
3✔
379
        }
380
    }
381

382
    /** @hidden @internal */
383
    public get singleInputFormat(): string {
384
        if (this.placeholder !== '') {
236✔
385
            return this.placeholder;
2✔
386
        }
387

388
        const format = this.appliedFormat;
234✔
389
        return `${format}${SingleInputDatesConcatenationString}${format}`;
234✔
390
    }
391

392
    /**
393
     * Gets calendar state.
394
     *
395
     * ```typescript
396
     * let state = this.dateRange.collapsed;
397
     * ```
398
     */
399
    public override get collapsed(): boolean {
400
        return this._collapsed;
442✔
401
    }
402

403
    /**
404
     * The currently selected value / range from the calendar
405
     *
406
     * @remarks
407
     * The current value is of type `DateRange`
408
     *
409
     * @example
410
     * ```typescript
411
     * const newValue: DateRange = { start: new Date("2/2/2012"), end: new Date("3/3/2013")};
412
     * this.dateRangePicker.value = newValue;
413
     * ```
414
     */
415
    public get value(): DateRange | null {
416
        return this._value;
1,020✔
417
    }
418

419
    @Input()
420
    public set value(value: DateRange | null) {
421
        this.updateValue(value);
86✔
422
        this.onChangeCallback(value);
86✔
423
        this.valueChange.emit(value);
86✔
424
    }
425

426
    /** @hidden @internal */
427
    public get hasProjectedInputs(): boolean {
428
        return this.projectedInputs?.length > 0;
2,018✔
429
    }
430

431
    /** @hidden @internal */
432
    public get separatorClass(): string {
433
        return 'igx-date-range-picker__label';
368✔
434
    }
435

436
    protected override get toggleContainer(): HTMLElement | undefined {
437
        return this._calendarContainer;
7✔
438
    }
439

440
    private get required(): boolean {
441
        if (this._ngControl && this._ngControl.control && this._ngControl.control.validator) {
177✔
442
            const error = this._ngControl.control.validator({} as AbstractControl);
140✔
443
            return (error && error.required) ? true : false;
140!
444
        }
445

446
        return false;
37✔
447
    }
448

449
    private get calendar(): IgxCalendarComponent {
450
        return this._calendar;
795✔
451
    }
452

453
    private get dropdownOverlaySettings(): OverlaySettings {
454
        return Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
35✔
455
    }
456

457
    private get dialogOverlaySettings(): OverlaySettings {
458
        return Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
6✔
459
    }
460

461
    private _resourceStrings = getCurrentResourceStrings(DateRangePickerResourceStringsEN);
85✔
462
    private _doneButtonText = null;
85✔
463
    private _dateSeparator = null;
85✔
464
    private _value: DateRange | null;
465
    private _overlayId: string;
466
    private _ngControl: NgControl;
467
    private _statusChanges$: Subscription;
468
    private _calendar: IgxCalendarComponent;
469
    private _calendarContainer?: HTMLElement;
470
    private _positionSettings: PositionSettings;
471
    private _focusedInput: IgxDateRangeInputsBaseComponent;
472
    private _overlaySubFilter:
85✔
473
        [MonoTypeOperatorFunction<OverlayEventArgs>, MonoTypeOperatorFunction<OverlayEventArgs | OverlayCancelableEventArgs>] = [
474
            filter(x => x.id === this._overlayId),
131✔
475
            takeUntil(merge(this._destroy$, this.closed))
476
        ];
477
    private _dialogOverlaySettings: OverlaySettings = {
85✔
478
        closeOnOutsideClick: true,
479
        modal: true,
480
        closeOnEscape: true
481
    };
482
    private _dropDownOverlaySettings: OverlaySettings = {
85✔
483
        closeOnOutsideClick: true,
484
        modal: false,
485
        closeOnEscape: true
486
    };
487
    private onChangeCallback: (dateRange: DateRange) => void = noop;
85✔
488
    private onTouchCallback: () => void = noop;
85✔
489
    private onValidatorChange: () => void = noop;
85✔
490

491
    constructor(element: ElementRef,
492
        @Inject(LOCALE_ID) _localeId: string,
493
        protected platform: PlatformUtil,
85✔
494
        private _injector: Injector,
85✔
495
        private _cdr: ChangeDetectorRef,
85✔
496
        @Inject(IgxOverlayService) private _overlayService: IgxOverlayService,
85✔
497
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType?: IgxInputGroupType) {
498
        super(element, _localeId, _inputGroupType);
85✔
499
        this.locale = this.locale || this._localeId;
85!
500
    }
501

502
    /** @hidden @internal */
503
    @HostListener('keydown', ['$event'])
504
    /** @hidden @internal */
505
    public onKeyDown(event: KeyboardEvent): void {
506
        switch (event.key) {
5!
507
            case this.platform.KEYMAP.ARROW_UP:
508
                if (event.altKey) {
×
509
                    this.close();
×
510
                }
511
                break;
×
512
            case this.platform.KEYMAP.ARROW_DOWN:
513
                if (event.altKey) {
5✔
514
                    this.open();
5✔
515
                }
516
                break;
5✔
517
        }
518
    }
519

520
    /**
521
     * Opens the date range picker's dropdown or dialog.
522
     *
523
     * @example
524
     * ```html
525
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
526
     *
527
     * <button type="button" igxButton (click)="dateRange.open()">Open Dialog</button
528
     * ```
529
     */
530
    public open(overlaySettings?: OverlaySettings): void {
531
        if (!this.collapsed || this.disabled) {
45✔
532
            return;
4✔
533
        }
534

535
        const settings = Object.assign({}, this.isDropdown
41✔
536
            ? this.dropdownOverlaySettings
537
            : this.dialogOverlaySettings
538
            , overlaySettings);
539

540
        this._overlayId = this._overlayService
41✔
541
            .attach(IgxCalendarContainerComponent, this.viewContainerRef, settings);
542
        this.subscribeToOverlayEvents();
41✔
543
        this._overlayService.show(this._overlayId);
41✔
544
    }
545

546
    /**
547
     * Closes the date range picker's dropdown or dialog.
548
     *
549
     * @example
550
     * ```html
551
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
552
     *
553
     * <button type="button" igxButton (click)="dateRange.close()">Close Dialog</button>
554
     * ```
555
     */
556
    public close(): void {
557
        if (!this.collapsed) {
60✔
558
            this._overlayService.hide(this._overlayId);
43✔
559
        }
560
    }
561

562
    /**
563
     * Toggles the date range picker's dropdown or dialog
564
     *
565
     * @example
566
     * ```html
567
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
568
     *
569
     * <button type="button" igxButton (click)="dateRange.toggle()">Toggle Dialog</button>
570
     * ```
571
     */
572
    public toggle(overlaySettings?: OverlaySettings): void {
573
        if (!this.collapsed) {
7✔
574
            this.close();
2✔
575
        } else {
576
            this.open(overlaySettings);
5✔
577
        }
578
    }
579

580
    /**
581
     * Selects a range of dates. If no `endDate` is passed, range is 1 day (only `startDate`)
582
     *
583
     * @example
584
     * ```typescript
585
     * public selectFiveDayRange() {
586
     *  const today = new Date();
587
     *  const inFiveDays = new Date(new Date().setDate(today.getDate() + 5));
588
     *  this.dateRange.select(today, inFiveDays);
589
     * }
590
     * ```
591
     */
592
    public select(startDate: Date, endDate?: Date): void {
593
        endDate = endDate ?? startDate;
25✔
594
        const dateRange = [startDate, endDate];
25✔
595
        this.handleSelection(dateRange);
25✔
596
    }
597

598
    /** @hidden @internal */
599
    public writeValue(value: DateRange): void {
600
        this.updateValue(value);
84✔
601
    }
602

603
    /** @hidden @internal */
604
    public registerOnChange(fn: any): void {
605
        this.onChangeCallback = fn;
52✔
606
    }
607

608
    /** @hidden @internal */
609
    public registerOnTouched(fn: any): void {
610
        this.onTouchCallback = fn;
51✔
611
    }
612

613
    /** @hidden @internal */
614
    public validate(control: AbstractControl): ValidationErrors | null {
615
        const value: DateRange = control.value;
271✔
616
        const errors = {};
271✔
617
        if (value) {
271✔
618
            if (this.hasProjectedInputs) {
57✔
619
                const startInput = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
54✔
620
                const endInput = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
108✔
621
                if (!startInput.dateTimeEditor.value) {
54!
622
                    Object.assign(errors, { startValue: true });
×
623
                }
624
                if (!endInput.dateTimeEditor.value) {
54✔
625
                    Object.assign(errors, { endValue: true });
1✔
626
                }
627
            }
628

629
            const min = parseDate(this.minValue);
57✔
630
            const max = parseDate(this.maxValue);
57✔
631
            const start = parseDate(value.start);
57✔
632
            const end = parseDate(value.end);
57✔
633
            if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false))
57✔
634
                || (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) {
635
                Object.assign(errors, { minValue: true });
2✔
636
            }
637
            if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false))
57✔
638
                || (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) {
639
                Object.assign(errors, { maxValue: true });
1✔
640
            }
641
        }
642

643
        return Object.keys(errors).length > 0 ? errors : null;
271✔
644
    }
645

646
    /** @hidden @internal */
647
    public registerOnValidatorChange?(fn: any): void {
648
        this.onValidatorChange = fn;
51✔
649
    }
650

651
    /** @hidden @internal */
652
    public setDisabledState?(isDisabled: boolean): void {
653
        this.disabled = isDisabled;
52✔
654
    }
655

656
    /** @hidden */
657
    public ngOnInit(): void {
658
        this._ngControl = this._injector.get<NgControl>(NgControl, null);
82✔
659

660
        this.locale = this.locale || this._localeId;
82!
661
    }
662

663
    /** @hidden */
664
    public override ngAfterViewInit(): void {
665
        super.ngAfterViewInit();
79✔
666
        this.subscribeToDateEditorEvents();
79✔
667
        this.configPositionStrategy();
79✔
668
        this.configOverlaySettings();
79✔
669
        this.cacheFocusedInput();
79✔
670
        this.attachOnTouched();
79✔
671

672
        this.setRequiredToInputs();
79✔
673

674
        if (this._ngControl) {
79✔
675
            this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this));
44✔
676
        }
677

678
        // delay invocations until the current change detection cycle has completed
679
        Promise.resolve().then(() => {
79✔
680
            this.updateDisabledState();
79✔
681
            this.initialSetValue();
79✔
682
            this.updateInputs();
79✔
683
            // B.P. 07 July 2021 - IgxDateRangePicker not showing initial disabled state with ChangeDetectionStrategy.OnPush #9776
684
            /**
685
             * if disabled is placed on the range picker element and there are projected inputs
686
             * run change detection since igxInput will initially set the projected inputs' disabled to false
687
             */
688
            if (this.hasProjectedInputs && this.disabled) {
79✔
689
                this._cdr.markForCheck();
3✔
690
            }
691
        });
692
        this.updateDisplayFormat();
79✔
693
        this.updateInputFormat();
79✔
694
    }
695

696
    /** @hidden @internal */
697
    public ngOnChanges(changes: SimpleChanges): void {
698
        if (changes['displayFormat'] && this.hasProjectedInputs) {
96✔
699
            this.updateDisplayFormat();
11✔
700
        }
701
        if (changes['inputFormat'] && this.hasProjectedInputs) {
96✔
702
            this.updateInputFormat();
4✔
703
        }
704
        if (changes['disabled']) {
96✔
705
            this.updateDisabledState();
73✔
706
        }
707
    }
708

709
    /** @hidden @internal */
710
    public override ngOnDestroy(): void {
711
        super.ngOnDestroy();
69✔
712
        if (this._statusChanges$) {
69✔
713
            this._statusChanges$.unsubscribe();
44✔
714
        }
715
        if (this._overlayId) {
69✔
716
            this._overlayService.detach(this._overlayId);
15✔
717
        }
718
    }
719

720
    /** @hidden @internal */
721
    public getEditElement() {
722
        return this.inputDirective.nativeElement;
×
723
    }
724

725
    protected onStatusChanged = () => {
85✔
726
        if (this.inputGroup) {
97✔
727
            this.setValidityState(this.inputDirective, this.inputGroup.isFocused);
7✔
728
        } else if (this.hasProjectedInputs) {
90✔
729
            this.projectedInputs
90✔
730
                .forEach((i) => {
731
                    this.setValidityState(i.inputDirective, i.isFocused);
180✔
732
                });
733
        }
734
        this.setRequiredToInputs();
97✔
735
    };
736

737
    private setValidityState(inputDirective: IgxInputDirective, isFocused: boolean) {
738
        if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) {
187✔
739
            if (this.hasValidators && isFocused) {
177✔
740
                inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
2✔
741
            } else {
742
                inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
175✔
743
            }
744
        } else {
745
            inputDirective.valid = IgxInputState.INITIAL;
10✔
746
        }
747
    }
748

749
    private get isTouchedOrDirty(): boolean {
750
        return (this._ngControl.control.touched || this._ngControl.control.dirty);
177✔
751
    }
752

753
    private get hasValidators(): boolean {
754
        return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
177✔
755
    }
756

757
    private handleSelection(selectionData: Date[]): void {
758
        let newValue = this.extractRange(selectionData);
43✔
759
        if (!newValue.start && !newValue.end) {
43!
760
            newValue = null;
×
761
        }
762
        this.value = newValue;
43✔
763
        if (this.isDropdown && selectionData?.length > 1) {
43✔
764
            this.close();
32✔
765
        }
766
    }
767

768
    private handleClosing(e: IBaseCancelableBrowserEventArgs): void {
769
        const args = { owner: this, cancel: e?.cancel, event: e?.event };
47✔
770
        this.closing.emit(args);
47✔
771
        e.cancel = args.cancel;
47✔
772
        if (args.cancel) {
47✔
773
            return;
3✔
774
        }
775

776
        if (this.isDropdown && e?.event && !this.isFocused) {
44!
777
            // outside click
778
            this.updateValidityOnBlur();
×
779
        } else {
780
            this.onTouchCallback();
44✔
781
            // input click
782
            if (this.hasProjectedInputs && this._focusedInput) {
44✔
783
                this._focusedInput.setFocus();
7✔
784
            }
785
            if (this.inputDirective) {
44✔
786
                this.inputDirective.focus();
15✔
787
            }
788
        }
789
    }
790

791
    private subscribeToOverlayEvents() {
792
        this._overlayService.opening.pipe(...this._overlaySubFilter).subscribe((e) => {
41✔
793
            const overlayEvent = e as OverlayCancelableEventArgs;
41✔
794
            const args = { owner: this, cancel: overlayEvent?.cancel, event: e.event };
41✔
795
            this.opening.emit(args);
41✔
796
            if (args.cancel) {
41!
797
                this._overlayService.detach(this._overlayId);
×
798
                overlayEvent.cancel = true;
×
799
                return;
×
800
            }
801

802
            this._initializeCalendarContainer(e.componentRef.instance);
41✔
803
            this._calendarContainer = e.componentRef.location.nativeElement;
41✔
804
            this._collapsed = false;
41✔
805
            this.updateCalendar();
41✔
806
        });
807

808
        this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => {
41✔
809
            this.calendar.wrapper.nativeElement.focus();
27✔
810
            this.opened.emit({ owner: this });
27✔
811
        });
812

813
        this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e: OverlayCancelableEventArgs) => {
41✔
814
            const isEscape = e.event && (e.event as KeyboardEvent).key === this.platform.KEYMAP.ESCAPE;
47✔
815
            if (this.isProjectedInputTarget(e.event) && !isEscape) {
47✔
816
                e.cancel = true;
1✔
817
            }
818
            this.handleClosing(e as OverlayCancelableEventArgs);
47✔
819
        });
820

821
        this._overlayService.closed.pipe(...this._overlaySubFilter).subscribe(() => {
41✔
822
            this._overlayService.detach(this._overlayId);
16✔
823
            this._collapsed = true;
16✔
824
            this._overlayId = null;
16✔
825
            this._calendar = null;
16✔
826
            this._calendarContainer = undefined;
16✔
827
            this.closed.emit({ owner: this });
16✔
828
        });
829
    }
830

831
    private isProjectedInputTarget(event: Event): boolean {
832
        if (!this.hasProjectedInputs || !event) {
47✔
833
            return false;
44✔
834
        }
835
        const path = event.composed ? event.composedPath() : [event.target];
3!
836
        return this.projectedInputs.some(i =>
3✔
837
            path.includes(i.dateTimeEditor.nativeElement)
5✔
838
        );
839
    }
840

841
    private updateValue(value: DateRange) {
842
        this._value = value ? value : null;
170✔
843
        this.updateInputs();
170✔
844
        this.updateCalendar();
170✔
845
    }
846

847
    private updateValidityOnBlur() {
848
        this._focusedInput = null;
2✔
849
        this.onTouchCallback();
2✔
850
        if (this._ngControl) {
2✔
851
            if (this.hasProjectedInputs) {
1✔
852
                this.projectedInputs.forEach(i => {
1✔
853
                    if (!this._ngControl.valid) {
2!
854
                        i.updateInputValidity(IgxInputState.INVALID);
2✔
855
                    } else {
856
                        i.updateInputValidity(IgxInputState.INITIAL);
×
857
                    }
858
                });
859
            }
860

861
            if (this.inputDirective) {
1!
862
                if (!this._ngControl.valid) {
×
863
                    this.inputDirective.valid = IgxInputState.INVALID;
×
864
                } else {
865
                    this.inputDirective.valid = IgxInputState.INITIAL;
×
866
                }
867
            }
868
        }
869
    }
870

871
    private updateDisabledState() {
872
        if (this.hasProjectedInputs) {
152✔
873
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
48✔
874
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
96✔
875
            start.inputDirective.disabled = this.disabled;
48✔
876
            end.inputDirective.disabled = this.disabled;
48✔
877
            return;
48✔
878
        }
879
    }
880

881
    private setRequiredToInputs(): void {
882
        // workaround for igxInput setting required
883
        Promise.resolve().then(() => {
176✔
884
            const isRequired = this.required;
176✔
885
            if (this.inputGroup && this.inputGroup.isRequired !== isRequired) {
176✔
886
                this.inputGroup.isRequired = isRequired;
4✔
887
            } else if (this.hasProjectedInputs && this._ngControl) {
172✔
888
                this.projectedInputs.forEach(i => i.isRequired = isRequired);
262✔
889
            }
890
        });
891
    }
892

893
    private parseMinValue(value: string | Date): Date | null {
894
        let minValue: Date = parseDate(value);
75✔
895
        if (!minValue && this.hasProjectedInputs) {
75✔
896
            const start = this.projectedInputs.filter(i => i instanceof IgxDateRangeStartComponent)[0];
90✔
897
            if (start) {
45✔
898
                minValue = parseDate(start.dateTimeEditor.minValue);
45✔
899
            }
900
        }
901

902
        return minValue;
75✔
903
    }
904

905
    private parseMaxValue(value: string | Date): Date | null {
906
        let maxValue: Date = parseDate(value);
75✔
907
        if (!maxValue && this.projectedInputs) {
75✔
908
            const end = this.projectedInputs.filter(i => i instanceof IgxDateRangeEndComponent)[0];
90✔
909
            if (end) {
73✔
910
                maxValue = parseDate(end.dateTimeEditor.maxValue);
45✔
911
            }
912
        }
913

914
        return maxValue;
75✔
915
    }
916

917
    private updateCalendar(): void {
918
        if (!this.calendar) {
213✔
919
            return;
138✔
920
        }
921
        this.calendar.disabledDates = [];
75✔
922
        const minValue = this.parseMinValue(this.minValue);
75✔
923
        if (minValue) {
75✔
924
            this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] });
2✔
925
        }
926
        const maxValue = this.parseMaxValue(this.maxValue);
75✔
927
        if (maxValue) {
75✔
928
            this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] });
2✔
929
        }
930

931
        const range: Date[] = [];
75✔
932
        if (this.value?.start && this.value?.end) {
75✔
933
            const _value = this.toRangeOfDates(this.value);
27✔
934
            if (DateTimeUtil.greaterThanMaxValue(_value.start, _value.end)) {
27!
935
                this.swapEditorDates();
×
936
            }
937
            if (this.valueInRange(this.value, minValue, maxValue)) {
27✔
938
                range.push(_value.start, _value.end);
27✔
939
            }
940
        }
941

942
        if (range.length > 0) {
75✔
943
            this.calendar.selectDate(range);
27✔
944
        } else if (range.length === 0 && this.calendar.monthViews) {
48✔
945
            this.calendar.deselectDate();
6✔
946
        }
947
        this.calendar.viewDate = range[0] || new Date();
75✔
948
    }
949

950
    private swapEditorDates(): void {
951
        if (this.hasProjectedInputs) {
×
952
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
953
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
954
            [start.dateTimeEditor.value, end.dateTimeEditor.value] = [end.dateTimeEditor.value, start.dateTimeEditor.value];
×
955
            [this.value.start, this.value.end] = [this.value.end, this.value.start];
×
956
        }
957
    }
958

959
    private valueInRange(value: DateRange, minValue?: Date, maxValue?: Date): boolean {
960
        const _value = this.toRangeOfDates(value);
27✔
961
        if (minValue && DateTimeUtil.lessThanMinValue(_value.start, minValue, false)) {
27!
962
            return false;
×
963
        }
964
        if (maxValue && DateTimeUtil.greaterThanMaxValue(_value.end, maxValue, false)) {
27!
965
            return false;
×
966
        }
967

968
        return true;
27✔
969
    }
970

971
    private extractRange(selection: Date[]): DateRange {
972
        return {
43✔
973
            start: selection[0] || null,
43!
974
            end: selection.length > 0 ? selection[selection.length - 1] : null
43!
975
        };
976
    }
977

978
    private toRangeOfDates(range: DateRange): { start: Date; end: Date } {
979
        let start;
980
        let end;
981
        if (!isDate(range.start)) {
110✔
982
            start = DateTimeUtil.parseIsoDate(range.start);
1✔
983
        }
984
        if (!isDate(range.end)) {
110✔
985
            end = DateTimeUtil.parseIsoDate(range.end);
2✔
986
        }
987

988
        if (start || end) {
110!
989
            return { start, end };
×
990
        }
991

992
        return { start: range.start as Date, end: range.end as Date };
110✔
993
    }
994

995
    private subscribeToDateEditorEvents(): void {
996
        if (this.hasProjectedInputs) {
79✔
997
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
43✔
998
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
86✔
999
            if (start && end) {
43✔
1000
                start.dateTimeEditor.valueChange
43✔
1001
                    .pipe(takeUntil(this._destroy$))
1002
                    .subscribe(value => {
1003
                        if (this.value) {
1!
1004
                            this.value = { start: value, end: this.value.end };
×
1005
                        } else {
1006
                            this.value = { start: value, end: null };
1✔
1007
                        }
1008
                    });
1009
                end.dateTimeEditor.valueChange
43✔
1010
                    .pipe(takeUntil(this._destroy$))
1011
                    .subscribe(value => {
1012
                        if (this.value) {
1!
1013
                            this.value = { start: this.value.start, end: value as Date };
1✔
1014
                        } else {
1015
                            this.value = { start: null, end: value as Date };
×
1016
                        }
1017
                    });
1018
            }
1019
        }
1020
    }
1021

1022
    private attachOnTouched(): void {
1023
        if (this.hasProjectedInputs) {
79✔
1024
            this.projectedInputs.forEach(i => {
43✔
1025
                fromEvent(i.dateTimeEditor.nativeElement, 'blur')
86✔
1026
                    .pipe(takeUntil(this._destroy$))
1027
                    .subscribe(() => {
1028
                        if (this.collapsed) {
9✔
1029
                            this.updateValidityOnBlur();
1✔
1030
                        }
1031
                    });
1032
            });
1033
        } else {
1034
            fromEvent(this.inputDirective.nativeElement, 'blur')
36✔
1035
                .pipe(takeUntil(this._destroy$))
1036
                .subscribe(() => {
1037
                    if (this.collapsed) {
7!
1038
                        this.updateValidityOnBlur();
×
1039
                    }
1040
                });
1041
        }
1042
    }
1043

1044
    private cacheFocusedInput(): void {
1045
        if (this.hasProjectedInputs) {
79✔
1046
            this.projectedInputs.forEach(i => {
43✔
1047
                fromEvent(i.dateTimeEditor.nativeElement, 'focus')
86✔
1048
                    .pipe(takeUntil(this._destroy$))
1049
                    .subscribe(() => this._focusedInput = i);
14✔
1050
            });
1051
        }
1052
    }
1053

1054
    private configPositionStrategy(): void {
1055
        this._positionSettings = {
79✔
1056
            openAnimation: fadeIn,
1057
            closeAnimation: fadeOut
1058
        };
1059
        this._dropDownOverlaySettings.positionStrategy = new AutoPositionStrategy(this._positionSettings);
79✔
1060
        this._dropDownOverlaySettings.target = this.element.nativeElement;
79✔
1061
    }
1062

1063
    private configOverlaySettings(): void {
1064
        if (this.overlaySettings !== null) {
79✔
1065
            this._dropDownOverlaySettings = Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
79✔
1066
            this._dialogOverlaySettings = Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
79✔
1067
        }
1068
    }
1069

1070
    private initialSetValue() {
1071
        // if there is no value and no ngControl on the picker but we have inputs we may have value set through
1072
        // their ngModels - we should generate our initial control value
1073
        if ((!this.value || (!this.value.start && !this.value.end)) && this.hasProjectedInputs && !this._ngControl) {
79!
1074
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent);
2✔
1075
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent);
4✔
1076
            this._value = {
2✔
1077
                start: start.dateTimeEditor.value as Date,
1078
                end: end.dateTimeEditor.value as Date
1079
            };
1080
        }
1081
    }
1082

1083
    private updateInputs(): void {
1084
        const start = this.projectedInputs?.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
249✔
1085
        const end = this.projectedInputs?.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
270✔
1086
        if (start && end) {
249✔
1087
            const _value = this.value ? this.toRangeOfDates(this.value) : null;
135✔
1088
            start.updateInputValue(_value?.start || null);
135✔
1089
            end.updateInputValue(_value?.end || null);
135✔
1090
        }
1091
    }
1092

1093
    private updateDisplayFormat(): void {
1094
        this.projectedInputs.forEach(i => {
93✔
1095
            const input = i as IgxDateRangeInputsBaseComponent;
114✔
1096
            input.dateTimeEditor.displayFormat = this.displayFormat;
114✔
1097
        });
1098
    }
1099

1100
    private updateInputFormat(): void {
1101
        this.projectedInputs.forEach(i => {
83✔
1102
            const input = i as IgxDateRangeInputsBaseComponent;
94✔
1103
            if (input.dateTimeEditor.inputFormat !== this.inputFormat) {
94✔
1104
                input.dateTimeEditor.inputFormat = this.inputFormat;
94✔
1105
            }
1106
        });
1107
    }
1108

1109
    private updateInputLocale(): void {
1110
        this.projectedInputs.forEach(i => {
3✔
1111
            const input = i as IgxDateRangeInputsBaseComponent;
6✔
1112
            input.dateTimeEditor.locale = this.locale;
6✔
1113
        });
1114
    }
1115

1116
    private _initializeCalendarContainer(componentInstance: IgxCalendarContainerComponent) {
1117
        this._calendar = componentInstance.calendar;
41✔
1118
        this.calendar.hasHeader = false;
41✔
1119
        this.calendar.locale = this.locale;
41✔
1120
        this.calendar.selection = CalendarSelection.RANGE;
41✔
1121
        this.calendar.weekStart = this.weekStart;
41✔
1122
        this.calendar.hideOutsideDays = this.hideOutsideDays;
41✔
1123
        this.calendar.monthsViewNumber = this.displayMonthsCount;
41✔
1124
        this.calendar.showWeekNumbers = this.showWeekNumbers;
41✔
1125
        this.calendar.selected.pipe(takeUntil(this._destroy$)).subscribe((ev: Date[]) => this.handleSelection(ev));
41✔
1126

1127
        componentInstance.mode = this.mode;
41✔
1128
        componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null;
41✔
1129
        componentInstance.pickerActions = this.pickerActions;
41✔
1130
        componentInstance.usePredefinedRanges = this.usePredefinedRanges;
41✔
1131
        componentInstance.customRanges = this.customRanges;
41✔
1132
        componentInstance.resourceStrings = this.resourceStrings;
41✔
1133
        componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close());
41✔
1134
        componentInstance.rangeSelected
41✔
1135
        .pipe(takeUntil(this._destroy$))
1136
        .subscribe((r: DateRange) => {
1137
            if (r?.start && r?.end) {
6✔
1138
            this.select(new Date(r.start), new Date(r.end));
6✔
1139
            }
1140

1141
            if (this.isDropdown) {
6✔
1142
            this.close();
6✔
1143
            }
1144
        });
1145
    }
1146
}
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