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

atinc / ngx-tethys / 0b9df710-60cf-468d-b3fe-08c55363086b

25 Mar 2025 08:15AM UTC coverage: 90.183% (-0.04%) from 90.223%
0b9df710-60cf-468d-b3fe-08c55363086b

push

circleci

web-flow
feat(date-picker): support date-fns i18n and support thySeparator (#3313)

5588 of 6859 branches covered (81.47%)

Branch coverage included in aggregate %.

25 of 32 new or added lines in 7 files covered. (78.13%)

37 existing lines in 7 files now uncovered.

13346 of 14136 relevant lines covered (94.41%)

992.1 hits per line

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

95.27
/src/date-picker/picker.component.ts
1
import { getFlexiblePositions, ThyPlacement } from 'ngx-tethys/core';
2
import { coerceBooleanProperty, TinyDate } from 'ngx-tethys/util';
3

4
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
5
import {
6
    AfterViewInit,
7
    ChangeDetectionStrategy,
8
    ChangeDetectorRef,
9
    Component,
10
    ElementRef,
11
    EventEmitter,
12
    inject,
13
    Input,
14
    OnChanges,
15
    Output,
16
    SimpleChanges,
17
    ViewChild
1✔
18
} from '@angular/core';
19

159✔
20
import { AsyncPipe, NgClass, NgTemplateOutlet } from '@angular/common';
159✔
21
import { scaleMotion, scaleXMotion, scaleYMotion } from 'ngx-tethys/core';
159✔
22
import { ThyIcon } from 'ngx-tethys/icon';
159✔
23
import { ThyInputDirective } from 'ngx-tethys/input';
159✔
24
import { ThyEnterDirective } from 'ngx-tethys/shared';
159✔
25
import { DateHelperService } from './date-helper.service';
159✔
26
import { CompatibleValue, RangePartType } from './inner-types';
159✔
27
import { getFlexibleAdvancedReadableValue } from './picker.util';
159✔
28
import { ThyDateGranularity } from './standard-types';
159✔
29

159✔
30
/**
159✔
31
 * @private
159✔
32
 */
159✔
33
@Component({
159✔
34
    selector: 'thy-picker',
35
    exportAs: 'thyPicker',
36
    templateUrl: './picker.component.html',
×
37
    changeDetection: ChangeDetectionStrategy.OnPush,
38
    standalone: true,
39
    imports: [CdkOverlayOrigin, ThyInputDirective, ThyEnterDirective, AsyncPipe, NgTemplateOutlet, ThyIcon, NgClass, CdkConnectedOverlay],
161✔
40
    animations: [scaleXMotion, scaleYMotion, scaleMotion]
161✔
41
})
42
export class ThyPicker implements OnChanges, AfterViewInit {
43
    private changeDetector = inject(ChangeDetectorRef);
×
44
    private dateHelper = inject(DateHelperService);
45

46
    @Input() isRange = false;
165✔
47
    @Input() open: boolean | undefined = undefined;
165✔
48
    @Input() disabled: boolean;
49
    @Input() placeholder: string | string[];
50
    @Input() readonly: boolean;
656✔
51
    @Input() allowClear: boolean;
52
    @Input() autoFocus: boolean;
53
    @Input() className: string;
320✔
54
    @Input() size: 'sm' | 'xs' | 'lg' | 'md' | 'default';
320✔
55
    @Input() suffixIcon: string;
312✔
56
    @Input() placement: ThyPlacement = 'bottomLeft';
57
    @Input() flexible: boolean = false;
58
    @Input() mode: string;
59
    @Input({ transform: coerceBooleanProperty }) hasBackdrop: boolean;
60
    @Input() separator: string;
1,561✔
61
    @Output() blur = new EventEmitter<Event>();
62
    @Output() readonly valueChange = new EventEmitter<TinyDate | TinyDate[] | null>();
63
    @Output() readonly openChange = new EventEmitter<boolean>(); // Emitted when overlay's open state change
665✔
64
    @Output() readonly inputChange = new EventEmitter<string>();
65

66
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
67
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
337✔
68
    @ViewChild('pickerInput', { static: true }) pickerInput: ElementRef;
8✔
69
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
5✔
70

71
    @Input()
72
    get format() {
3✔
73
        return this.innerFormat;
74
    }
75

76
    set format(value: string) {
77
        this.innerFormat = value;
159✔
78
        this.updateReadableDate(this.innerValue);
159✔
79
    }
1✔
80

81
    @Input()
82
    get flexibleDateGranularity() {
83
        return this.innerflexibleDateGranularity;
59✔
84
    }
85

86
    set flexibleDateGranularity(granularity: ThyDateGranularity) {
5✔
87
        this.innerflexibleDateGranularity = granularity;
5!
88
        this.updateReadableDate(this.innerValue);
5✔
89
    }
90

5✔
91
    @Input()
92
    get value() {
93
        return this.innerValue;
17✔
94
    }
17✔
95

17✔
96
    set value(value: TinyDate | TinyDate[] | null) {
97
        this.innerValue = value;
98
        if (!this.entering) {
8!
UNCOV
99
            this.updateReadableDate(this.innerValue);
×
100
        }
101
    }
8!
102

8✔
103
    private innerflexibleDateGranularity: ThyDateGranularity;
104

105
    private innerFormat: string;
125✔
106

124✔
107
    private innerValue: TinyDate | TinyDate[] | null;
124✔
108

124✔
109
    entering = false;
124✔
110

124!
111
    prefixCls = 'thy-calendar';
124✔
112

113
    isShowDatePopup = false;
114

115
    overlayOpen = false; // Available when "open"=undefined
116

117
    overlayPositions = getFlexiblePositions(this.placement, 4);
111✔
118

57✔
119
    get realOpenState(): boolean {
57✔
120
        // The value that really decide the open state of overlay
57✔
121
        return this.isOpenHandledByUser() ? !!this.open : this.overlayOpen;
57✔
122
    }
123

124
    get readonlyState(): boolean {
125
        return this.isRange || this.readonly || this.mode !== 'date';
129✔
126
    }
129✔
127

128
    ngOnChanges(changes: SimpleChanges): void {
129
        // open by user
130
        if (changes.open && changes.open.currentValue !== undefined) {
60✔
131
            if (changes.open.currentValue) {
60✔
132
                this.showDatePopup();
60✔
133
            } else {
134
                this.closeDatePopup();
135
            }
136
        }
128✔
137
    }
125✔
138

139
    ngAfterViewInit(): void {
140
        this.overlayPositions = getFlexiblePositions(this.placement, 4);
141
        if (this.autoFocus) {
12✔
142
            this.focus();
143
        }
144
    }
52✔
145

146
    focus(): void {
147
        this.pickerInput.nativeElement.focus();
128✔
148
    }
149

150
    onBlur(event: FocusEvent) {
6✔
151
        this.blur.emit(event);
6✔
152
        if (this.entering) {
6✔
153
            this.valueChange.emit(this.pickerInput.nativeElement.value);
6✔
154
        }
155
        this.entering = false;
UNCOV
156
    }
×
157

158
    onInput(event: InputEvent) {
159
        this.entering = true;
656✔
160
        const inputValue = (event.target as HTMLElement)['value'];
259✔
161
        this.inputChange.emit(inputValue);
162
    }
397✔
163

249✔
164
    onEnter() {
165
        if (this.readonlyState) {
166
            return;
148✔
167
        }
168
        this.valueChange.emit(this.pickerInput.nativeElement.value || this.getReadableValue(new TinyDate()));
169
        this.entering = false;
170
    }
171

1,689✔
172
    showOverlay(): void {
173
        if (!this.realOpenState) {
174
            this.overlayOpen = true;
175
            this.showDatePopup();
687✔
176

264✔
177
            this.openChange.emit(this.overlayOpen);
46✔
178
            setTimeout(() => {
179
                if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
180
                    this.cdkConnectedOverlay.overlayRef.updatePosition();
218✔
181
                }
218✔
182
            });
218✔
183
        }
184
    }
185

186
    hideOverlay(): void {
423✔
187
        if (this.realOpenState) {
423✔
188
            this.overlayOpen = false;
189
            this.closeDatePopup();
190

191
            this.openChange.emit(this.overlayOpen);
192
            this.focus();
193
        }
205✔
194
    }
13✔
195

196
    showDatePopup() {
197
        this.isShowDatePopup = true;
192✔
198
        this.changeDetector.markForCheck();
199
    }
200

201
    closeDatePopup() {
656✔
202
        // Delay 200ms before destroying the date-popup, otherwise you will not see the closing animation.
203
        setTimeout(() => {
204
            this.isShowDatePopup = false;
205
            this.changeDetector.markForCheck();
206
        }, 200);
638✔
207
    }
638✔
208

40✔
209
    onClickInputBox(): void {
210
        if (!this.disabled && !this.readonly && !this.isOpenHandledByUser()) {
598✔
211
            this.showOverlay();
212
        }
1✔
213
    }
214

215
    onClickBackdrop(): void {
216
        this.hideOverlay();
217
    }
218

219
    onOverlayDetach(): void {
220
        this.hideOverlay();
221
    }
222

223
    onPositionChange(position: ConnectedOverlayPositionChange): void {
224
        this.changeDetector.detectChanges();
225
    }
226

227
    onClickClear(event: MouseEvent): void {
228
        event.preventDefault();
229
        event.stopPropagation();
230

231
        this.innerValue = this.isRange ? [] : null;
232
        this.valueChange.emit(this.innerValue);
233
    }
234

235
    getPartTypeIndex(partType: RangePartType): number {
236
        return { left: 0, right: 1 }[partType];
237
    }
238

239
    isEmptyValue(value: CompatibleValue | null): boolean {
240
        if (value === null) {
241
            return true;
1✔
242
        } else if (this.isRange) {
243
            return !value || !Array.isArray(value) || value.every(val => !val);
244
        } else {
245
            return !value;
246
        }
247
    }
248

249
    // Whether open state is permanently controlled by user himself
250
    isOpenHandledByUser(): boolean {
251
        return this.open !== undefined;
252
    }
253

254
    getReadableValue(tinyDate: TinyDate | TinyDate[]): string | null {
255
        let value: TinyDate;
256
        if (this.isRange) {
257
            if (this.flexible && this.innerflexibleDateGranularity !== 'day') {
258
                return getFlexibleAdvancedReadableValue(tinyDate as TinyDate[], this.innerflexibleDateGranularity, this.separator);
259
            } else {
260
                const start = tinyDate[0] ? this.formatDate(tinyDate[0]) : '';
261
                const end = tinyDate[1] ? this.formatDate(tinyDate[1]) : '';
262
                return start && end ? `${start}${this.separator}${end}` : null;
263
            }
264
        } else {
265
            value = tinyDate as TinyDate;
266
            return value ? this.formatDate(value) : null;
267
        }
268
    }
269

270
    formatDate(value: TinyDate) {
271
        // dateHelper.format() 使用的是 angular 的 format,不支持季度,修改的话,改动比较大。
272
        // 此处通过对 innerFormat 做下判断,如果是季度的 format,使用 date-fns 的 format()
273
        if (this.innerFormat && (this.innerFormat.includes('q') || this.innerFormat.includes('Q'))) {
274
            return value.format(this.innerFormat);
275
        } else {
276
            return this.dateHelper.format(value.nativeDate, this.innerFormat);
277
        }
278
    }
279

280
    getPlaceholder(): string {
281
        return this.isRange && this.placeholder && Array.isArray(this.placeholder)
282
            ? (this.placeholder as string[]).join(this.separator)
283
            : (this.placeholder as string);
284
    }
285

286
    private updateReadableDate(setValue: TinyDate | TinyDate[] | null) {
287
        const readableValue = this.getReadableValue(setValue);
288
        if (readableValue === this.pickerInput.nativeElement['value']) {
289
            return;
290
        }
291

292
        this.pickerInput.nativeElement.value = readableValue;
293
    }
294
}
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