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

atinc / ngx-tethys / 13fcf11d-0958-4626-8dcf-f200b8133961

14 Jun 2024 10:13AM UTC coverage: 90.422%. Remained the same
13fcf11d-0958-4626-8dcf-f200b8133961

push

circleci

web-flow
feat: use the ngx-tethys/util's coerceBooleanProperty instead of booleanAttribute #INFR-12648 (#3106)

5467 of 6692 branches covered (81.69%)

Branch coverage included in aggregate %.

117 of 120 new or added lines in 66 files covered. (97.5%)

183 existing lines in 46 files now uncovered.

13216 of 13970 relevant lines covered (94.6%)

985.9 hits per line

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

95.29
/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
    Input,
13
    OnChanges,
14
    Output,
15
    SimpleChanges,
16
    ViewChild
17
} from '@angular/core';
1✔
18

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

29
/**
163✔
30
 * @private
163✔
31
 */
32
@Component({
33
    selector: 'thy-picker',
642✔
34
    exportAs: 'thyPicker',
35
    templateUrl: './picker.component.html',
36
    changeDetection: ChangeDetectionStrategy.OnPush,
314✔
37
    standalone: true,
314✔
38
    imports: [
306✔
39
        CdkOverlayOrigin,
40
        ThyInputDirective,
41
        ThyEnterDirective,
42
        AsyncPipe,
43
        NgTemplateOutlet,
885✔
44
        NgIf,
45
        ThyIcon,
46
        NgClass,
651✔
47
        CdkConnectedOverlay
48
    ],
49
    animations: [scaleXMotion, scaleYMotion, scaleMotion]
157✔
50
})
157✔
51
export class ThyPicker implements OnChanges, AfterViewInit {
157✔
52
    @Input() isRange = false;
157✔
53
    @Input() open: boolean | undefined = undefined;
157✔
54
    @Input() disabled: boolean;
157✔
55
    @Input() placeholder: string | string[];
157✔
56
    @Input() readonly: boolean;
157✔
57
    @Input() allowClear: boolean;
157✔
58
    @Input() autoFocus: boolean;
157✔
59
    @Input() className: string;
157✔
60
    @Input() size: 'sm' | 'xs' | 'lg' | 'md' | 'default';
157✔
61
    @Input() suffixIcon: string;
157✔
62
    @Input() placement: ThyPlacement = 'bottomLeft';
157✔
63
    @Input() flexible: boolean = false;
157✔
64
    @Input() mode: string;
65
    @Input({ transform: coerceBooleanProperty }) hasBackdrop: boolean;
66
    @Output() blur = new EventEmitter<Event>();
67
    @Output() readonly valueChange = new EventEmitter<TinyDate | TinyDate[] | null>();
329✔
68
    @Output() readonly openChange = new EventEmitter<boolean>(); // Emitted when overlay's open state change
8✔
69
    @Output() readonly inputChange = new EventEmitter<string>();
5✔
70

71
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
72
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
3✔
73
    @ViewChild('pickerInput', { static: true }) pickerInput: ElementRef;
74
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
75

76
    @Input()
77
    get format() {
157✔
78
        return this.innerFormat;
157✔
79
    }
1✔
80

81
    set format(value: string) {
82
        this.innerFormat = value;
83
        this.updateReadableDate(this.innerValue);
57✔
84
    }
85

86
    @Input()
5✔
87
    get flexibleDateGranularity() {
5!
88
        return this.innerflexibleDateGranularity;
5✔
89
    }
90

5✔
91
    set flexibleDateGranularity(granularity: ThyDateGranularity) {
92
        this.innerflexibleDateGranularity = granularity;
93
        this.updateReadableDate(this.innerValue);
17✔
94
    }
17✔
95

17✔
96
    @Input()
97
    get value() {
98
        return this.innerValue;
8!
UNCOV
99
    }
×
100

101
    set value(value: TinyDate | TinyDate[] | null) {
8!
102
        this.innerValue = value;
8✔
103
        if (!this.entering) {
104
            this.updateReadableDate(this.innerValue);
105
        }
123✔
106
    }
122✔
107

122✔
108
    private innerflexibleDateGranularity: ThyDateGranularity;
122✔
109

122✔
110
    private innerFormat: string;
122!
111

122✔
112
    private innerValue: TinyDate | TinyDate[] | null;
113

114
    entering = false;
115

116
    prefixCls = 'thy-calendar';
117

107✔
118
    isShowDatePopup = false;
55✔
119

55✔
120
    overlayOpen = false; // Available when "open"=undefined
55✔
121

55✔
122
    overlayPositions = getFlexiblePositions(this.placement, 4);
123

124
    get realOpenState(): boolean {
125
        // The value that really decide the open state of overlay
127✔
126
        return this.isOpenHandledByUser() ? !!this.open : this.overlayOpen;
127✔
127
    }
128

129
    get readonlyState(): boolean {
130
        return this.isRange || this.readonly || this.mode !== 'date';
58✔
131
    }
58✔
132

58✔
133
    constructor(private changeDetector: ChangeDetectorRef, private dateHelper: DateHelperService) {}
134

135
    ngOnChanges(changes: SimpleChanges): void {
136
        // open by user
126✔
137
        if (changes.open && changes.open.currentValue !== undefined) {
123✔
138
            if (changes.open.currentValue) {
139
                this.showDatePopup();
140
            } else {
141
                this.closeDatePopup();
12✔
142
            }
143
        }
144
    }
50✔
145

146
    ngAfterViewInit(): void {
147
        this.overlayPositions = getFlexiblePositions(this.placement, 4);
126✔
148
        if (this.autoFocus) {
149
            this.focus();
150
        }
6✔
151
    }
6✔
152

6✔
153
    focus(): void {
6✔
154
        this.pickerInput.nativeElement.focus();
155
    }
UNCOV
156

×
157
    onBlur(event: FocusEvent) {
158
        this.blur.emit(event);
159
        if (this.entering) {
642✔
160
            this.valueChange.emit(this.pickerInput.nativeElement.value);
259✔
161
        }
162
        this.entering = false;
383✔
163
    }
235✔
164

165
    onInput(event: InputEvent) {
166
        this.entering = true;
148✔
167
        const inputValue = (event.target as HTMLElement)['value'];
168
        this.inputChange.emit(inputValue);
169
    }
170

171
    onEnter() {
1,011✔
172
        if (this.readonlyState) {
173
            return;
174
        }
175
        this.valueChange.emit(this.pickerInput.nativeElement.value || this.getReadableValue(new TinyDate(new Date())));
675✔
176
        this.entering = false;
252✔
177
    }
46✔
178

179
    showOverlay(): void {
180
        if (!this.realOpenState) {
206✔
181
            this.overlayOpen = true;
206✔
182
            this.showDatePopup();
206✔
183

184
            this.openChange.emit(this.overlayOpen);
185
            setTimeout(() => {
186
                if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
423✔
187
                    this.cdkConnectedOverlay.overlayRef.updatePosition();
423✔
188
                }
189
            });
190
        }
191
    }
192

193
    hideOverlay(): void {
197✔
194
        if (this.realOpenState) {
13✔
195
            this.overlayOpen = false;
196
            this.closeDatePopup();
197

184✔
198
            this.openChange.emit(this.overlayOpen);
199
            this.focus();
200
        }
201
    }
642✔
202

203
    showDatePopup() {
204
        this.isShowDatePopup = true;
205
        this.changeDetector.markForCheck();
206
    }
628✔
207

628✔
208
    closeDatePopup() {
40✔
209
        // Delay 200ms before destroying the date-popup, otherwise you will not see the closing animation.
210
        setTimeout(() => {
588✔
211
            this.isShowDatePopup = false;
212
            this.changeDetector.markForCheck();
1✔
213
        }, 200);
214
    }
215

216
    onClickInputBox(): void {
1✔
217
        if (!this.disabled && !this.readonly && !this.isOpenHandledByUser()) {
218
            this.showOverlay();
219
        }
220
    }
221

222
    onClickBackdrop(): void {
223
        this.hideOverlay();
224
    }
225

226
    onOverlayDetach(): void {
227
        this.hideOverlay();
228
    }
229

230
    onPositionChange(position: ConnectedOverlayPositionChange): void {
231
        this.changeDetector.detectChanges();
232
    }
233

234
    onClickClear(event: MouseEvent): void {
235
        event.preventDefault();
236
        event.stopPropagation();
237

238
        this.innerValue = this.isRange ? [] : null;
239
        this.valueChange.emit(this.innerValue);
240
    }
241

242
    getPartTypeIndex(partType: RangePartType): number {
243
        return { left: 0, right: 1 }[partType];
244
    }
1✔
245

246
    isEmptyValue(value: CompatibleValue | null): boolean {
247
        if (value === null) {
248
            return true;
249
        } else if (this.isRange) {
250
            return !value || !Array.isArray(value) || value.every(val => !val);
251
        } else {
252
            return !value;
253
        }
254
    }
255

256
    // Whether open state is permanently controlled by user himself
257
    isOpenHandledByUser(): boolean {
258
        return this.open !== undefined;
259
    }
260

261
    getReadableValue(tinyDate: TinyDate | TinyDate[]): string | null {
262
        let value: TinyDate;
263
        if (this.isRange) {
264
            if (this.flexible && this.innerflexibleDateGranularity !== 'day') {
265
                return getFlexibleAdvancedReadableValue(tinyDate as TinyDate[], this.innerflexibleDateGranularity);
266
            } else {
267
                const start = tinyDate[0] ? this.formatDate(tinyDate[0]) : '';
268
                const end = tinyDate[1] ? this.formatDate(tinyDate[1]) : '';
269
                return start && end ? `${start} ~ ${end}` : null;
270
            }
271
        } else {
272
            value = tinyDate as TinyDate;
273
            return value ? this.formatDate(value) : null;
274
        }
275
    }
276

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

287
    getPlaceholder(): string {
288
        return this.isRange && this.placeholder && Array.isArray(this.placeholder)
289
            ? (this.placeholder as string[]).join(' ~ ')
290
            : (this.placeholder as string);
291
    }
292

293
    private updateReadableDate(setValue: TinyDate | TinyDate[] | null) {
294
        const readableValue = this.getReadableValue(setValue);
295
        if (readableValue === this.pickerInput.nativeElement['value']) {
296
            return;
297
        }
298

299
        this.pickerInput.nativeElement.value = readableValue;
300
    }
301
}
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