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

IgniteUI / igniteui-angular / 11349003465

15 Oct 2024 03:10PM UTC coverage: 91.609% (+0.03%) from 91.576%
11349003465

push

github

web-flow
fix(grid): nav service cdr & event emit while row edit (#14735)

* test(grid): add test for row edit nav service handling/events

* fix(grid): nav service cdr & event emit while row edit

* fix(grid): nav service cdr & event emit while row edit

---------

Co-authored-by: Radoslav Karaivanov <rkaraivanov@infragistics.com>
Co-authored-by: Desislava Dincheva <34240583+ddincheva@users.noreply.github.com>

12911 of 15123 branches covered (85.37%)

4 of 4 new or added lines in 2 files covered. (100.0%)

143 existing lines in 9 files now uncovered.

26212 of 28613 relevant lines covered (91.61%)

33835.34 hits per line

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

91.64
/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, 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 = ' - ';
2✔
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
    standalone: true,
71
    imports: [
72
        NgIf,
73
        NgTemplateOutlet,
74
        IgxIconComponent,
75
        IgxInputGroupComponent,
76
        IgxInputDirective,
77
        IgxPrefixDirective,
78
        DateRangePickerFormatPipe
79
    ]
80
})
81
export class IgxDateRangePickerComponent extends PickerBaseDirective
2✔
82
    implements OnChanges, OnInit, AfterViewInit, OnDestroy, ControlValueAccessor, Validator {
83

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

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

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

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

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

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

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

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

203
    public get minValue(): Date | string {
204
        return this._minValue;
101✔
205
    }
206

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

219
    public get maxValue(): Date | string {
220
        return this._maxValue;
101✔
221
    }
222

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

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

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

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

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

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

291
    /** @hidden @internal */
292
    @HostBinding('class.igx-date-range-picker')
293
    public cssClass = 'igx-date-range-picker';
72✔
294

295
    @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef })
296
    private viewContainerRef: ViewContainerRef;
297

298
    /** @hidden @internal */
299
    @ViewChild(IgxInputDirective)
300
    public inputDirective: IgxInputDirective;
301

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

306
    @ContentChild(IgxLabelDirective)
307
    public label: IgxLabelDirective;
308

309
    @ContentChild(IgxPickerActionsDirective)
310
    public pickerActions: IgxPickerActionsDirective;
311

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

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

324
    /** @hidden @internal */
325
    public get appliedFormat(): string {
326
        return DateTimeUtil.getLocaleDateFormat(this.locale, this.displayFormat)
506✔
327
            || DateTimeUtil.DEFAULT_INPUT_FORMAT;
328
    }
329

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

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

363
    /** @hidden @internal */
364
    public get singleInputFormat(): string {
365
        if (this.placeholder !== '') {
236✔
366
            return this.placeholder;
2✔
367
        }
368

369
        const format = this.appliedFormat;
234✔
370
        return `${format}${SingleInputDatesConcatenationString}${format}`;
234✔
371
    }
372

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

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

400
    @Input()
401
    public set value(value: DateRange | null) {
402
        this.updateValue(value);
73✔
403
        this.onChangeCallback(value);
73✔
404
        this.valueChange.emit(value);
73✔
405
    }
406

407
    /** @hidden @internal */
408
    public get hasProjectedInputs(): boolean {
409
        return this.projectedInputs?.length > 0;
1,612✔
410
    }
411

412
    /** @hidden @internal */
413
    public get separatorClass(): string {
414
        return 'igx-date-range-picker__label';
266✔
415
    }
416

417
    private get required(): boolean {
418
        if (this._ngControl && this._ngControl.control && this._ngControl.control.validator) {
144✔
419
            const error = this._ngControl.control.validator({} as AbstractControl);
107✔
420
            return (error && error.required) ? true : false;
107!
421
        }
422

423
        return false;
37✔
424
    }
425

426
    private get calendar(): IgxCalendarComponent {
427
        return this._calendar;
582✔
428
    }
429

430
    private get dropdownOverlaySettings(): OverlaySettings {
431
        return Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
26✔
432
    }
433

434
    private get dialogOverlaySettings(): OverlaySettings {
435
        return Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
6✔
436
    }
437

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

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

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

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

511
        const settings = Object.assign({}, this.isDropdown
32✔
512
            ? this.dropdownOverlaySettings
513
            : this.dialogOverlaySettings
514
            , overlaySettings);
515

516
        this._overlayId = this._overlayService
32✔
517
            .attach(IgxCalendarContainerComponent, this.viewContainerRef, settings);
518
        this.subscribeToOverlayEvents();
32✔
519
        this._overlayService.show(this._overlayId);
32✔
520
    }
521

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

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

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

574
    /** @hidden @internal */
575
    public writeValue(value: DateRange): void {
576
        this.updateValue(value);
58✔
577
    }
578

579
    /** @hidden @internal */
580
    public registerOnChange(fn: any): void {
581
        this.onChangeCallback = fn;
39✔
582
    }
583

584
    /** @hidden @internal */
585
    public registerOnTouched(fn: any): void {
586
        this.onTouchCallback = fn;
38✔
587
    }
588

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

605
            const min = parseDate(this.minValue);
44✔
606
            const max = parseDate(this.maxValue);
44✔
607
            const start = parseDate(value.start);
44✔
608
            const end = parseDate(value.end);
44✔
609
            if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false))
44✔
610
                || (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) {
611
                Object.assign(errors, { minValue: true });
2✔
612
            }
613
            if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false))
44✔
614
                || (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) {
615
                Object.assign(errors, { maxValue: true });
1✔
616
            }
617
        }
618

619
        return Object.keys(errors).length > 0 ? errors : null;
199✔
620
    }
621

622
    /** @hidden @internal */
623
    public registerOnValidatorChange?(fn: any): void {
624
        this.onValidatorChange = fn;
38✔
625
    }
626

627
    /** @hidden @internal */
628
    public setDisabledState?(isDisabled: boolean): void {
629
        this.disabled = isDisabled;
39✔
630
    }
631

632
    /** @hidden */
633
    public ngOnInit(): void {
634
        this._ngControl = this._injector.get<NgControl>(NgControl, null);
69✔
635

636
        this.locale = this.locale || this._localeId;
69!
637
    }
638

639
    /** @hidden */
640
    public override ngAfterViewInit(): void {
641
        super.ngAfterViewInit();
66✔
642
        this.subscribeToDateEditorEvents();
66✔
643
        this.configPositionStrategy();
66✔
644
        this.configOverlaySettings();
66✔
645
        this.cacheFocusedInput();
66✔
646
        this.attachOnTouched();
66✔
647

648
        this.setRequiredToInputs();
66✔
649

650
        if (this._ngControl) {
66✔
651
            this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this));
31✔
652
        }
653

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

672
    /** @hidden @internal */
673
    public ngOnChanges(changes: SimpleChanges): void {
674
        if (changes['displayFormat'] && this.hasProjectedInputs) {
83✔
675
            this.updateDisplayFormat();
11✔
676
        }
677
        if (changes['inputFormat'] && this.hasProjectedInputs) {
83✔
678
            this.updateInputFormat();
4✔
679
        }
680
        if (changes['disabled']) {
83✔
681
            this.updateDisabledState();
60✔
682
        }
683
    }
684

685
    /** @hidden @internal */
686
    public override ngOnDestroy(): void {
687
        super.ngOnDestroy();
56✔
688
        if (this._statusChanges$) {
56✔
689
            this._statusChanges$.unsubscribe();
31✔
690
        }
691
        if (this._overlayId) {
56✔
692
            this._overlayService.detach(this._overlayId);
8✔
693
        }
694
    }
695

696
    /** @hidden @internal */
697
    public getEditElement() {
UNCOV
698
        return this.inputDirective.nativeElement;
×
699
    }
700

701
    protected onStatusChanged = () => {
72✔
702
        if (this.inputGroup) {
77✔
703
            this.setValidityState(this.inputDirective, this.inputGroup.isFocused);
7✔
704
        } else if (this.hasProjectedInputs) {
70✔
705
            this.projectedInputs
70✔
706
                .forEach((i) => {
707
                    this.setValidityState(i.inputDirective, i.isFocused);
140✔
708
                });
709
        }
710
        this.setRequiredToInputs();
77✔
711
    };
712

713
    private setValidityState(inputDirective: IgxInputDirective, isFocused: boolean) {
714
        if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) {
147✔
715
            if (this.hasValidators && isFocused) {
137✔
716
                inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
2✔
717
            } else {
718
                inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
135✔
719
            }
720
        } else {
721
            inputDirective.valid = IgxInputState.INITIAL;
10✔
722
        }
723
    }
724

725
    private get isTouchedOrDirty(): boolean {
726
        return (this._ngControl.control.touched || this._ngControl.control.dirty);
137✔
727
    }
728

729
    private get hasValidators(): boolean {
730
        return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
137✔
731
    }
732

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

744
    private handleClosing(e: IBaseCancelableBrowserEventArgs): void {
745
        const args = { owner: this, cancel: e?.cancel, event: e?.event };
32✔
746
        this.closing.emit(args);
32✔
747
        e.cancel = args.cancel;
32✔
748
        if (args.cancel) {
32✔
749
            return;
2✔
750
        }
751

752
        if (this.isDropdown && e?.event && !this.element.nativeElement.contains(e.event.target)) {
30✔
753
            // outside click
754
            this.updateValidityOnBlur();
3✔
755
        } else {
756
            this.onTouchCallback();
27✔
757
            // input click
758
            if (this.hasProjectedInputs && this._focusedInput) {
27✔
759
                this._focusedInput.setFocus();
2✔
760
                this._focusedInput = null;
2✔
761
            }
762
            if (this.inputDirective) {
27✔
763
                this.inputDirective.focus();
14✔
764
            }
765
        }
766
    }
767

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

779
            this._initializeCalendarContainer(e.componentRef.instance);
32✔
780
            this._collapsed = false;
32✔
781
            this.updateCalendar();
32✔
782
        });
783

784
        this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => {
32✔
785
            this.opened.emit({ owner: this });
24✔
786
        });
787

788
        this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e) => {
32✔
789
            this.handleClosing(e as OverlayCancelableEventArgs);
32✔
790
        });
791

792
        this._overlayService.closed.pipe(...this._overlaySubFilter).subscribe(() => {
32✔
793
            this._overlayService.detach(this._overlayId);
14✔
794
            this._collapsed = true;
14✔
795
            this._overlayId = null;
14✔
796
            this.closed.emit({ owner: this });
14✔
797
        });
798
    }
799

800
    private updateValue(value: DateRange) {
801
        this._value = value ? value : null;
131✔
802
        this.updateInputs();
131✔
803
        this.updateCalendar();
131✔
804
    }
805

806
    private updateValidityOnBlur() {
807
        this.onTouchCallback();
5✔
808
        if (this._ngControl) {
5✔
809
            if (this.hasProjectedInputs) {
3✔
810
                this.projectedInputs.forEach(i => {
3✔
811
                    if (!this._ngControl.valid) {
6!
812
                        i.updateInputValidity(IgxInputState.INVALID);
6✔
813
                    } else {
UNCOV
814
                        i.updateInputValidity(IgxInputState.INITIAL);
×
815
                    }
816
                });
817
            }
818

819
            if (this.inputDirective) {
3!
UNCOV
820
                if (!this._ngControl.valid) {
×
UNCOV
821
                    this.inputDirective.valid = IgxInputState.INVALID;
×
822
                } else {
UNCOV
823
                    this.inputDirective.valid = IgxInputState.INITIAL;
×
824
                }
825
            }
826
        }
827
    }
828

829
    private updateDisabledState() {
830
        if (this.hasProjectedInputs) {
126✔
831
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
35✔
832
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
70✔
833
            start.inputDirective.disabled = this.disabled;
35✔
834
            end.inputDirective.disabled = this.disabled;
35✔
835
            return;
35✔
836
        }
837
    }
838

839
    private setRequiredToInputs(): void {
840
        // workaround for igxInput setting required
841
        Promise.resolve().then(() => {
143✔
842
            const isRequired = this.required;
143✔
843
            if (this.inputGroup && this.inputGroup.isRequired !== isRequired) {
143✔
844
                this.inputGroup.isRequired = isRequired;
4✔
845
            } else if (this.hasProjectedInputs && this._ngControl) {
139✔
846
                this.projectedInputs.forEach(i => i.isRequired = isRequired);
196✔
847
            }
848
        });
849
    }
850

851
    private parseMinValue(value: string | Date): Date | null {
852
        let minValue: Date = parseDate(value);
55✔
853
        if (!minValue && this.hasProjectedInputs) {
55✔
854
            const start = this.projectedInputs.filter(i => i instanceof IgxDateRangeStartComponent)[0];
50✔
855
            if (start) {
25✔
856
                minValue = parseDate(start.dateTimeEditor.minValue);
25✔
857
            }
858
        }
859

860
        return minValue;
55✔
861
    }
862

863
    private parseMaxValue(value: string | Date): Date | null {
864
        let maxValue: Date = parseDate(value);
55✔
865
        if (!maxValue && this.projectedInputs) {
55✔
866
            const end = this.projectedInputs.filter(i => i instanceof IgxDateRangeEndComponent)[0];
53✔
867
            if (end) {
53✔
868
                maxValue = parseDate(end.dateTimeEditor.maxValue);
25✔
869
            }
870
        }
871

872
        return maxValue;
55✔
873
    }
874

875
    private updateCalendar(): void {
876
        if (!this.calendar) {
165✔
877
            return;
110✔
878
        }
879
        this.calendar.disabledDates = [];
55✔
880
        const minValue = this.parseMinValue(this.minValue);
55✔
881
        if (minValue) {
55✔
882
            this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] });
2✔
883
        }
884
        const maxValue = this.parseMaxValue(this.maxValue);
55✔
885
        if (maxValue) {
55✔
886
            this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] });
2✔
887
        }
888

889
        const range: Date[] = [];
55✔
890
        if (this.value?.start && this.value?.end) {
55✔
891
            const _value = this.toRangeOfDates(this.value);
22✔
892
            if (DateTimeUtil.greaterThanMaxValue(_value.start, _value.end)) {
22!
893
                this.swapEditorDates();
×
894
            }
895
            if (this.valueInRange(this.value, minValue, maxValue)) {
22✔
896
                range.push(_value.start, _value.end);
22✔
897
            }
898
        }
899

900
        if (range.length > 0) {
55✔
901
            this.calendar.selectDate(range);
22✔
902
        } else if (range.length === 0 && this.calendar.monthViews) {
33!
UNCOV
903
            this.calendar.deselectDate();
×
904
        }
905
        this.calendar.viewDate = range[0] || new Date();
55✔
906
    }
907

908
    private swapEditorDates(): void {
UNCOV
909
        if (this.hasProjectedInputs) {
×
UNCOV
910
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
×
UNCOV
911
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
×
UNCOV
912
            [start.dateTimeEditor.value, end.dateTimeEditor.value] = [end.dateTimeEditor.value, start.dateTimeEditor.value];
×
UNCOV
913
            [this.value.start, this.value.end] = [this.value.end, this.value.start];
×
914
        }
915
    }
916

917
    private valueInRange(value: DateRange, minValue?: Date, maxValue?: Date): boolean {
918
        const _value = this.toRangeOfDates(value);
22✔
919
        if (minValue && DateTimeUtil.lessThanMinValue(_value.start, minValue, false)) {
22!
UNCOV
920
            return false;
×
921
        }
922
        if (maxValue && DateTimeUtil.greaterThanMaxValue(_value.end, maxValue, false)) {
22!
UNCOV
923
            return false;
×
924
        }
925

926
        return true;
22✔
927
    }
928

929
    private extractRange(selection: Date[]): DateRange {
930
        return {
37✔
931
            start: selection[0] || null,
37!
932
            end: selection.length > 0 ? selection[selection.length - 1] : null
37!
933
        };
934
    }
935

936
    private toRangeOfDates(range: DateRange): { start: Date; end: Date } {
937
        let start;
938
        let end;
939
        if (!isDate(range.start)) {
87✔
940
            start = DateTimeUtil.parseIsoDate(range.start);
1✔
941
        }
942
        if (!isDate(range.end)) {
87✔
943
            end = DateTimeUtil.parseIsoDate(range.end);
2✔
944
        }
945

946
        if (start || end) {
87!
UNCOV
947
            return { start, end };
×
948
        }
949

950
        return { start: range.start as Date, end: range.end as Date };
87✔
951
    }
952

953
    private subscribeToDateEditorEvents(): void {
954
        if (this.hasProjectedInputs) {
66✔
955
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
30✔
956
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
60✔
957
            if (start && end) {
30✔
958
                start.dateTimeEditor.valueChange
30✔
959
                    .pipe(takeUntil(this._destroy$))
960
                    .subscribe(value => {
961
                        if (this.value) {
1!
UNCOV
962
                            this.value = { start: value, end: this.value.end };
×
963
                        } else {
964
                            this.value = { start: value, end: null };
1✔
965
                        }
966
                    });
967
                end.dateTimeEditor.valueChange
30✔
968
                    .pipe(takeUntil(this._destroy$))
969
                    .subscribe(value => {
970
                        if (this.value) {
1!
971
                            this.value = { start: this.value.start, end: value as Date };
1✔
972
                        } else {
UNCOV
973
                            this.value = { start: null, end: value as Date };
×
974
                        }
975
                    });
976
            }
977
        }
978
    }
979

980
    private attachOnTouched(): void {
981
        if (this.hasProjectedInputs) {
66✔
982
            this.projectedInputs.forEach(i => {
30✔
983
                fromEvent(i.dateTimeEditor.nativeElement, 'blur')
60✔
984
                    .pipe(takeUntil(this._destroy$))
985
                    .subscribe(() => {
986
                        if (this.collapsed) {
1✔
987
                            this.updateValidityOnBlur();
1✔
988
                        }
989
                    });
990
            });
991
        } else {
992
            fromEvent(this.inputDirective.nativeElement, 'blur')
36✔
993
                .pipe(takeUntil(this._destroy$))
994
                .subscribe(() => {
995
                    if (this.collapsed) {
10!
UNCOV
996
                        this.updateValidityOnBlur();
×
997
                    }
998
                });
999
        }
1000
    }
1001

1002
    private cacheFocusedInput(): void {
1003
        if (this.hasProjectedInputs) {
66✔
1004
            this.projectedInputs.forEach(i => {
30✔
1005
                fromEvent(i.dateTimeEditor.nativeElement, 'focus')
60✔
1006
                    .pipe(takeUntil(this._destroy$))
1007
                    .subscribe(() => this._focusedInput = i);
4✔
1008
            });
1009
        }
1010
    }
1011

1012
    private configPositionStrategy(): void {
1013
        this._positionSettings = {
66✔
1014
            openAnimation: fadeIn,
1015
            closeAnimation: fadeOut
1016
        };
1017
        this._dropDownOverlaySettings.positionStrategy = new AutoPositionStrategy(this._positionSettings);
66✔
1018
        this._dropDownOverlaySettings.target = this.element.nativeElement;
66✔
1019
    }
1020

1021
    private configOverlaySettings(): void {
1022
        if (this.overlaySettings !== null) {
66✔
1023
            this._dropDownOverlaySettings = Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings);
66✔
1024
            this._dialogOverlaySettings = Object.assign({}, this._dialogOverlaySettings, this.overlaySettings);
66✔
1025
        }
1026
    }
1027

1028
    private initialSetValue() {
1029
        // if there is no value and no ngControl on the picker but we have inputs we may have value set through
1030
        // their ngModels - we should generate our initial control value
1031
        if ((!this.value || (!this.value.start && !this.value.end)) && this.hasProjectedInputs && !this._ngControl) {
66!
1032
            const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent);
2✔
1033
            const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent);
4✔
1034
            this._value = {
2✔
1035
                start: start.dateTimeEditor.value as Date,
1036
                end: end.dateTimeEditor.value as Date
1037
            };
1038
        }
1039
    }
1040

1041
    private updateInputs(): void {
1042
        const start = this.projectedInputs?.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent;
197✔
1043
        const end = this.projectedInputs?.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent;
197✔
1044
        if (start && end) {
197✔
1045
            const _value = this.value ? this.toRangeOfDates(this.value) : null;
96✔
1046
            start.updateInputValue(_value?.start || null);
96✔
1047
            end.updateInputValue(_value?.end || null);
96✔
1048
        }
1049
    }
1050

1051
    private updateDisplayFormat(): void {
1052
        this.projectedInputs.forEach(i => {
80✔
1053
            const input = i as IgxDateRangeInputsBaseComponent;
88✔
1054
            input.dateTimeEditor.displayFormat = this.displayFormat;
88✔
1055
        });
1056
    }
1057

1058
    private updateInputFormat(): void {
1059
        this.projectedInputs.forEach(i => {
70✔
1060
            const input = i as IgxDateRangeInputsBaseComponent;
68✔
1061
            if (input.dateTimeEditor.inputFormat !== this.inputFormat) {
68✔
1062
                input.dateTimeEditor.inputFormat = this.inputFormat;
68✔
1063
            }
1064
        });
1065
    }
1066

1067
    private updateInputLocale(): void {
1068
        this.projectedInputs.forEach(i => {
3✔
1069
            const input = i as IgxDateRangeInputsBaseComponent;
6✔
1070
            input.dateTimeEditor.locale = this.locale;
6✔
1071
        });
1072
    }
1073

1074
    private _initializeCalendarContainer(componentInstance: IgxCalendarContainerComponent) {
1075
        this._calendar = componentInstance.calendar;
32✔
1076
        this.calendar.hasHeader = false;
32✔
1077
        this.calendar.locale = this.locale;
32✔
1078
        this.calendar.selection = CalendarSelection.RANGE;
32✔
1079
        this.calendar.weekStart = this.weekStart;
32✔
1080
        this.calendar.hideOutsideDays = this.hideOutsideDays;
32✔
1081
        this.calendar.monthsViewNumber = this.displayMonthsCount;
32✔
1082
        this.calendar.showWeekNumbers = this.showWeekNumbers;
32✔
1083
        this.calendar.selected.pipe(takeUntil(this._destroy$)).subscribe((ev: Date[]) => this.handleSelection(ev));
32✔
1084

1085
        componentInstance.mode = this.mode;
32✔
1086
        componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null;
32✔
1087
        componentInstance.pickerActions = this.pickerActions;
32✔
1088
        componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close());
32✔
1089
    }
1090
}
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