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

IgniteUI / igniteui-angular / 16193550997

10 Jul 2025 11:12AM UTC coverage: 4.657% (-87.0%) from 91.64%
16193550997

Pull #16028

github

web-flow
Merge f7a9963b8 into 87246e3ce
Pull Request #16028: fix(radio-group): dynamically added radio buttons do not initialize

178 of 15764 branches covered (1.13%)

18 of 19 new or added lines in 2 files covered. (94.74%)

25721 existing lines in 324 files now uncovered.

1377 of 29570 relevant lines covered (4.66%)

0.53 hits per line

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

0.59
/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 } 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()
UNCOV
94
    public displayMonthsCount = 2;
×
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) {
UNCOV
143
        this._doneButtonText = value;
×
144
    }
145

146
    public get doneButtonText(): string {
UNCOV
147
        if (this._doneButtonText === null) {
×
UNCOV
148
            return this.resourceStrings.igx_date_range_picker_done_button;
×
149
        }
UNCOV
150
        return this._doneButtonText;
×
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) {
UNCOV
197
        this._minValue = value;
×
UNCOV
198
        this.onValidatorChange();
×
199
    }
200

201
    public get minValue(): Date | string {
UNCOV
202
        return this._minValue;
×
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) {
UNCOV
213
        this._maxValue = value;
×
UNCOV
214
        this.onValidatorChange();
×
215
    }
216

217
    public get maxValue(): Date | string {
UNCOV
218
        return this._maxValue;
×
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);
×
228
    }
229

230
    /**
231
     * An accessor that returns the resource strings.
232
     */
233
    public get resourceStrings(): IDateRangePickerResourceStrings {
UNCOV
234
        return this._resourceStrings;
×
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()
UNCOV
246
    public override placeholder = '';
×
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 })
UNCOV
276
    public showWeekNumbers = false;
×
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
    @Output()
UNCOV
287
    public valueChange = new EventEmitter<DateRange>();
×
288

289
    /** @hidden @internal */
290
    @HostBinding('class.igx-date-range-picker')
UNCOV
291
    public cssClass = 'igx-date-range-picker';
×
292

293
    @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef })
294
    private viewContainerRef: ViewContainerRef;
295

296
    /** @hidden @internal */
297
    @ViewChild(IgxInputDirective)
298
    public inputDirective: IgxInputDirective;
299

300
    /** @hidden @internal */
301
    @ContentChildren(IgxDateRangeInputsBaseComponent)
302
    public projectedInputs: QueryList<IgxDateRangeInputsBaseComponent>;
303

304
    @ContentChild(IgxLabelDirective)
305
    public label: IgxLabelDirective;
306

307
    @ContentChild(IgxPickerActionsDirective)
308
    public pickerActions: IgxPickerActionsDirective;
309

310
    /** @hidden @internal */
311
    @ContentChild(IgxDateRangeSeparatorDirective, { read: TemplateRef })
312
    public dateSeparatorTemplate: TemplateRef<any>;
313

314
    /** @hidden @internal */
315
    public get dateSeparator(): string {
UNCOV
316
        if (this._dateSeparator === null) {
×
UNCOV
317
            return this.resourceStrings.igx_date_range_picker_date_separator;
×
318
        }
319
        return this._dateSeparator;
×
320
    }
321

322
    /** @hidden @internal */
323
    public get appliedFormat(): string {
UNCOV
324
        return DateTimeUtil.getLocaleDateFormat(this.locale, this.displayFormat)
×
325
            || DateTimeUtil.DEFAULT_INPUT_FORMAT;
326
    }
327

328
    /**
329
     * @example
330
     * ```html
331
     * <igx-date-range-picker locale="jp"></igx-date-range-picker>
332
     * ```
333
     */
334
    /**
335
     * Gets the `locale` of the date-range-picker.
336
     * If not set, defaults to application's locale.
337
     */
338
    @Input()
339
    public override get locale(): string {
UNCOV
340
        return this._locale;
×
341
    }
342

343
    /**
344
     * Sets the `locale` of the date-picker.
345
     * Expects a valid BCP 47 language tag.
346
     */
347
    public override set locale(value: string) {
UNCOV
348
        this._locale = value;
×
349
        // if value is invalid, set it back to _localeId
UNCOV
350
        try {
×
UNCOV
351
            getLocaleFirstDayOfWeek(this._locale);
×
352
        } catch (e) {
UNCOV
353
            this._locale = this._localeId;
×
354
        }
UNCOV
355
        if (this.hasProjectedInputs) {
×
UNCOV
356
            this.updateInputLocale();
×
UNCOV
357
            this.updateDisplayFormat();
×
358
        }
359
    }
360

361
    /** @hidden @internal */
362
    public get singleInputFormat(): string {
UNCOV
363
        if (this.placeholder !== '') {
×
UNCOV
364
            return this.placeholder;
×
365
        }
366

UNCOV
367
        const format = this.appliedFormat;
×
UNCOV
368
        return `${format}${SingleInputDatesConcatenationString}${format}`;
×
369
    }
370

371
    /**
372
     * Gets calendar state.
373
     *
374
     * ```typescript
375
     * let state = this.dateRange.collapsed;
376
     * ```
377
     */
378
    public override get collapsed(): boolean {
UNCOV
379
        return this._collapsed;
×
380
    }
381

382
    /**
383
     * The currently selected value / range from the calendar
384
     *
385
     * @remarks
386
     * The current value is of type `DateRange`
387
     *
388
     * @example
389
     * ```typescript
390
     * const newValue: DateRange = { start: new Date("2/2/2012"), end: new Date("3/3/2013")};
391
     * this.dateRangePicker.value = newValue;
392
     * ```
393
     */
394
    public get value(): DateRange | null {
UNCOV
395
        return this._value;
×
396
    }
397

398
    @Input()
399
    public set value(value: DateRange | null) {
UNCOV
400
        this.updateValue(value);
×
UNCOV
401
        this.onChangeCallback(value);
×
UNCOV
402
        this.valueChange.emit(value);
×
403
    }
404

405
    /** @hidden @internal */
406
    public get hasProjectedInputs(): boolean {
UNCOV
407
        return this.projectedInputs?.length > 0;
×
408
    }
409

410
    /** @hidden @internal */
411
    public get separatorClass(): string {
UNCOV
412
        return 'igx-date-range-picker__label';
×
413
    }
414

415
    protected override get toggleContainer(): HTMLElement | undefined {
UNCOV
416
        return this._calendarContainer;
×
417
    }
418

419
    private get required(): boolean {
UNCOV
420
        if (this._ngControl && this._ngControl.control && this._ngControl.control.validator) {
×
UNCOV
421
            const error = this._ngControl.control.validator({} as AbstractControl);
×
UNCOV
422
            return (error && error.required) ? true : false;
×
423
        }
424

UNCOV
425
        return false;
×
426
    }
427

428
    private get calendar(): IgxCalendarComponent {
UNCOV
429
        return this._calendar;
×
430
    }
431

432
    private get dropdownOverlaySettings(): OverlaySettings {
UNCOV
433
        return Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
×
434
    }
435

436
    private get dialogOverlaySettings(): OverlaySettings {
UNCOV
437
        return Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
×
438
    }
439

UNCOV
440
    private _resourceStrings = getCurrentResourceStrings(DateRangePickerResourceStringsEN);
×
UNCOV
441
    private _doneButtonText = null;
×
UNCOV
442
    private _dateSeparator = null;
×
443
    private _value: DateRange | null;
444
    private _overlayId: string;
445
    private _ngControl: NgControl;
446
    private _statusChanges$: Subscription;
447
    private _calendar: IgxCalendarComponent;
448
    private _calendarContainer?: HTMLElement;
449
    private _positionSettings: PositionSettings;
450
    private _focusedInput: IgxDateRangeInputsBaseComponent;
UNCOV
451
    private _overlaySubFilter:
×
452
        [MonoTypeOperatorFunction<OverlayEventArgs>, MonoTypeOperatorFunction<OverlayEventArgs | OverlayCancelableEventArgs>] = [
UNCOV
453
            filter(x => x.id === this._overlayId),
×
454
            takeUntil(merge(this._destroy$, this.closed))
455
        ];
UNCOV
456
    private _dialogOverlaySettings: OverlaySettings = {
×
457
        closeOnOutsideClick: true,
458
        modal: true,
459
        closeOnEscape: true
460
    };
UNCOV
461
    private _dropDownOverlaySettings: OverlaySettings = {
×
462
        closeOnOutsideClick: true,
463
        modal: false,
464
        closeOnEscape: true
465
    };
UNCOV
466
    private onChangeCallback: (dateRange: DateRange) => void = noop;
×
UNCOV
467
    private onTouchCallback: () => void = noop;
×
UNCOV
468
    private onValidatorChange: () => void = noop;
×
469

470
    constructor(element: ElementRef,
471
        @Inject(LOCALE_ID) _localeId: string,
UNCOV
472
        protected platform: PlatformUtil,
×
UNCOV
473
        private _injector: Injector,
×
UNCOV
474
        private _cdr: ChangeDetectorRef,
×
UNCOV
475
        @Inject(IgxOverlayService) private _overlayService: IgxOverlayService,
×
476
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType?: IgxInputGroupType) {
UNCOV
477
        super(element, _localeId, _inputGroupType);
×
UNCOV
478
        this.locale = this.locale || this._localeId;
×
479
    }
480

481
    /** @hidden @internal */
482
    @HostListener('keydown', ['$event'])
483
    /** @hidden @internal */
484
    public onKeyDown(event: KeyboardEvent): void {
UNCOV
485
        switch (event.key) {
×
486
            case this.platform.KEYMAP.ARROW_UP:
487
                if (event.altKey) {
×
488
                    this.close();
×
489
                }
490
                break;
×
491
            case this.platform.KEYMAP.ARROW_DOWN:
UNCOV
492
                if (event.altKey) {
×
UNCOV
493
                    this.open();
×
494
                }
UNCOV
495
                break;
×
496
        }
497
    }
498

499
    /**
500
     * Opens the date range picker's dropdown or dialog.
501
     *
502
     * @example
503
     * ```html
504
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
505
     *
506
     * <button type="button" igxButton (click)="dateRange.open()">Open Dialog</button
507
     * ```
508
     */
509
    public open(overlaySettings?: OverlaySettings): void {
UNCOV
510
        if (!this.collapsed || this.disabled) {
×
UNCOV
511
            return;
×
512
        }
513

UNCOV
514
        const settings = Object.assign({}, this.isDropdown
×
515
            ? this.dropdownOverlaySettings
516
            : this.dialogOverlaySettings
517
            , overlaySettings);
518

UNCOV
519
        this._overlayId = this._overlayService
×
520
            .attach(IgxCalendarContainerComponent, this.viewContainerRef, settings);
UNCOV
521
        this.subscribeToOverlayEvents();
×
UNCOV
522
        this._overlayService.show(this._overlayId);
×
523
    }
524

525
    /**
526
     * Closes the date range picker's dropdown or dialog.
527
     *
528
     * @example
529
     * ```html
530
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
531
     *
532
     * <button type="button" igxButton (click)="dateRange.close()">Close Dialog</button>
533
     * ```
534
     */
535
    public close(): void {
UNCOV
536
        if (!this.collapsed) {
×
UNCOV
537
            this._overlayService.hide(this._overlayId);
×
538
        }
539
    }
540

541
    /**
542
     * Toggles the date range picker's dropdown or dialog
543
     *
544
     * @example
545
     * ```html
546
     * <igx-date-range-picker #dateRange></igx-date-range-picker>
547
     *
548
     * <button type="button" igxButton (click)="dateRange.toggle()">Toggle Dialog</button>
549
     * ```
550
     */
551
    public toggle(overlaySettings?: OverlaySettings): void {
UNCOV
552
        if (!this.collapsed) {
×
UNCOV
553
            this.close();
×
554
        } else {
UNCOV
555
            this.open(overlaySettings);
×
556
        }
557
    }
558

559
    /**
560
     * Selects a range of dates. If no `endDate` is passed, range is 1 day (only `startDate`)
561
     *
562
     * @example
563
     * ```typescript
564
     * public selectFiveDayRange() {
565
     *  const today = new Date();
566
     *  const inFiveDays = new Date(new Date().setDate(today.getDate() + 5));
567
     *  this.dateRange.select(today, inFiveDays);
568
     * }
569
     * ```
570
     */
571
    public select(startDate: Date, endDate?: Date): void {
UNCOV
572
        endDate = endDate ?? startDate;
×
UNCOV
573
        const dateRange = [startDate, endDate];
×
UNCOV
574
        this.handleSelection(dateRange);
×
575
    }
576

577
    /** @hidden @internal */
578
    public writeValue(value: DateRange): void {
UNCOV
579
        this.updateValue(value);
×
580
    }
581

582
    /** @hidden @internal */
583
    public registerOnChange(fn: any): void {
UNCOV
584
        this.onChangeCallback = fn;
×
585
    }
586

587
    /** @hidden @internal */
588
    public registerOnTouched(fn: any): void {
UNCOV
589
        this.onTouchCallback = fn;
×
590
    }
591

592
    /** @hidden @internal */
593
    public validate(control: AbstractControl): ValidationErrors | null {
UNCOV
594
        const value: DateRange = control.value;
×
UNCOV
595
        const errors = {};
×
UNCOV
596
        if (value) {
×
UNCOV
597
            if (this.hasProjectedInputs) {
×
UNCOV
598
                const startInput = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
UNCOV
599
                const endInput = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
UNCOV
600
                if (!startInput.dateTimeEditor.value) {
×
601
                    Object.assign(errors, { startValue: true });
×
602
                }
UNCOV
603
                if (!endInput.dateTimeEditor.value) {
×
UNCOV
604
                    Object.assign(errors, { endValue: true });
×
605
                }
606
            }
607

UNCOV
608
            const min = parseDate(this.minValue);
×
UNCOV
609
            const max = parseDate(this.maxValue);
×
UNCOV
610
            const start = parseDate(value.start);
×
UNCOV
611
            const end = parseDate(value.end);
×
UNCOV
612
            if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false))
×
613
                || (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) {
UNCOV
614
                Object.assign(errors, { minValue: true });
×
615
            }
UNCOV
616
            if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false))
×
617
                || (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) {
UNCOV
618
                Object.assign(errors, { maxValue: true });
×
619
            }
620
        }
621

UNCOV
622
        return Object.keys(errors).length > 0 ? errors : null;
×
623
    }
624

625
    /** @hidden @internal */
626
    public registerOnValidatorChange?(fn: any): void {
UNCOV
627
        this.onValidatorChange = fn;
×
628
    }
629

630
    /** @hidden @internal */
631
    public setDisabledState?(isDisabled: boolean): void {
UNCOV
632
        this.disabled = isDisabled;
×
633
    }
634

635
    /** @hidden */
636
    public ngOnInit(): void {
UNCOV
637
        this._ngControl = this._injector.get<NgControl>(NgControl, null);
×
638

UNCOV
639
        this.locale = this.locale || this._localeId;
×
640
    }
641

642
    /** @hidden */
643
    public override ngAfterViewInit(): void {
UNCOV
644
        super.ngAfterViewInit();
×
UNCOV
645
        this.subscribeToDateEditorEvents();
×
UNCOV
646
        this.configPositionStrategy();
×
UNCOV
647
        this.configOverlaySettings();
×
UNCOV
648
        this.cacheFocusedInput();
×
UNCOV
649
        this.attachOnTouched();
×
650

UNCOV
651
        this.setRequiredToInputs();
×
652

UNCOV
653
        if (this._ngControl) {
×
UNCOV
654
            this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this));
×
655
        }
656

657
        // delay invocations until the current change detection cycle has completed
UNCOV
658
        Promise.resolve().then(() => {
×
UNCOV
659
            this.updateDisabledState();
×
UNCOV
660
            this.initialSetValue();
×
UNCOV
661
            this.updateInputs();
×
662
            // B.P. 07 July 2021 - IgxDateRangePicker not showing initial disabled state with ChangeDetectionStrategy.OnPush #9776
663
            /**
664
             * if disabled is placed on the range picker element and there are projected inputs
665
             * run change detection since igxInput will initially set the projected inputs' disabled to false
666
             */
UNCOV
667
            if (this.hasProjectedInputs && this.disabled) {
×
UNCOV
668
                this._cdr.markForCheck();
×
669
            }
670
        });
UNCOV
671
        this.updateDisplayFormat();
×
UNCOV
672
        this.updateInputFormat();
×
673
    }
674

675
    /** @hidden @internal */
676
    public ngOnChanges(changes: SimpleChanges): void {
UNCOV
677
        if (changes['displayFormat'] && this.hasProjectedInputs) {
×
UNCOV
678
            this.updateDisplayFormat();
×
679
        }
UNCOV
680
        if (changes['inputFormat'] && this.hasProjectedInputs) {
×
UNCOV
681
            this.updateInputFormat();
×
682
        }
UNCOV
683
        if (changes['disabled']) {
×
UNCOV
684
            this.updateDisabledState();
×
685
        }
686
    }
687

688
    /** @hidden @internal */
689
    public override ngOnDestroy(): void {
UNCOV
690
        super.ngOnDestroy();
×
UNCOV
691
        if (this._statusChanges$) {
×
UNCOV
692
            this._statusChanges$.unsubscribe();
×
693
        }
UNCOV
694
        if (this._overlayId) {
×
UNCOV
695
            this._overlayService.detach(this._overlayId);
×
696
        }
697
    }
698

699
    /** @hidden @internal */
700
    public getEditElement() {
701
        return this.inputDirective.nativeElement;
×
702
    }
703

UNCOV
704
    protected onStatusChanged = () => {
×
UNCOV
705
        if (this.inputGroup) {
×
UNCOV
706
            this.setValidityState(this.inputDirective, this.inputGroup.isFocused);
×
UNCOV
707
        } else if (this.hasProjectedInputs) {
×
UNCOV
708
            this.projectedInputs
×
709
                .forEach((i) => {
UNCOV
710
                    this.setValidityState(i.inputDirective, i.isFocused);
×
711
                });
712
        }
UNCOV
713
        this.setRequiredToInputs();
×
714
    };
715

716
    private setValidityState(inputDirective: IgxInputDirective, isFocused: boolean) {
UNCOV
717
        if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) {
×
UNCOV
718
            if (this.hasValidators && isFocused) {
×
UNCOV
719
                inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
×
720
            } else {
UNCOV
721
                inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
×
722
            }
723
        } else {
UNCOV
724
            inputDirective.valid = IgxInputState.INITIAL;
×
725
        }
726
    }
727

728
    private get isTouchedOrDirty(): boolean {
UNCOV
729
        return (this._ngControl.control.touched || this._ngControl.control.dirty);
×
730
    }
731

732
    private get hasValidators(): boolean {
UNCOV
733
        return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
×
734
    }
735

736
    private handleSelection(selectionData: Date[]): void {
UNCOV
737
        let newValue = this.extractRange(selectionData);
×
UNCOV
738
        if (!newValue.start && !newValue.end) {
×
739
            newValue = null;
×
740
        }
UNCOV
741
        this.value = newValue;
×
UNCOV
742
        if (this.isDropdown && selectionData?.length > 1) {
×
UNCOV
743
            this.close();
×
744
        }
745
    }
746

747
    private handleClosing(e: IBaseCancelableBrowserEventArgs): void {
UNCOV
748
        const args = { owner: this, cancel: e?.cancel, event: e?.event };
×
UNCOV
749
        this.closing.emit(args);
×
UNCOV
750
        e.cancel = args.cancel;
×
UNCOV
751
        if (args.cancel) {
×
UNCOV
752
            return;
×
753
        }
754

UNCOV
755
        if (this.isDropdown && e?.event && !this.isFocused) {
×
756
            // outside click
757
            this.updateValidityOnBlur();
×
758
        } else {
UNCOV
759
            this.onTouchCallback();
×
760
            // input click
UNCOV
761
            if (this.hasProjectedInputs && this._focusedInput) {
×
UNCOV
762
                this._focusedInput.setFocus();
×
763
            }
UNCOV
764
            if (this.inputDirective) {
×
UNCOV
765
                this.inputDirective.focus();
×
766
            }
767
        }
768
    }
769

770
    private subscribeToOverlayEvents() {
UNCOV
771
        this._overlayService.opening.pipe(...this._overlaySubFilter).subscribe((e) => {
×
UNCOV
772
            const overlayEvent = e as OverlayCancelableEventArgs;
×
UNCOV
773
            const args = { owner: this, cancel: overlayEvent?.cancel, event: e.event };
×
UNCOV
774
            this.opening.emit(args);
×
UNCOV
775
            if (args.cancel) {
×
776
                this._overlayService.detach(this._overlayId);
×
777
                overlayEvent.cancel = true;
×
778
                return;
×
779
            }
780

UNCOV
781
            this._initializeCalendarContainer(e.componentRef.instance);
×
UNCOV
782
            this._calendarContainer = e.componentRef.location.nativeElement;
×
UNCOV
783
            this._collapsed = false;
×
UNCOV
784
            this.updateCalendar();
×
785
        });
786

UNCOV
787
        this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => {
×
UNCOV
788
            this.calendar.wrapper.nativeElement.focus();
×
UNCOV
789
            this.opened.emit({ owner: this });
×
790
        });
791

UNCOV
792
        this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e) => {
×
UNCOV
793
            this.handleClosing(e as OverlayCancelableEventArgs);
×
794
        });
795

UNCOV
796
        this._overlayService.closed.pipe(...this._overlaySubFilter).subscribe(() => {
×
UNCOV
797
            this._overlayService.detach(this._overlayId);
×
UNCOV
798
            this._collapsed = true;
×
UNCOV
799
            this._overlayId = null;
×
UNCOV
800
            this._calendar = null;
×
UNCOV
801
            this._calendarContainer = undefined;
×
UNCOV
802
            this.closed.emit({ owner: this });
×
803
        });
804
    }
805

806
    private updateValue(value: DateRange) {
UNCOV
807
        this._value = value ? value : null;
×
UNCOV
808
        this.updateInputs();
×
UNCOV
809
        this.updateCalendar();
×
810
    }
811

812
    private updateValidityOnBlur() {
UNCOV
813
        this._focusedInput = null;
×
UNCOV
814
        this.onTouchCallback();
×
UNCOV
815
        if (this._ngControl) {
×
UNCOV
816
            if (this.hasProjectedInputs) {
×
UNCOV
817
                this.projectedInputs.forEach(i => {
×
UNCOV
818
                    if (!this._ngControl.valid) {
×
UNCOV
819
                        i.updateInputValidity(IgxInputState.INVALID);
×
820
                    } else {
821
                        i.updateInputValidity(IgxInputState.INITIAL);
×
822
                    }
823
                });
824
            }
825

UNCOV
826
            if (this.inputDirective) {
×
827
                if (!this._ngControl.valid) {
×
828
                    this.inputDirective.valid = IgxInputState.INVALID;
×
829
                } else {
830
                    this.inputDirective.valid = IgxInputState.INITIAL;
×
831
                }
832
            }
833
        }
834
    }
835

836
    private updateDisabledState() {
UNCOV
837
        if (this.hasProjectedInputs) {
×
UNCOV
838
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
UNCOV
839
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
UNCOV
840
            start.inputDirective.disabled = this.disabled;
×
UNCOV
841
            end.inputDirective.disabled = this.disabled;
×
UNCOV
842
            return;
×
843
        }
844
    }
845

846
    private setRequiredToInputs(): void {
847
        // workaround for igxInput setting required
UNCOV
848
        Promise.resolve().then(() => {
×
UNCOV
849
            const isRequired = this.required;
×
UNCOV
850
            if (this.inputGroup && this.inputGroup.isRequired !== isRequired) {
×
UNCOV
851
                this.inputGroup.isRequired = isRequired;
×
UNCOV
852
            } else if (this.hasProjectedInputs && this._ngControl) {
×
UNCOV
853
                this.projectedInputs.forEach(i => i.isRequired = isRequired);
×
854
            }
855
        });
856
    }
857

858
    private parseMinValue(value: string | Date): Date | null {
UNCOV
859
        let minValue: Date = parseDate(value);
×
UNCOV
860
        if (!minValue && this.hasProjectedInputs) {
×
UNCOV
861
            const start = this.projectedInputs.filter(i => i instanceof IgxDateRangeStartComponent)[0];
×
UNCOV
862
            if (start) {
×
UNCOV
863
                minValue = parseDate(start.dateTimeEditor.minValue);
×
864
            }
865
        }
866

UNCOV
867
        return minValue;
×
868
    }
869

870
    private parseMaxValue(value: string | Date): Date | null {
UNCOV
871
        let maxValue: Date = parseDate(value);
×
UNCOV
872
        if (!maxValue && this.projectedInputs) {
×
UNCOV
873
            const end = this.projectedInputs.filter(i => i instanceof IgxDateRangeEndComponent)[0];
×
UNCOV
874
            if (end) {
×
UNCOV
875
                maxValue = parseDate(end.dateTimeEditor.maxValue);
×
876
            }
877
        }
878

UNCOV
879
        return maxValue;
×
880
    }
881

882
    private updateCalendar(): void {
UNCOV
883
        if (!this.calendar) {
×
UNCOV
884
            return;
×
885
        }
UNCOV
886
        this.calendar.disabledDates = [];
×
UNCOV
887
        const minValue = this.parseMinValue(this.minValue);
×
UNCOV
888
        if (minValue) {
×
UNCOV
889
            this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] });
×
890
        }
UNCOV
891
        const maxValue = this.parseMaxValue(this.maxValue);
×
UNCOV
892
        if (maxValue) {
×
UNCOV
893
            this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] });
×
894
        }
895

UNCOV
896
        const range: Date[] = [];
×
UNCOV
897
        if (this.value?.start && this.value?.end) {
×
UNCOV
898
            const _value = this.toRangeOfDates(this.value);
×
UNCOV
899
            if (DateTimeUtil.greaterThanMaxValue(_value.start, _value.end)) {
×
900
                this.swapEditorDates();
×
901
            }
UNCOV
902
            if (this.valueInRange(this.value, minValue, maxValue)) {
×
UNCOV
903
                range.push(_value.start, _value.end);
×
904
            }
905
        }
906

UNCOV
907
        if (range.length > 0) {
×
UNCOV
908
            this.calendar.selectDate(range);
×
UNCOV
909
        } else if (range.length === 0 && this.calendar.monthViews) {
×
910
            this.calendar.deselectDate();
×
911
        }
UNCOV
912
        this.calendar.viewDate = range[0] || new Date();
×
913
    }
914

915
    private swapEditorDates(): void {
916
        if (this.hasProjectedInputs) {
×
917
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
918
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
919
            [start.dateTimeEditor.value, end.dateTimeEditor.value] = [end.dateTimeEditor.value, start.dateTimeEditor.value];
×
920
            [this.value.start, this.value.end] = [this.value.end, this.value.start];
×
921
        }
922
    }
923

924
    private valueInRange(value: DateRange, minValue?: Date, maxValue?: Date): boolean {
UNCOV
925
        const _value = this.toRangeOfDates(value);
×
UNCOV
926
        if (minValue && DateTimeUtil.lessThanMinValue(_value.start, minValue, false)) {
×
927
            return false;
×
928
        }
UNCOV
929
        if (maxValue && DateTimeUtil.greaterThanMaxValue(_value.end, maxValue, false)) {
×
930
            return false;
×
931
        }
932

UNCOV
933
        return true;
×
934
    }
935

936
    private extractRange(selection: Date[]): DateRange {
UNCOV
937
        return {
×
938
            start: selection[0] || null,
×
939
            end: selection.length > 0 ? selection[selection.length - 1] : null
×
940
        };
941
    }
942

943
    private toRangeOfDates(range: DateRange): { start: Date; end: Date } {
944
        let start;
945
        let end;
UNCOV
946
        if (!isDate(range.start)) {
×
UNCOV
947
            start = DateTimeUtil.parseIsoDate(range.start);
×
948
        }
UNCOV
949
        if (!isDate(range.end)) {
×
UNCOV
950
            end = DateTimeUtil.parseIsoDate(range.end);
×
951
        }
952

UNCOV
953
        if (start || end) {
×
954
            return { start, end };
×
955
        }
956

UNCOV
957
        return { start: range.start as Date, end: range.end as Date };
×
958
    }
959

960
    private subscribeToDateEditorEvents(): void {
UNCOV
961
        if (this.hasProjectedInputs) {
×
UNCOV
962
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
UNCOV
963
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
UNCOV
964
            if (start && end) {
×
UNCOV
965
                start.dateTimeEditor.valueChange
×
966
                    .pipe(takeUntil(this._destroy$))
967
                    .subscribe(value => {
UNCOV
968
                        if (this.value) {
×
969
                            this.value = { start: value, end: this.value.end };
×
970
                        } else {
UNCOV
971
                            this.value = { start: value, end: null };
×
972
                        }
973
                    });
UNCOV
974
                end.dateTimeEditor.valueChange
×
975
                    .pipe(takeUntil(this._destroy$))
976
                    .subscribe(value => {
UNCOV
977
                        if (this.value) {
×
UNCOV
978
                            this.value = { start: this.value.start, end: value as Date };
×
979
                        } else {
980
                            this.value = { start: null, end: value as Date };
×
981
                        }
982
                    });
983
            }
984
        }
985
    }
986

987
    private attachOnTouched(): void {
UNCOV
988
        if (this.hasProjectedInputs) {
×
UNCOV
989
            this.projectedInputs.forEach(i => {
×
UNCOV
990
                fromEvent(i.dateTimeEditor.nativeElement, 'blur')
×
991
                    .pipe(takeUntil(this._destroy$))
992
                    .subscribe(() => {
UNCOV
993
                        if (this.collapsed) {
×
UNCOV
994
                            this.updateValidityOnBlur();
×
995
                        }
996
                    });
997
            });
998
        } else {
UNCOV
999
            fromEvent(this.inputDirective.nativeElement, 'blur')
×
1000
                .pipe(takeUntil(this._destroy$))
1001
                .subscribe(() => {
UNCOV
1002
                    if (this.collapsed) {
×
1003
                        this.updateValidityOnBlur();
×
1004
                    }
1005
                });
1006
        }
1007
    }
1008

1009
    private cacheFocusedInput(): void {
UNCOV
1010
        if (this.hasProjectedInputs) {
×
UNCOV
1011
            this.projectedInputs.forEach(i => {
×
UNCOV
1012
                fromEvent(i.dateTimeEditor.nativeElement, 'focus')
×
1013
                    .pipe(takeUntil(this._destroy$))
UNCOV
1014
                    .subscribe(() => this._focusedInput = i);
×
1015
            });
1016
        }
1017
    }
1018

1019
    private configPositionStrategy(): void {
UNCOV
1020
        this._positionSettings = {
×
1021
            openAnimation: fadeIn,
1022
            closeAnimation: fadeOut
1023
        };
UNCOV
1024
        this._dropDownOverlaySettings.positionStrategy = new AutoPositionStrategy(this._positionSettings);
×
UNCOV
1025
        this._dropDownOverlaySettings.target = this.element.nativeElement;
×
1026
    }
1027

1028
    private configOverlaySettings(): void {
UNCOV
1029
        if (this.overlaySettings !== null) {
×
UNCOV
1030
            this._dropDownOverlaySettings = Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
×
UNCOV
1031
            this._dialogOverlaySettings = Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
×
1032
        }
1033
    }
1034

1035
    private initialSetValue() {
1036
        // if there is no value and no ngControl on the picker but we have inputs we may have value set through
1037
        // their ngModels - we should generate our initial control value
UNCOV
1038
        if ((!this.value || (!this.value.start && !this.value.end)) && this.hasProjectedInputs && !this._ngControl) {
×
UNCOV
1039
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent);
×
UNCOV
1040
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent);
×
UNCOV
1041
            this._value = {
×
1042
                start: start.dateTimeEditor.value as Date,
1043
                end: end.dateTimeEditor.value as Date
1044
            };
1045
        }
1046
    }
1047

1048
    private updateInputs(): void {
UNCOV
1049
        const start = this.projectedInputs?.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
UNCOV
1050
        const end = this.projectedInputs?.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
UNCOV
1051
        if (start && end) {
×
UNCOV
1052
            const _value = this.value ? this.toRangeOfDates(this.value) : null;
×
UNCOV
1053
            start.updateInputValue(_value?.start || null);
×
UNCOV
1054
            end.updateInputValue(_value?.end || null);
×
1055
        }
1056
    }
1057

1058
    private updateDisplayFormat(): void {
UNCOV
1059
        this.projectedInputs.forEach(i => {
×
UNCOV
1060
            const input = i as IgxDateRangeInputsBaseComponent;
×
UNCOV
1061
            input.dateTimeEditor.displayFormat = this.displayFormat;
×
1062
        });
1063
    }
1064

1065
    private updateInputFormat(): void {
UNCOV
1066
        this.projectedInputs.forEach(i => {
×
UNCOV
1067
            const input = i as IgxDateRangeInputsBaseComponent;
×
UNCOV
1068
            if (input.dateTimeEditor.inputFormat !== this.inputFormat) {
×
UNCOV
1069
                input.dateTimeEditor.inputFormat = this.inputFormat;
×
1070
            }
1071
        });
1072
    }
1073

1074
    private updateInputLocale(): void {
UNCOV
1075
        this.projectedInputs.forEach(i => {
×
UNCOV
1076
            const input = i as IgxDateRangeInputsBaseComponent;
×
UNCOV
1077
            input.dateTimeEditor.locale = this.locale;
×
1078
        });
1079
    }
1080

1081
    private _initializeCalendarContainer(componentInstance: IgxCalendarContainerComponent) {
UNCOV
1082
        this._calendar = componentInstance.calendar;
×
UNCOV
1083
        this.calendar.hasHeader = false;
×
UNCOV
1084
        this.calendar.locale = this.locale;
×
UNCOV
1085
        this.calendar.selection = CalendarSelection.RANGE;
×
UNCOV
1086
        this.calendar.weekStart = this.weekStart;
×
UNCOV
1087
        this.calendar.hideOutsideDays = this.hideOutsideDays;
×
UNCOV
1088
        this.calendar.monthsViewNumber = this.displayMonthsCount;
×
UNCOV
1089
        this.calendar.showWeekNumbers = this.showWeekNumbers;
×
UNCOV
1090
        this.calendar.selected.pipe(takeUntil(this._destroy$)).subscribe((ev: Date[]) => this.handleSelection(ev));
×
1091

UNCOV
1092
        componentInstance.mode = this.mode;
×
UNCOV
1093
        componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null;
×
UNCOV
1094
        componentInstance.pickerActions = this.pickerActions;
×
UNCOV
1095
        componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close());
×
1096
    }
1097
}
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