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

atinc / ngx-tethys / cd64db52-e563-41a3-85f3-a0adb87ce135

30 Oct 2024 08:03AM UTC coverage: 90.402% (-0.04%) from 90.438%
cd64db52-e563-41a3-85f3-a0adb87ce135

push

circleci

web-flow
refactor: refactor constructor to the inject function (#3222)

5503 of 6730 branches covered (81.77%)

Branch coverage included in aggregate %.

422 of 429 new or added lines in 170 files covered. (98.37%)

344 existing lines in 81 files now uncovered.

13184 of 13941 relevant lines covered (94.57%)

997.19 hits per line

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

89.89
/src/shared/select/select-control/select-control.component.ts
1
import { ThyTagSize } from 'ngx-tethys/tag';
2
import { coerceBooleanProperty, isUndefinedOrNull } from 'ngx-tethys/util';
3

4
import {
5
    ChangeDetectionStrategy,
6
    Component,
7
    ElementRef,
8
    EventEmitter,
9
    Input,
10
    OnInit,
11
    Output,
12
    Renderer2,
13
    TemplateRef,
14
    ViewChild,
1✔
15
    numberAttribute,
16
    inject
285✔
17
} from '@angular/core';
285✔
18
import { useHostRenderer } from '@tethys/cdk/dom';
285✔
19

285✔
20
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
285✔
21
import { FormsModule } from '@angular/forms';
285✔
22
import { ThyGridModule } from 'ngx-tethys/grid';
285✔
23
import { ThyIcon } from 'ngx-tethys/icon';
285✔
24
import { ThyTag } from 'ngx-tethys/tag';
285✔
25
import { SelectOptionBase } from '../../option/select-option-base';
285✔
26

285✔
27
export type SelectControlSize = 'xs' | 'sm' | 'md' | 'lg' | '';
285✔
28

285✔
29
/**
285✔
30
 * @private
285✔
31
 */
285✔
32
@Component({
285✔
33
    selector: 'thy-select-control,[thySelectControl]',
34
    templateUrl: './select-control.component.html',
UNCOV
35
    changeDetection: ChangeDetectionStrategy.OnPush,
×
36
    standalone: true,
37
    imports: [FormsModule, NgClass, NgStyle, ThyTag, NgTemplateOutlet, ThyIcon, ThyGridModule],
38
    host: {
461✔
39
        '[class.select-control-borderless]': 'thyBorderless'
461✔
40
    }
18✔
41
})
18✔
42
export class ThySelectControl implements OnInit {
43
    private renderer = inject(Renderer2);
44

461✔
45
    inputValue = '';
6✔
46

6✔
47
    isComposing = false;
48

49
    panelOpened = false;
461✔
50

51
    isMultiple = false;
UNCOV
52

×
53
    showSearch = false;
54

55
    disabled = false;
298✔
56

298✔
57
    size: SelectControlSize;
58

59
    selectedOptions: SelectOptionBase | SelectOptionBase[];
2,429✔
60

61
    searchInputControlClass: { [key: string]: boolean };
62

298✔
63
    tagSize: ThyTagSize;
298✔
64

65
    private hostRenderer = useHostRenderer();
66

2,293✔
67
    @Input({ transform: coerceBooleanProperty })
68
    get thyPanelOpened(): boolean {
69
        return this.panelOpened;
423✔
70
    }
423✔
71

423✔
72
    set thyPanelOpened(value: boolean) {
75✔
73
        this.panelOpened = value;
8✔
74
        if (this.panelOpened && this.thyShowSearch) {
75
            Promise.resolve(null).then(() => {
76
                this.inputElement.nativeElement.focus();
77
            });
348✔
78
        }
6✔
79
        if (!this.panelOpened && this.thyShowSearch) {
80
            Promise.resolve(null).then(() => {
81
                this.setInputValue('');
423✔
82
            });
423✔
83
        }
4✔
84
        this.setSelectControlClass();
3✔
85
    }
3✔
86

87
    @Input({ transform: coerceBooleanProperty })
88
    get thyIsMultiple(): boolean {
89
        return this.isMultiple;
4✔
90
    }
4!
91

4✔
92
    set thyIsMultiple(value: boolean) {
93
        this.isMultiple = value;
94
        this.setSelectControlClass();
95
    }
96

97
    @Input({ transform: coerceBooleanProperty })
671✔
98
    get thyShowSearch(): boolean {
99
        return this.showSearch;
100
    }
293✔
101

293✔
102
    set thyShowSearch(value: boolean) {
103
        this.showSearch = value;
104
        this.setSelectControlClass();
7,692✔
105
    }
106

107
    @Input()
289✔
108
    get thySelectedOptions(): SelectOptionBase | SelectOptionBase[] {
289✔
109
        return this.selectedOptions;
289✔
110
    }
4✔
111

112
    set thySelectedOptions(value: SelectOptionBase | SelectOptionBase[]) {
285✔
113
        let sameValue = false;
1✔
114
        const oldValue = this.selectedOptions;
115
        if (this.isMultiple) {
116
            if (oldValue instanceof Array && value instanceof Array && oldValue.length === value.length) {
284✔
117
                sameValue = value.every((option, index) => option.thyValue === oldValue[index].thyValue);
118
            }
119
        } else {
120
            if (oldValue && value) {
66✔
121
                sameValue = (oldValue as SelectOptionBase).thyValue === (value as SelectOptionBase).thyValue;
66✔
122
            }
8✔
123
        }
5✔
124
        this.selectedOptions = value;
125
        if (this.panelOpened && this.thyShowSearch) {
126
            if (!sameValue) {
3✔
127
                Promise.resolve(null).then(() => {
128
                    this.setInputValue('');
129
                });
130
            }
58✔
131
            //等待组件渲染好再聚焦
132
            setTimeout(() => {
66✔
133
                if (this.panelOpened) {
134
                    this.inputElement.nativeElement.focus();
135
                }
534✔
136
            }, 200);
534✔
137
        }
1✔
138
    }
139

534✔
140
    @Input({ transform: coerceBooleanProperty })
255✔
141
    get thyDisabled(): boolean {
142
        return this.disabled;
534✔
143
    }
19✔
144

145
    set thyDisabled(value: boolean) {
534✔
146
        this.disabled = value;
147
        this.setSelectControlClass();
148
    }
113✔
149

150
    @Input()
151
    customDisplayTemplate: TemplateRef<any>;
2✔
152

153
    @Input({ transform: coerceBooleanProperty })
154
    thyAllowClear = false;
191✔
155

1✔
156
    @Input()
157
    thyPlaceholder = '';
190✔
158

159
    @Input()
160
    get thySize(): SelectControlSize {
671✔
161
        return this.size;
162
    }
163

1,985✔
164
    set thySize(value: SelectControlSize) {
165
        this.size = value;
166
        this.setSelectControlClass();
167

284✔
168
        if (value === 'xs' || value === 'sm') {
169
            this.tagSize = 'sm';
170
        } else if (value === 'lg') {
1,923✔
171
            this.tagSize = 'lg';
1,923✔
172
        } else {
173
            this.tagSize = 'md';
174
        }
175
    }
176

177
    @Input({ transform: numberAttribute }) thyMaxTagCount = 0;
178

179
    @Input({ transform: coerceBooleanProperty }) thyBorderless = false;
180

181
    @Input() thyPreset: string = '';
1,923✔
182

1,923✔
183
    @Output()
184
    thyOnSearch = new EventEmitter<string>();
185

186
    @Output()
187
    public thyOnRemove = new EventEmitter<{ item: SelectOptionBase; $eventOrigin: Event }>();
188

189
    @Output()
190
    public thyOnClear = new EventEmitter<Event>();
27✔
191

21✔
192
    @Output()
21✔
193
    public thyOnBlur = new EventEmitter<Event>();
21✔
194

195
    @ViewChild('inputElement')
196
    inputElement: ElementRef;
UNCOV
197

×
UNCOV
198
    get selectedValueStyle(): { [key: string]: string } {
×
199
        let showSelectedValue = false;
200
        if (this.showSearch) {
×
201
            if (this.panelOpened) {
×
UNCOV
202
                showSelectedValue = !(this.isComposing || this.inputValue);
×
203
            } else {
204
                showSelectedValue = true;
205
            }
206
        } else {
207
            showSelectedValue = true;
39✔
208
        }
2!
209
        return { display: showSelectedValue ? 'flex' : 'none' };
2✔
210
    }
211

UNCOV
212
    get placeholderStyle(): { [key: string]: string } {
×
213
        let placeholder = true;
214
        if (this.isSelectedValue) {
215
            placeholder = false;
216
        }
217
        if (!this.thyPlaceholder) {
6✔
218
            placeholder = false;
219
        }
220
        if (this.isComposing || this.inputValue) {
6✔
221
            placeholder = false;
222
        }
223
        return { display: placeholder ? 'block' : 'none' };
216✔
224
    }
225

226
    get selectedValue(): any {
1✔
227
        return this.thySelectedOptions;
228
    }
1✔
229

230
    get multipleSelectedValue(): any {
231
        return this.thySelectedOptions;
232
    }
233

234
    get maxSelectedTags() {
235
        if (this.thyMaxTagCount > 0 && this.thySelectedOptions instanceof Array && this.thySelectedOptions.length > this.thyMaxTagCount) {
236
            return this.thySelectedOptions.slice(0, this.thyMaxTagCount - 1);
237
        }
238
        return this.thySelectedOptions as SelectOptionBase[];
239
    }
240

241
    get showClearIcon(): boolean {
242
        return this.thyAllowClear && this.isSelectedValue;
243
    }
244

245
    get isSelectedValue(): boolean {
246
        return (
247
            (!this.isMultiple && !isUndefinedOrNull(this.thySelectedOptions)) ||
248
            (this.isMultiple && (<SelectOptionBase[]>this.thySelectedOptions).length > 0)
1✔
249
        );
250
    }
251

252
    ngOnInit() {
253
        this.setSelectControlClass();
254
    }
255

256
    setSelectControlClass() {
257
        const modeType = this.isMultiple ? 'multiple' : 'single';
258
        const selectControlClass = {
259
            [`form-control`]: true,
260
            [`form-control-${this.thySize}`]: !!this.thySize,
261
            [`form-control-custom`]: true,
262
            [`select-control`]: true,
263
            [`select-control-${modeType}`]: true,
264
            [`select-control-show-search`]: this.showSearch,
265
            [`panel-is-opened`]: this.panelOpened,
266
            [`disabled`]: this.disabled
267
        };
268
        this.hostRenderer.updateClassByMap(selectControlClass);
269
        this.searchInputControlClass = {
270
            [`form-control`]: true,
271
            [`form-control-${this.thySize}`]: !!this.thySize,
272
            [`search-input-field`]: true,
273
            [`hidden`]: !this.thyShowSearch
274
        };
275
    }
276

277
    setInputValue(value: string) {
278
        if (value !== this.inputValue) {
279
            this.inputValue = value;
280
            this.updateWidth();
281
            this.thyOnSearch.emit(this.inputValue);
282
        }
283
    }
284

285
    handleBackspace(event: Event) {
286
        if ((event as KeyboardEvent).isComposing) {
287
            return;
288
        }
289
        if (this.inputValue.length === 0 && this.selectedOptions instanceof Array) {
290
            if (this.selectedOptions.length > 0) {
291
                this.removeHandle(this.selectedOptions[this.selectedOptions.length - 1], event);
292
            }
293
        }
294
    }
295

296
    updateWidth() {
297
        if (this.isMultiple && this.thyShowSearch) {
298
            if (this.inputValue || this.isComposing) {
299
                this.renderer.setStyle(this.inputElement.nativeElement, 'width', `${this.inputElement.nativeElement.scrollWidth}px`);
300
            } else {
301
                this.renderer.removeStyle(this.inputElement.nativeElement, 'width');
302
            }
303
        }
304
    }
305

306
    removeHandle(item: SelectOptionBase, $event: Event) {
307
        this.thyOnRemove.emit({ item: item, $eventOrigin: $event });
308
    }
309

310
    clearHandle($event: Event) {
311
        this.thyOnClear.emit($event);
312
    }
313

314
    trackValue(_index: number, option: SelectOptionBase): any {
315
        return option.thyValue;
316
    }
317

318
    onBlur(event: Event) {
319
        this.thyOnBlur.emit(event);
320
    }
321
}
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