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

IgniteUI / igniteui-angular / 6653488239

26 Oct 2023 11:33AM UTC coverage: 92.101% (-0.1%) from 92.206%
6653488239

push

github

web-flow
Merge pull request #13451 from IgniteUI/bundle-test-extended

refactor(i18n, util): tree shaking i18n

15273 of 17962 branches covered (0.0%)

45 of 45 new or added lines in 24 files covered. (100.0%)

26410 of 28675 relevant lines covered (92.1%)

30213.01 hits per line

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

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

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

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

40
const SingleInputDatesConcatenationString = ' - ';
41

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

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

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

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

132
    /**
133
     * The default text of the calendar dialog `done` button.
1,316✔
134
     *
135
     * @remarks
136
     * Default value is `Done`.
137
     * An @Input property that renders Done button with custom text. By default `doneButtonText` is set to Done.
240✔
138
     * The button will only show up in `dialog` mode.
139
     *
140
     * @example
135✔
141
     * ```html
98✔
142
     * <igx-date-range-picker doneButtonText="完了"></igx-date-range-picker>
98!
143
     * ```
144
     */
37✔
145
    @Input()
146
    public set doneButtonText(value: string) {
147
        this._doneButtonText = value;
597✔
148
    }
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

181✔
303
    /** @hidden @internal */
41✔
304
    @ViewChild(IgxInputDirective)
38✔
305
    public inputDirective: IgxInputDirective;
76✔
306

38!
307
    /** @hidden @internal */
×
308
    @ContentChildren(IgxDateRangeInputsBaseComponent)
309
    public projectedInputs: QueryList<IgxDateRangeInputsBaseComponent>;
38✔
310

1✔
311
    @ContentChild(IgxLabelDirective)
312
    public label: IgxLabelDirective;
313

41✔
314
    @ContentChild(IgxPickerActionsDirective)
41✔
315
    public pickerActions: IgxPickerActionsDirective;
41✔
316

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

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

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

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

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

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

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

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

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

384
    /** @hidden @internal */
385
    public get separatorClass(): string {
53✔
386
        return this.getComponentDensityClass('igx-date-range-picker__label');
53✔
387
    }
28✔
388

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

395
        return false;
×
396
    }
397

398
    private get calendar(): IgxCalendarComponent {
135✔
399
        return this._calendar;
125✔
400
    }
2✔
401

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

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

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

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

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

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

5✔
484
        const settings = Object.assign({}, this.isDropdown
5✔
485
            ? this.dropdownOverlaySettings
2!
486
            : this.dialogOverlaySettings
2✔
487
            , overlaySettings);
4!
488

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

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

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

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

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

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

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

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

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

×
592
        return Object.keys(errors).length > 0 ? errors : null;
593
    }
22✔
594

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

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

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

2✔
609
        this.locale = this.locale || this._localeId;
610
        super.ngOnInit();
84!
611
    }
×
612

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

622
        this.setRequiredToInputs();
623

1!
624
        if (this._ngControl) {
×
625
            this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this));
626
        }
627

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

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

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

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

675
    protected onStatusChanged = () => {
63✔
676
        if (this.inputGroup) {
677
            this.setValidityState(this.inputDirective, this.inputGroup.isFocused);
678
        } else if (this.hasProjectedInputs) {
679
            this.projectedInputs
63✔
680
                .forEach((i) => {
63✔
681
                    this.setValidityState(i.inputDirective, i.isFocused);
682
                });
683
        }
63!
684
        this.setRequiredToInputs();
63✔
685
    };
63✔
686

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

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

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

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

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

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

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

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

758
        this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => {
759
            this.calendar?.daysView?.focusActiveDate();
760
            this.opened.emit({ owner: this });
761
        });
762

763
        this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e) => {
764
            this.handleClosing(e as OverlayCancelableEventArgs);
765
        });
766

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

2✔
775
    private updateValue(value: DateRange) {
776
        this._value = value ? value : null;
777
        this.updateInputs();
778
        this.updateCalendar();
779
    }
780

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

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

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

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

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

835
        return minValue;
836
    }
837

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

847
        return maxValue;
848
    }
849

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

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

875
        if (range.length > 0) {
876
            this.calendar.selectDate(range);
877
        } else if (range.length === 0 && this.calendar.monthViews) {
878
            this.calendar.deselectDate();
879
        }
880
        this.calendar.viewDate = range[0] || new Date();
881
    }
882

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

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

901
        return true;
902
    }
903

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

911
    private toRangeOfDates(range: DateRange): { start: Date; end: Date } {
912
        let start;
913
        let end;
914
        if (!isDate(range.start)) {
915
            start = DateTimeUtil.parseIsoDate(range.start);
916
        }
917
        if (!isDate(range.end)) {
918
            end = DateTimeUtil.parseIsoDate(range.end);
919
        }
920

921
        if (start || end) {
922
            return { start, end };
923
        }
924

925
        return { start: range.start as Date, end: range.end as Date };
926
    }
927

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

955
    private attachOnTouched(): void {
956
        if (this.hasProjectedInputs) {
957
            this.projectedInputs.forEach(i => {
958
                fromEvent(i.dateTimeEditor.nativeElement, 'blur')
959
                    .pipe(takeUntil(this._destroy$))
960
                    .subscribe(() => {
961
                        if (this.collapsed) {
962
                            this.updateValidityOnBlur();
963
                        }
964
                    });
965
            });
966
        } else {
967
            fromEvent(this.inputDirective.nativeElement, 'blur')
968
                .pipe(takeUntil(this._destroy$))
969
                .subscribe(() => {
970
                    if (this.collapsed) {
971
                        this.updateValidityOnBlur();
972
                    }
973
                });
974
        }
975
    }
976

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

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

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

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

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

1026
    private updateDisplayFormat(): void {
1027
        this.projectedInputs.forEach(i => {
1028
            const input = i as IgxDateRangeInputsBaseComponent;
1029
            input.dateTimeEditor.displayFormat = this.displayFormat;
1030
        });
1031
    }
1032

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

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

1053
        componentInstance.mode = this.mode;
1054
        componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null;
1055
        componentInstance.pickerActions = this.pickerActions;
1056
        componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close());
1057
    }
1058
}
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