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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

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

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

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

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

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

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

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

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

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

202
    public get minValue(): Date | string {
UNCOV
203
        return this._minValue;
×
204
    }
205

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

218
    public get maxValue(): Date | string {
UNCOV
219
        return this._maxValue;
×
220
    }
221

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
426
        return false;
×
427
    }
428

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
652
        this.setRequiredToInputs();
×
653

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
868
        return minValue;
×
869
    }
870

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

UNCOV
880
        return maxValue;
×
881
    }
882

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

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

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

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

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

UNCOV
934
        return true;
×
935
    }
936

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc