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

atinc / ngx-tethys / #101

13 Aug 2025 01:01PM UTC coverage: 90.43% (+0.02%) from 90.407%
#101

push

web-flow
Merge b7d320098 into 254bab68c

5524 of 6796 branches covered (81.28%)

Branch coverage included in aggregate %.

149 of 166 new or added lines in 15 files covered. (89.76%)

12 existing lines in 7 files now uncovered.

14018 of 14814 relevant lines covered (94.63%)

906.76 hits per line

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

88.32
/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
    OnInit,
9
    Renderer2,
10
    TemplateRef,
11
    numberAttribute,
12
    inject,
13
    input,
14
    viewChild,
15
    output,
1✔
16
    effect,
17
    Signal,
65✔
18
    computed,
65✔
19
    linkedSignal,
7✔
20
    untracked
4✔
21
} from '@angular/core';
22
import { useHostRenderer } from '@tethys/cdk/dom';
23

3✔
24
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
25
import { FormsModule } from '@angular/forms';
26
import { ThyGridModule } from 'ngx-tethys/grid';
27
import { ThyIcon } from 'ngx-tethys/icon';
58✔
28
import { ThyTag } from 'ngx-tethys/tag';
29
import { SelectOptionBase } from '../../option/select-option-base';
65✔
30

31
export type SelectControlSize = 'xs' | 'sm' | 'md' | 'lg' | '';
32

563✔
33
/**
563✔
34
 * @private
1✔
35
 */
36
@Component({
563✔
37
    selector: 'thy-select-control,[thySelectControl]',
126✔
38
    templateUrl: './select-control.component.html',
39
    changeDetection: ChangeDetectionStrategy.OnPush,
563✔
40
    imports: [FormsModule, NgClass, NgStyle, ThyTag, NgTemplateOutlet, ThyIcon, ThyGridModule],
20✔
41
    host: {
42
        '[class.select-control-borderless]': 'thyBorderless()'
563✔
43
    }
44
})
45
export class ThySelectControl implements OnInit {
288✔
46
    private renderer = inject(Renderer2);
288✔
47

288✔
48
    inputValue = '';
288✔
49

288✔
50
    isComposing = false;
288✔
51

288✔
52
    searchInputControlClass: { [key: string]: boolean };
288✔
53

288✔
54
    private hostRenderer = useHostRenderer();
446✔
55

446✔
56
    readonly thyPanelOpened = input(false, { transform: coerceBooleanProperty });
57

288✔
58
    readonly thyIsMultiple = input(false, { transform: coerceBooleanProperty });
288✔
59

288✔
60
    readonly thyShowSearch = input(false, { transform: coerceBooleanProperty });
288✔
61

288✔
62
    readonly thySelectedOptions = input<SelectOptionBase | SelectOptionBase[]>();
288✔
63

41✔
64
    protected readonly previousSelectedOptions = linkedSignal({
41!
NEW
65
        source: () => this.thySelectedOptions(),
×
66
        computation: (source, previous) => previous?.source
67
    });
41!
UNCOV
68

×
69
    readonly thyDisabled = input(false, { transform: coerceBooleanProperty });
70

71
    readonly customDisplayTemplate = input<TemplateRef<any>>(undefined);
41✔
72

73
    readonly thyAllowClear = input(false, { transform: coerceBooleanProperty });
74

288✔
75
    readonly thyPlaceholder = input('');
288✔
76

288✔
77
    readonly thySize = input<SelectControlSize>();
288✔
78

288✔
79
    readonly tagSize: Signal<ThyTagSize> = computed(() => {
288✔
80
        const value = this.thySize();
288✔
81
        if (value === 'xs' || value === 'sm') {
288✔
82
            return 'sm';
288✔
83
        } else if (value === 'lg') {
446✔
84
            return 'lg';
85
        } else {
86
            return 'md';
288✔
87
        }
349✔
88
    });
89

288✔
90
    readonly thyMaxTagCount = input(0, { transform: numberAttribute });
165✔
91

165✔
92
    readonly thyBorderless = input(false, { transform: coerceBooleanProperty });
1✔
93

94
    readonly thyPreset = input<string>('');
164✔
95

96
    public readonly thyOnSearch = output<string>();
288✔
97

466✔
98
    public readonly thyOnRemove = output<{ item: SelectOptionBase; $eventOrigin: Event }>();
466✔
99

141✔
100
    public readonly thyOnClear = output<Event>();
141✔
101

22✔
102
    public readonly thyOnBlur = output<Event>();
22✔
103

104
    readonly inputElement = viewChild<ElementRef>('inputElement');
105

106
    isSelectedValue = computed(() => {
107
        return (
108
            (!this.thyIsMultiple() && !isUndefinedOrNull(this.thySelectedOptions())) ||
325✔
109
            (this.thyIsMultiple() && (<SelectOptionBase[]>this.thySelectedOptions()).length > 0)
325✔
110
        );
27✔
111
    });
27✔
112

4✔
113
    showClearIcon = computed(() => {
4✔
114
        return this.thyAllowClear() && this.isSelectedValue();
4✔
115
    });
116

117
    maxSelectedTags = computed(() => {
118
        const selectedOptions = this.thySelectedOptions();
119
        if (this.thyMaxTagCount() > 0 && selectedOptions instanceof Array && selectedOptions.length > this.thyMaxTagCount()) {
120
            return selectedOptions.slice(0, this.thyMaxTagCount() - 1);
121
        }
288✔
122
        return selectedOptions as SelectOptionBase[];
500✔
123
    });
124

288✔
125
    get selectedValueStyle() {
446✔
126
        let showSelectedValue = false;
446✔
127
        if (this.thyShowSearch()) {
446✔
128
            if (this.thyPanelOpened()) {
446✔
129
                showSelectedValue = !(this.isComposing || this.inputValue);
446✔
130
            } else {
165✔
131
                showSelectedValue = true;
27✔
132
            }
133
        } else {
134
            showSelectedValue = true;
135
        }
281✔
136
        return { display: showSelectedValue ? 'flex' : 'none' };
5✔
137
    }
138

139
    get placeholderStyle() {
446✔
140
        let placeholder = true;
4✔
141
        if (this.isSelectedValue()) {
3✔
142
            placeholder = false;
3✔
143
        }
3✔
144
        if (!this.thyPlaceholder()) {
145
            placeholder = false;
146
        }
147
        if (this.isComposing || this.inputValue) {
4✔
148
            placeholder = false;
4!
149
        }
4✔
150
        return { display: placeholder ? 'block' : 'none' };
151
    }
152

153
    constructor() {
154
        effect(() => {
155
            const panelOpened = this.thyPanelOpened();
156
            if (panelOpened) {
157
                untracked(() => {
158
                    if (this.thyShowSearch()) {
500✔
159
                        Promise.resolve(null).then(() => {
500✔
160
                            this.inputElement().nativeElement.focus();
161
                        });
162
                    }
163
                });
164
            } else {
165
                untracked(() => {
166
                    if (this.thyShowSearch()) {
167
                        new Promise(resolve => setTimeout(resolve, 100)).then(() => {
168
                            if (this.inputValue) {
169
                                this.inputValue = '';
500✔
170
                                this.updateWidth();
500✔
171
                                this.thyOnSearch.emit(this.inputValue);
172
                            }
173
                        });
174
                    }
175
                });
176
            }
177
        });
178

179
        effect(() => {
19!
180
            this.setSelectControlClass();
19✔
181
        });
19✔
182

19✔
183
        effect(() => {
184
            let sameValue = false;
185
            const oldValue = this.previousSelectedOptions();
NEW
186
            const value = this.thySelectedOptions();
×
NEW
187
            untracked(() => {
×
188
                if (this.thyIsMultiple()) {
NEW
189
                    if (oldValue instanceof Array && value instanceof Array && oldValue.length === value.length) {
×
NEW
190
                        sameValue = value.every((option, index) => option.thyValue === oldValue[index].thyValue);
×
NEW
191
                    }
×
NEW
192
                } else {
×
193
                    if (oldValue && value) {
194
                        sameValue = (oldValue as SelectOptionBase).thyValue === (value as SelectOptionBase).thyValue;
195
                    }
196
                }
197

45✔
198
                if (this.thyPanelOpened() && this.thyShowSearch()) {
2!
199
                    if (!sameValue) {
2✔
200
                        Promise.resolve(null).then(() => {
201
                            this.inputValue = '';
NEW
202
                            this.updateWidth();
×
203
                        });
204
                    }
205
                    //等待组件渲染好再聚焦
206
                    setTimeout(() => {
207
                        if (this.thyPanelOpened()) {
6✔
208
                            this.inputElement().nativeElement.focus();
209
                        }
210
                    }, 200);
6✔
211
                }
212
            });
213
        });
216✔
214
    }
215

216
    ngOnInit() {}
2✔
217

218
    setSelectControlClass() {
1✔
219
        const modeType = this.thyIsMultiple() ? 'multiple' : 'single';
1✔
220
        const selectControlClass = {
221
            [`form-control`]: true,
222
            [`form-control-${this.thySize()}`]: !!this.thySize(),
223
            [`form-control-custom`]: true,
224
            [`select-control`]: true,
225
            [`select-control-${modeType}`]: true,
226
            [`select-control-show-search`]: this.thyShowSearch(),
227
            [`panel-is-opened`]: this.thyPanelOpened(),
228
            [`disabled`]: this.thyDisabled()
229
        };
230
        this.hostRenderer.updateClassByMap(selectControlClass);
231
        this.searchInputControlClass = {
232
            [`form-control`]: true,
233
            [`form-control-${this.thySize()}`]: !!this.thySize(),
234
            [`search-input-field`]: true,
235
            [`hidden`]: !this.thyShowSearch(),
236
            [`disabled`]: this.thyDisabled()
237
        };
238
    }
239

1✔
240
    setInputValue(value: string) {
241
        if (value !== this.inputValue) {
242
            this.inputValue = value;
243
            this.updateWidth();
244
            this.thyOnSearch.emit(this.inputValue);
245
        }
246
    }
247

248
    handleBackspace(event: Event) {
249
        if ((event as KeyboardEvent).isComposing) {
250
            return;
251
        }
252
        const selectedOptions = this.thySelectedOptions();
253
        if (!this.inputValue?.length && selectedOptions instanceof Array) {
254
            if (selectedOptions.length > 0) {
255
                this.removeHandle(selectedOptions[selectedOptions.length - 1], event);
256
            }
257
        }
258
    }
259

260
    updateWidth() {
261
        if (this.thyIsMultiple() && this.thyShowSearch()) {
262
            if (this.inputValue || this.isComposing) {
263
                this.renderer.setStyle(this.inputElement().nativeElement, 'width', `${this.inputElement().nativeElement.scrollWidth}px`);
264
            } else {
265
                this.renderer.removeStyle(this.inputElement().nativeElement, 'width');
266
            }
267
        }
268
    }
269

270
    removeHandle(item: SelectOptionBase, $event: Event) {
271
        this.thyOnRemove.emit({ item: item, $eventOrigin: $event });
272
    }
273

274
    clearHandle($event: Event) {
275
        this.thyOnClear.emit($event);
276
    }
277

278
    trackValue(_index: number, option: SelectOptionBase): any {
279
        return option.thyValue;
280
    }
281

282
    onBlur(event: Event) {
283
        this.thyOnBlur.emit(event);
284
    }
285
}
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