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

atinc / ngx-tethys / #55

30 Jul 2025 07:08AM UTC coverage: 9.866% (-80.4%) from 90.297%
#55

push

why520crazy
feat(empty): add setMessage for update display text #TINFR-2616

92 of 6794 branches covered (1.35%)

Branch coverage included in aggregate %.

2014 of 14552 relevant lines covered (13.84%)

6.15 hits per line

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

5.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,
1✔
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';
1✔
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 = {
×
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 {
×
57
    private thyTranslate = inject(ThyTranslate);
×
58
    private ngZone = inject(NgZone);
×
59

×
60
    private hostRenderer = useHostRenderer();
×
61

62
    private hostFocusControl = useHostFocusControl();
63

×
64
    private readonly destroyRef = inject(DestroyRef);
65

66
    public isTextareaSuffix = signal(false);
×
67

×
68
    public hasScrollbar = signal(false);
×
69

×
70
    disabled = signal(false);
×
71

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

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

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

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

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

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

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

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

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

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

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

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

140
    private disabledObservable: MutationObserver;
141

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

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

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

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

193
    private determineHasScrollbar() {
194
        this.ngZone.runOutsideAngular(() => {
195
            this.resizeObserver(this.inputDirective().nativeElement)
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[]> {
210
        return typeof ResizeObserver === 'undefined' || !ResizeObserver
211
            ? of(null)
212
            : new Observable(observer => {
213
                  const resize = new ResizeObserver((entries: ResizeObserverEntry[]) => {
214
                      observer.next(entries);
215
                  });
216
                  resize.observe(element);
217
                  return () => {
218
                      resize.disconnect();
219
                  };
220
              });
221
    }
222

223
    ngOnDestroy() {
224
        this.hostFocusControl.destroy();
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

© 2025 Coveralls, Inc