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

atinc / ngx-tethys / 2c11b3d7-fd65-411c-a4e3-dc774f8b7c23

02 Apr 2024 08:08AM UTC coverage: 90.55% (-0.04%) from 90.585%
2c11b3d7-fd65-411c-a4e3-dc774f8b7c23

Pull #3061

circleci

minlovehua
refactor(all): use takeUntilDestroyed instead of mixinUnsubscribe INFR-9529
Pull Request #3061: refactor(all): use takeUntilDestroyed instead of mixinUnsubscribe INFR-9529

5417 of 6635 branches covered (81.64%)

Branch coverage included in aggregate %.

52 of 55 new or added lines in 16 files covered. (94.55%)

54 existing lines in 9 files now uncovered.

13460 of 14212 relevant lines covered (94.71%)

979.65 hits per line

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

91.95
/src/autocomplete/autocomplete.component.ts
1
import {
2
    Component,
3
    TemplateRef,
4
    ViewChild,
5
    ChangeDetectionStrategy,
6
    ContentChildren,
7
    QueryList,
8
    OnInit,
9
    Output,
10
    EventEmitter,
11
    NgZone,
12
    OnDestroy,
13
    AfterContentInit,
14
    ChangeDetectorRef,
15
    Input,
16
    ElementRef
17
} from '@angular/core';
1✔
18
import { InputBoolean } from 'ngx-tethys/core';
19
import { defer, merge, Observable, Subject, timer } from 'rxjs';
1✔
20
import { take, switchMap, takeUntil, startWith } from 'rxjs/operators';
21
import { SelectionModel } from '@angular/cdk/collections';
22
import {
12✔
23
    THY_OPTION_PARENT_COMPONENT,
24
    IThyOptionParentComponent,
25
    ThyOption,
15✔
26
    ThyOptionSelectionChangeEvent,
15✔
27
    ThyStopPropagationDirective
15✔
28
} from 'ngx-tethys/shared';
15✔
29
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
15✔
30
import { coerceBooleanProperty } from '@angular/cdk/coercion';
15✔
31
import { ThyEmpty } from 'ngx-tethys/empty';
15✔
32
import { NgClass, NgIf } from '@angular/common';
15✔
33

15!
34
/** Event object that is emitted when an autocomplete option is activated. */
102✔
35
export interface ThyAutocompleteActivatedEvent {
UNCOV
36
    /** Reference to the autocomplete panel that emitted the event. */
×
37
    source: ThyAutocomplete;
38

15✔
39
    /** Option that was selected. */
15✔
40
    option: ThyOption | null;
15✔
41
}
15✔
42

15✔
43
/**
44
 * 自动完成组件
45
 * @name thy-autocomplete
15✔
46
 */
15✔
47
@Component({
48
    selector: 'thy-autocomplete',
49
    templateUrl: 'autocomplete.component.html',
15✔
50
    changeDetection: ChangeDetectionStrategy.OnPush,
15✔
51
    providers: [
15✔
52
        {
15✔
53
            provide: THY_OPTION_PARENT_COMPONENT,
15✔
54
            useExisting: ThyAutocomplete
55
        }
15✔
56
    ],
57
    standalone: true,
58
    imports: [ThyStopPropagationDirective, NgClass, NgIf, ThyEmpty]
59
})
15✔
60
export class ThyAutocomplete implements IThyOptionParentComponent, OnInit, AfterContentInit, OnDestroy {
15✔
61
    private ngUnsubscribe$ = new Subject<void>();
15✔
62

8✔
63
    dropDownClass: { [key: string]: boolean };
64

65
    isMultiple = false;
66

11✔
67
    mode = '';
11✔
68

11✔
69
    isEmptyOptions = false;
70

71
    selectionModel: SelectionModel<ThyOption>;
11✔
72

11✔
73
    isOpened = false;
74

75
    /** Manages active item in option list based on key events. */
15✔
76
    keyManager: ActiveDescendantKeyManager<ThyOption>;
15✔
77

5✔
78
    @ViewChild('contentTemplate', { static: true })
79
    contentTemplateRef: TemplateRef<any>;
80

81
    // scroll element container
15!
UNCOV
82
    @ViewChild('panel')
×
83
    optionsContainer: ElementRef<any>;
84

15✔
85
    /**
15✔
86
     * @private
3✔
87
     */
3✔
88
    @ContentChildren(ThyOption, { descendants: true }) options: QueryList<ThyOption>;
89

90
    readonly optionSelectionChanges: Observable<ThyOptionSelectionChangeEvent> = defer(() => {
91
        if (this.options) {
5✔
92
            return merge(...this.options.map(option => option.selectionChange));
5✔
93
        }
2✔
94
        return this.ngZone.onStable.asObservable().pipe(
2✔
95
            take(1),
96
            switchMap(() => this.optionSelectionChanges)
97
        );
3!
98
    }) as Observable<ThyOptionSelectionChangeEvent>;
3!
99

100
    /**
3!
101
     * 空选项时的文本
3✔
102
     * @type string
103
     */
104
    @Input()
5✔
105
    thyEmptyText = '没有任何数据';
3✔
106

107
    /**
5✔
108
     * 是否默认高亮第一个选项
109
     * @type boolean
110
     * @default false
15✔
111
     */
15✔
112
    @Input()
3✔
113
    @InputBoolean()
114
    set thyAutoActiveFirstOption(value: boolean) {
115
        this._autoActiveFirstOption = coerceBooleanProperty(value);
12✔
116
    }
117

15✔
118
    get thyAutoActiveFirstOption(): boolean {
119
        return this._autoActiveFirstOption;
120
    }
121
    private _autoActiveFirstOption: boolean;
122

123
    /**
15✔
124
     * 被选中时调用,参数包含选中项的 value 值
15✔
125
     * @type EventEmitter<ThyOptionSelectionChangeEvent>
126
     */
1✔
127
    @Output() thyOptionSelected: EventEmitter<ThyOptionSelectionChangeEvent> = new EventEmitter<ThyOptionSelectionChangeEvent>();
128

129
    /**
130
     * 只读,展开下拉菜单的回调
1✔
131
     * @type EventEmitter<void>
132
     */
133
    @Output() readonly thyOpened: EventEmitter<void> = new EventEmitter<void>();
134

135
    /**
136
     * 只读,关闭下拉菜单的回调
137
     * @type EventEmitter<void>
138
     */
139
    @Output() readonly thyClosed: EventEmitter<void> = new EventEmitter<void>();
140

141
    /** Emits whenever an option is activated using the keyboard. */
142
    /**
1✔
143
     * 只读,option 激活状态变化时,调用此函数
144
     * @type EventEmitter<ThyAutocompleteActivatedEvent>
145
     */
146
    @Output() readonly thyOptionActivated: EventEmitter<ThyAutocompleteActivatedEvent> = new EventEmitter<ThyAutocompleteActivatedEvent>();
147

1✔
148
    constructor(private ngZone: NgZone, private changeDetectorRef: ChangeDetectorRef) {}
149

150
    ngOnInit() {
151
        this.setDropDownClass();
152
        this.instanceSelectionModel();
153
    }
154

155
    ngAfterContentInit() {
156
        this.options.changes.pipe(startWith(null), takeUntil(this.ngUnsubscribe$)).subscribe(() => {
157
            this.resetOptions();
158
            timer(0).subscribe(() => {
159
                this.isEmptyOptions = this.options.length <= 0;
160
                this.changeDetectorRef.detectChanges();
161
            });
162
            this.initKeyManager();
163
        });
164
    }
165

166
    initKeyManager() {
167
        const changedOrDestroyed$ = merge(this.options.changes, this.ngUnsubscribe$);
168
        this.keyManager = new ActiveDescendantKeyManager<ThyOption>(this.options).withWrap();
169
        this.keyManager.change.pipe(takeUntil(changedOrDestroyed$)).subscribe(index => {
170
            this.thyOptionActivated.emit({ source: this, option: this.options.toArray()[index] || null });
171
        });
172
    }
173

174
    open() {
175
        this.isOpened = true;
176
        this.changeDetectorRef.markForCheck();
177
        this.thyOpened.emit();
178
    }
179

180
    close() {
181
        this.isOpened = false;
182
        this.thyClosed.emit();
183
    }
184

185
    private resetOptions() {
186
        const changedOrDestroyed$ = merge(this.options.changes, this.ngUnsubscribe$);
187

188
        this.optionSelectionChanges.pipe(takeUntil(changedOrDestroyed$)).subscribe((event: ThyOptionSelectionChangeEvent) => {
189
            this.onSelect(event.option, event.isUserInput);
190
        });
191
    }
192

193
    private instanceSelectionModel() {
194
        if (this.selectionModel) {
195
            this.selectionModel.clear();
196
        }
197
        this.selectionModel = new SelectionModel<ThyOption>(this.isMultiple);
198
        this.selectionModel.changed.pipe(takeUntil(this.ngUnsubscribe$)).subscribe(event => {
199
            event.added.forEach(option => option.select());
200
            event.removed.forEach(option => option.deselect());
201
        });
202
    }
203

204
    private onSelect(option: ThyOption, isUserInput: boolean) {
205
        const wasSelected = this.selectionModel.isSelected(option);
206

207
        if (option.thyValue == null && !this.isMultiple) {
208
            option.deselect();
209
            this.selectionModel.clear();
210
        } else {
211
            if (wasSelected !== option.selected) {
212
                option.selected ? this.selectionModel.select(option) : this.selectionModel.deselect(option);
213
            }
214

215
            if (isUserInput) {
216
                this.keyManager.setActiveItem(option);
217
            }
218
        }
219

220
        if (wasSelected !== this.selectionModel.isSelected(option)) {
221
            this.thyOptionSelected.emit(new ThyOptionSelectionChangeEvent(option, false));
222
        }
223
        this.changeDetectorRef.markForCheck();
224
    }
225

226
    private setDropDownClass() {
227
        let modeClass = '';
228
        if (this.isMultiple) {
229
            modeClass = `thy-select-dropdown-${this.mode}`;
230
        } else {
231
            modeClass = `thy-select-dropdown-single`;
232
        }
233
        this.dropDownClass = {
234
            [`thy-select-dropdown`]: true,
235
            [modeClass]: true
236
        };
237
    }
238

239
    ngOnDestroy() {
240
        this.ngUnsubscribe$.next();
241
        this.ngUnsubscribe$.complete();
242
    }
243
}
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