• 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

3.41
/src/input-number/input-number.component.ts
1
import { TabIndexDisabledControlValueAccessorMixin, useHostFocusControl } from 'ngx-tethys/core';
2
import { ThyMaxDirective, ThyMinDirective } from 'ngx-tethys/form';
3
import { ThyIcon } from 'ngx-tethys/icon';
4
import { ThyInputDirective } from 'ngx-tethys/input';
5
import { ThyAutofocusDirective } from 'ngx-tethys/shared';
6
import { coerceBooleanProperty, DOWN_ARROW, ENTER, isFloat, isNumber, isUndefinedOrNull, UP_ARROW } from 'ngx-tethys/util';
7

8
import { FocusOrigin } from '@angular/cdk/a11y';
9
import {
10
    Component,
11
    ElementRef,
12
    forwardRef,
1✔
13
    Input,
1✔
14
    numberAttribute,
1✔
15
    OnDestroy,
2✔
16
    OnInit,
17
    input,
18
    effect,
19
    signal,
20
    output,
21
    viewChild
1✔
22
} from '@angular/core';
23
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
×
24

×
25
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
×
26

×
27
enum Type {
×
28
    up,
×
29
    down
×
30
}
×
31

×
32
/**
×
33
 * 数字输入框
×
34
 * @name thy-input-number
×
35
 * @order 10
×
36
 */
×
37
@Component({
×
38
    selector: 'thy-input-number',
×
39
    templateUrl: './input-number.component.html',
×
40
    providers: [
×
41
        {
×
42
            provide: NG_VALUE_ACCESSOR,
×
43
            useExisting: forwardRef(() => ThyInputNumber),
×
44
            multi: true
×
45
        }
×
46
    ],
×
47
    imports: [ThyIcon, ThyInputDirective, ThyAutofocusDirective, FormsModule],
×
48
    host: {
49
        class: 'thy-input-number',
50
        '[attr.tabindex]': 'tabIndex'
×
51
    }
×
52
})
×
53
export class ThyInputNumber extends TabIndexDisabledControlValueAccessorMixin implements ControlValueAccessor, OnInit, OnDestroy {
×
54
    readonly inputElement = viewChild<ElementRef<any>>('input');
×
55

×
56
    private autoStepTimer: any;
57

58
    private hostFocusControl = useHostFocusControl();
×
59

×
60
    validValue: number | string;
×
61

×
62
    displayValue = signal<number | string>(undefined);
×
63

64
    disabledUp = signal(false);
65

66
    disabledDown = signal(false);
×
67

68
    activeValue: string = '';
69

×
70
    /**
×
71
     * 是否自动聚焦
×
72
     * @default false
73
     */
×
74
    readonly thyAutoFocus = input(false, { transform: coerceBooleanProperty });
×
75

×
76
    /**
77
     * 输入框的placeholder
78
     */
79
    readonly thyPlaceholder = input<string>('');
×
80

×
81
    /**
×
82
     * 是否禁用
×
83
     * @default false
×
84
     */
85
    @Input({ transform: coerceBooleanProperty }) thyDisabled: boolean;
86

87
    /**
88
     * 最大值
89
     * @default Infinity
×
90
     */
×
91
    readonly thyMax = input(Infinity, { transform: (value: number) => (isNumber(value) ? value : Infinity) });
×
92

93
    /**
94
     * 最小值
×
95
     * @default -Infinity
×
96
     */
97
    readonly thyMin = input(-Infinity, { transform: (value: number) => (isNumber(value) ? value : -Infinity) });
×
98

×
99
    /**
100
     * 每次改变步数,可以为小数
×
101
     */
×
102
    readonly thyStep = input(1, { transform: numberAttribute });
×
103

×
104
    /**
×
105
     * 改变步数时的延迟毫秒数,值越小变化的速度越快
×
106
     * @default 300
107
     */
×
108
    readonly thyStepDelay = input(300, { transform: numberAttribute });
×
109

110
    /**
111
     * 输入框大小
112
     * @type xs | sm | md | lg
113
     */
×
114
    readonly thySize = input<InputSize>();
×
115

×
116
    /**
117
     * 数值精度
118
     */
×
119
    readonly thyPrecision = input<number>();
×
120

121
    /**
×
122
     * 数值后缀
×
123
     */
×
124
    readonly thySuffix = input<string>();
×
125

126
    /**
127
     * 焦点失去事件
128
     */
×
129
    readonly thyBlur = output();
×
130

×
131
    /**
×
132
     * 焦点激活事件
133
     */
134
    readonly thyFocus = output<Event>();
135

×
136
    /**
×
137
     * 上下箭头点击事件
×
138
     */
139
    readonly thyStepChange = output<{ value: number; type: Type }>();
×
140

×
141
    private isFocused: boolean;
×
142

143
    constructor() {
×
144
        super();
×
145

146
        effect(() => {
147
            const displayValue = this.displayValue();
148
            const max = this.thyMax();
×
149
            if (displayValue || displayValue === 0) {
×
150
                const val = Number(displayValue);
151
                this.disabledUp.set(val >= max);
×
152
            }
153
        });
154

×
155
        effect(() => {
×
156
            const min = this.thyMin();
×
157
            const displayValue = this.displayValue();
×
158
            if (displayValue || displayValue === 0) {
159
                const val = Number(displayValue);
×
160
                this.disabledDown.set(val <= min);
161
            }
×
162
        });
×
163

164
        effect(() => {
×
165
            const suffix = this.thySuffix();
×
166
            const validValue = this.getCurrentValidValue(this.validValue);
167
            this.updateValidValue(validValue);
×
168
            this.displayValue.set(this.formatterValue(validValue));
×
169
        });
×
170
    }
×
171

×
172
    setDisabledState?(isDisabled: boolean): void {
×
173
        this.thyDisabled = isDisabled;
×
174
    }
×
175

176
    ngOnInit() {
×
177
        this.hostFocusControl.focusChanged = (origin: FocusOrigin) => {
×
178
            if (this.thyDisabled) {
179
                return;
180
            }
181

×
182
            if (origin) {
×
183
                if (!this.isFocused) {
×
184
                    this.inputElement().nativeElement.focus();
×
185
                }
186
            } else {
187
                if (this.isFocused) {
×
188
                    this.displayValue.set(this.formatterValue(this.validValue));
×
189
                    this.onTouchedFn();
×
190
                    this.thyBlur.emit();
×
191
                    this.isFocused = false;
192
                }
193
            }
×
194
        };
×
195
    }
×
196

197
    writeValue(value: number | string): void {
×
198
        const _value = this.getCurrentValidValue(value);
×
199
        this.updateValidValue(_value);
×
200
        this.displayValue.set(this.formatterValue(_value));
×
201
    }
202

×
203
    updateValidValue(value: number | string): void {
204
        if (this.isNotValid(value)) {
205
            this.validValue = '';
×
206
        } else if (this.validValue !== value) {
×
207
            this.validValue = value;
208
        }
209
        this.disabledUp.set(false);
×
210
        this.disabledDown.set(false);
211
        if (value || value === 0) {
×
212
            const val = Number(value);
×
213
            if (val >= this.thyMax()) {
214
                this.disabledUp.set(true);
×
215
            }
216
            if (val <= this.thyMin()) {
×
217
                this.disabledDown.set(true);
×
218
            }
219
        }
×
220
    }
221

222
    onModelChange(value: string): void {
×
223
        const parseValue = this.parser(value);
×
224
        if (this.isInputNumber(value)) {
225
            this.activeValue = value;
226
        } else {
×
227
            this.displayValue.set(parseValue);
×
228
            this.inputElement().nativeElement.value = parseValue;
229
        }
230
        const validValue = this.getCurrentValidValue(parseValue);
×
231
        if (`${this.validValue}` !== `${validValue}`) {
×
232
            this.updateValidValue(validValue);
×
233
            this.onChangeFn(validValue);
×
234
        }
235
    }
236

×
237
    onInputFocus(event?: Event) {
238
        this.activeValue = this.parser(this.displayValue().toString());
239
        if (!this.isFocused) {
240
            this.isFocused = true;
×
241
            this.thyFocus.emit(event);
242
        }
243
    }
244

245
    onKeyDown(e: KeyboardEvent): void {
246
        if (e.keyCode === UP_ARROW) {
247
            this.up(e);
×
248
            this.stop();
×
249
        } else if (e.keyCode === DOWN_ARROW) {
×
250
            this.down(e);
251
            this.stop();
×
252
        } else if (e.keyCode === ENTER) {
×
253
            this.displayValue.set(this.formatterValue(this.validValue));
×
254
        }
255
    }
×
256

×
257
    stop() {
258
        if (this.autoStepTimer) {
×
259
            clearTimeout(this.autoStepTimer);
×
260
        }
261
        this.displayValue.set(this.toNumber(this.displayValue()));
×
262
    }
263

264
    step(type: Type, e: MouseEvent | KeyboardEvent): void {
×
265
        this.stop();
266
        e.preventDefault();
267
        if (this.thyDisabled) {
×
268
            return;
×
269
        }
270
        const value = this.validValue as number;
×
271
        let val;
×
272
        if (type === Type.up) {
×
273
            val = this.upStep(value);
×
274
        } else if (type === Type.down) {
275
            val = this.downStep(value);
×
276
        }
277
        const outOfRange = val > this.thyMax() || val < this.thyMin();
278
        val = this.getCurrentValidValue(val);
×
279
        this.updateValidValue(val);
280
        this.onChangeFn(this.validValue);
281
        this.thyStepChange.emit({ value: this.validValue as number, type });
×
282
        this.displayValue.set(this.formatterValue(val));
283
        if (outOfRange) {
1✔
284
            return;
1✔
285
        }
286
        this.autoStepTimer = setTimeout(() => {
287
            (this[Type[type] as keyof typeof Type] as (e: MouseEvent | KeyboardEvent) => void)(e);
288
        }, this.thyStepDelay());
289
    }
290

291
    upStep(value: number): number {
292
        const precisionFactor = this.getPrecisionFactor(value);
293
        const precision = this.getMaxPrecision(value);
294
        const result = ((precisionFactor * value + precisionFactor * this.thyStep()) / precisionFactor).toFixed(precision);
295
        return this.toNumber(result);
296
    }
297

298
    downStep(value: number): number {
299
        const precisionFactor = this.getPrecisionFactor(value);
300
        const precision = Math.abs(this.getMaxPrecision(value));
301
        const result = ((precisionFactor * value - precisionFactor * this.thyStep()) / precisionFactor).toFixed(precision);
1✔
302
        return this.toNumber(result);
303
    }
304

305
    getMaxPrecision(value: string | number): number {
306
        const precision = this.thyPrecision();
307
        if (!isUndefinedOrNull(precision)) {
308
            return precision;
×
309
        }
310
        const stepPrecision = this.getPrecision(this.thyStep());
311
        const currentValuePrecision = this.getPrecision(value as number);
312
        if (!value) {
313
            return stepPrecision;
314
        }
315
        return Math.max(currentValuePrecision, stepPrecision);
316
    }
317

318
    getPrecisionFactor(activeValue: string | number): number {
319
        const precision = this.getMaxPrecision(activeValue);
320
        return Math.pow(10, precision);
321
    }
322

323
    getPrecision(value: number): number {
324
        const valueString = value.toString();
325
        // 0.0000000004.toString() = 4e10  => 10
326
        if (valueString.indexOf('e-') >= 0) {
327
            return parseInt(valueString.slice(valueString.indexOf('e-') + 2), 10);
328
        }
329
        let precision = 0;
330
        // 1.2222 =>  4
331
        if (valueString.indexOf('.') >= 0) {
332
            precision = valueString.length - valueString.indexOf('.') - 1;
333
        }
334
        return precision;
335
    }
336

337
    up(e: MouseEvent | KeyboardEvent) {
338
        this.inputElement().nativeElement.focus();
339
        this.step(Type.up, e);
340
    }
341

342
    down(e: MouseEvent | KeyboardEvent) {
343
        this.inputElement().nativeElement.focus();
344
        this.step(Type.down, e);
345
    }
346

347
    formatterValue(value: number | string) {
348
        const parseValue = this.parser(`${value}`);
349
        if (parseValue) {
350
            const suffix = this.thySuffix();
351
            return suffix ? `${parseValue} ${suffix}` : parseValue;
352
        } else {
353
            return '';
354
        }
355
    }
356

357
    parser(value: string) {
358
        return value
359
            .trim()
360
            .replace(/。/g, '.')
361
            .replace(/[^\w\.-]+/g, '')
362
            .replace(this.thySuffix(), '');
363
    }
364

365
    getCurrentValidValue(value: string | number): number | string {
366
        let val = value;
367
        if (value === '' || isUndefinedOrNull(value)) {
368
            return '';
369
        }
370
        val = parseFloat(value as string);
371
        if (this.isNotValid(val)) {
372
            val = this.validValue;
373
        }
374
        if ((val as number) < this.thyMin()) {
375
            val = this.thyMin();
376
        }
377
        if ((val as number) > this.thyMax()) {
378
            val = this.thyMax();
379
        }
380

381
        return this.toNumber(val);
382
    }
383

384
    isNotValid(num: string | number): boolean {
385
        return isNaN(num as number) || num === '' || num === null || !!(num && num.toString().indexOf('.') === num.toString().length - 1);
386
    }
387

388
    toNumber(num: string | number): number {
389
        if (this.isNotValid(num)) {
390
            return num as number;
391
        }
392
        const numStr = String(num);
393
        const precision = this.thyPrecision();
394
        if (numStr.indexOf('.') >= 0 && !isUndefinedOrNull(precision)) {
395
            return Number(Number(num).toFixed(precision));
396
        }
397
        return Number(num);
398
    }
399

400
    isInputNumber(value: string) {
401
        return isFloat(value) || /^[-+Ee]$|^([-+.]?[0-9])*(([.]|[.eE])?[eE]?[+-]?)?$|^$/.test(value);
402
    }
403

404
    ngOnDestroy() {
405
        this.hostFocusControl.destroy();
406
    }
407
}
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