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

atinc / ngx-tethys / #102

26 May 2026 08:11AM UTC coverage: 91.111% (+0.7%) from 90.407%
#102

push

web-flow
build: bump docgeni to 2.8.0-next.5 (#3809)

4571 of 5491 branches covered (83.25%)

Branch coverage included in aggregate %.

13141 of 13949 relevant lines covered (94.21%)

966.75 hits per line

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

92.05
/src/input/input-group.component.ts
1
import {
2
    Component,
3
    TemplateRef,
4
    ViewEncapsulation,
5
    ChangeDetectionStrategy,
6
    OnInit,
7
    OnDestroy,
8
    NgZone,
9
    inject,
10
    DestroyRef,
11
    input,
12
    computed,
13
    effect,
14
    contentChild,
15
    signal
16
} from '@angular/core';
17
import { ThyTranslate, useHostFocusControl } from 'ngx-tethys/core';
18
import { useHostRenderer } from '@tethys/cdk/dom';
19
import { ThyInputDirective } from './input.directive';
20
import { NgTemplateOutlet } from '@angular/common';
21
import { throttleTime } from 'rxjs/operators';
22
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
23
import { Observable, of } from 'rxjs';
24
import { FocusOrigin } from '@angular/cdk/a11y';
25
import { MutationObserverFactory } from '@angular/cdk/observers';
26

27
export type InputGroupSize = 'sm' | 'lg' | 'md' | '';
28

29
const inputGroupSizeMap = {
1✔
30
    sm: ['input-group-sm'],
31
    lg: ['input-group-lg'],
32
    md: ['input-group-md']
33
};
34

35
/**
36
 * 输入框分组
37
 * @name thy-input-group
38
 * @order 20
39
 */
40
@Component({
41
    selector: 'thy-input-group',
42
    templateUrl: './input-group.component.html',
43
    changeDetection: ChangeDetectionStrategy.OnPush,
44
    encapsulation: ViewEncapsulation.None,
45
    host: {
46
        class: 'thy-input-group',
47
        '[class.form-control]': 'prefixTemplate() || suffixTemplate()',
48
        '[class.thy-input-group-with-prefix]': 'prefixTemplate()',
49
        '[class.thy-input-group-with-suffix]': 'suffixTemplate()',
50
        '[class.thy-input-group-with-textarea-suffix]': 'isTextareaSuffix()',
51
        '[class.thy-input-group-with-scroll-bar]': 'isTextareaSuffix() && hasScrollbar()',
52
        '[class.disabled]': 'disabled()'
53
    },
54
    imports: [NgTemplateOutlet]
55
})
56
export class ThyInputGroup implements OnInit, OnDestroy {
1✔
57
    private thyTranslate = inject(ThyTranslate);
40✔
58
    private ngZone = inject(NgZone);
40✔
59

60
    private hostRenderer = useHostRenderer();
40✔
61

62
    private hostFocusControl = useHostFocusControl();
40✔
63

64
    private readonly destroyRef = inject(DestroyRef);
40✔
65

66
    public isTextareaSuffix = signal(false);
40✔
67

68
    public hasScrollbar = signal(false);
40✔
69

70
    disabled = signal(false);
40✔
71

72
    /**
73
     * 输入框上添加的后置文本
74
     */
75
    readonly thyAppendText = input<string>();
40✔
76

77
    /**
78
     * 输入框上添加的后置文本多语言 Key
79
     */
80
    readonly thyAppendTextTranslateKey = input<string>();
40✔
81

82
    /**
83
     * 输入框上添加的前置文本
84
     */
85
    readonly thyPrependText = input<string>();
40✔
86

87
    /**
88
     * 输入框上添加的前置文本多语言 Key
89
     */
90
    readonly thyPrependTextTranslateKey = input<string>();
40✔
91

92
    protected readonly prependText = computed(() => {
40✔
93
        const prependTextTranslateKey = this.thyPrependTextTranslateKey();
40✔
94
        if (prependTextTranslateKey) {
40✔
95
            return this.thyTranslate.instant(prependTextTranslateKey);
8✔
96
        }
97
        return this.thyPrependText();
32✔
98
    });
99

100
    protected readonly appendText = computed(() => {
40✔
101
        const appendTextTranslateKey = this.thyAppendTextTranslateKey();
40✔
102
        if (appendTextTranslateKey) {
40✔
103
            return this.thyTranslate.instant(appendTextTranslateKey);
8✔
104
        }
105
        return this.thyAppendText();
32✔
106
    });
107

108
    /**
109
     * 输入框分组大小
110
     * @type 'sm' | 'lg' | 'md' | ''
111
     * @default ''
112
     */
113
    readonly thySize = input<InputGroupSize>();
40✔
114

115
    /**
116
     * 后置模板
117
     */
118
    readonly appendTemplate = contentChild<TemplateRef<unknown>>('append');
40✔
119

120
    /**
121
     * 前置模板
122
     */
123
    readonly prependTemplate = contentChild<TemplateRef<unknown>>('prepend');
40✔
124

125
    /**
126
     * 前缀
127
     */
128
    readonly prefixTemplate = contentChild<TemplateRef<unknown>>('prefix');
40✔
129

130
    /**
131
     * 后缀
132
     */
133
    readonly suffixTemplate = contentChild<TemplateRef<unknown>>('suffix');
40✔
134

135
    /**
136
     * @private
137
     */
138
    readonly inputDirective = contentChild(ThyInputDirective);
40✔
139

140
    private disabledObservable: MutationObserver | null = null;
141

142
    constructor() {
143
        effect(() => {
40✔
144
            const size = this.thySize();
42✔
145
            if (size && inputGroupSizeMap[size]) {
42✔
146
                this.hostRenderer.updateClass(inputGroupSizeMap[size]);
3✔
147
            } else {
148
                this.hostRenderer.updateClass([]);
39✔
149
            }
150
        });
151

152
        effect(() => {
40✔
153
            const inputDirective = this.inputDirective();
40✔
154
            if (inputDirective?.nativeElement) {
40✔
155
                this.isTextareaSuffix.set(inputDirective?.nativeElement?.tagName === 'TEXTAREA');
40✔
156
                if (this.isTextareaSuffix()) {
40✔
157
                    this.determineHasScrollbar();
2✔
158
                }
159
            }
160
        });
161

162
        effect(() => {
40✔
163
            const inputDirective = this.inputDirective();
40✔
164
            this.disabledObservable?.disconnect();
40✔
165
            if (inputDirective?.nativeElement) {
40✔
166
                this.disabledObservable = new MutationObserverFactory().create(mutations => {
40✔
167
                    for (const mutation of mutations) {
1✔
168
                        if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
1✔
169
                            this.disabled.set(!!inputDirective.nativeElement.hasAttribute('disabled'));
1✔
170
                        }
171
                    }
172
                });
173
                if (this.disabledObservable) {
40✔
174
                    this.disabledObservable.observe(inputDirective.nativeElement, {
40✔
175
                        attributes: true,
176
                        attributeFilter: ['disabled']
177
                    });
178
                }
179
            }
180
        });
181
    }
182

183
    ngOnInit() {
184
        this.hostFocusControl.focusChanged = (origin: FocusOrigin) => {
40✔
185
            if (origin) {
4✔
186
                this.hostRenderer.addClass('form-control-active');
2✔
187
            } else {
188
                this.hostRenderer.removeClass('form-control-active');
2✔
189
            }
190
        };
191
    }
192

193
    private determineHasScrollbar() {
194
        this.ngZone.runOutsideAngular(() => {
2✔
195
            this.resizeObserver(this.inputDirective()!.nativeElement)
2✔
196
                .pipe(throttleTime(100), takeUntilDestroyed(this.destroyRef))
197
                .subscribe(() => {
198
                    const hasScrollbar =
199
                        this.inputDirective()!.nativeElement.scrollHeight > this.inputDirective()!.nativeElement.clientHeight;
×
200
                    if (this.hasScrollbar() !== hasScrollbar) {
×
201
                        this.ngZone.run(() => {
×
202
                            this.hasScrollbar.set(hasScrollbar);
×
203
                        });
204
                    }
205
                });
206
        });
207
    }
208

209
    private resizeObserver(element: HTMLElement): Observable<ResizeObserverEntry[] | null> {
210
        return typeof ResizeObserver === 'undefined' || !ResizeObserver
2!
211
            ? of(null)
212
            : new Observable(observer => {
213
                  const resize = new ResizeObserver((entries: ResizeObserverEntry[]) => {
2✔
214
                      observer.next(entries);
×
215
                  });
216
                  resize.observe(element);
2✔
217
                  return () => {
2✔
218
                      resize.disconnect();
2✔
219
                  };
220
              });
221
    }
222

223
    ngOnDestroy() {
224
        this.hostFocusControl.destroy();
40✔
225
    }
226
}
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