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

atinc / ngx-tethys / edbc1d43-1648-411a-a6bc-f24c9aa3f654

27 Mar 2025 06:13AM UTC coverage: 90.236% (+0.06%) from 90.179%
edbc1d43-1648-411a-a6bc-f24c9aa3f654

push

circleci

web-flow
Merge pull request #3282 from atinc/v19.0.0-next

5598 of 6865 branches covered (81.54%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 7 files covered. (100.0%)

157 existing lines in 46 files now uncovered.

13357 of 14141 relevant lines covered (94.46%)

992.51 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',
35
    changeDetection: ChangeDetectionStrategy.OnPush,
×
36
    imports: [FormsModule, NgClass, NgStyle, ThyTag, NgTemplateOutlet, ThyIcon, ThyGridModule],
37
    host: {
38
        '[class.select-control-borderless]': 'thyBorderless'
461✔
39
    }
461✔
40
})
18✔
41
export class ThySelectControl implements OnInit {
18✔
42
    private renderer = inject(Renderer2);
43

44
    inputValue = '';
461✔
45

6✔
46
    isComposing = false;
6✔
47

48
    panelOpened = false;
49

461✔
50
    isMultiple = false;
51

UNCOV
52
    showSearch = false;
×
53

54
    disabled = false;
55

298✔
56
    size: SelectControlSize;
298✔
57

58
    selectedOptions: SelectOptionBase | SelectOptionBase[];
59

2,429✔
60
    searchInputControlClass: { [key: string]: boolean };
61

62
    tagSize: ThyTagSize;
298✔
63

298✔
64
    private hostRenderer = useHostRenderer();
65

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

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

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

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

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

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

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

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

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

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

113✔
149
    @Input()
150
    customDisplayTemplate: TemplateRef<any>;
151

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

191✔
155
    @Input()
1✔
156
    thyPlaceholder = '';
157

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

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

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

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

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

180
    @Input() thyPreset: string = '';
181

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

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

188
    @Output()
189
    public thyOnClear = new EventEmitter<Event>();
190

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

21✔
194
    @ViewChild('inputElement')
195
    inputElement: ElementRef;
196

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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