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

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 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';
UNCOV
23
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
×
UNCOV
24

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

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

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

×
56
    private autoStepTimer: any;
57

UNCOV
58
    private hostFocusControl = useHostFocusControl();
×
UNCOV
59

×
UNCOV
60
    validValue: number | string;
×
UNCOV
61

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

64
    disabledUp = signal(false);
65

UNCOV
66
    disabledDown = signal(false);
×
67

68
    activeValue: string = '';
UNCOV
69

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

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

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

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

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

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

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

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

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

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

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

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

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

×
UNCOV
141
    private isFocused: boolean;
×
142

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
264
    step(type: Type, e: MouseEvent | KeyboardEvent): void {
×
265
        this.stop();
266
        e.preventDefault();
UNCOV
267
        if (this.thyDisabled) {
×
UNCOV
268
            return;
×
269
        }
UNCOV
270
        const value = this.validValue as number;
×
UNCOV
271
        let val;
×
UNCOV
272
        if (type === Type.up) {
×
UNCOV
273
            val = this.upStep(value);
×
274
        } else if (type === Type.down) {
UNCOV
275
            val = this.downStep(value);
×
276
        }
277
        const outOfRange = val > this.thyMax() || val < this.thyMin();
UNCOV
278
        val = this.getCurrentValidValue(val);
×
279
        this.updateValidValue(val);
280
        this.onChangeFn(this.validValue);
UNCOV
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)) {
UNCOV
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

© 2025 Coveralls, Inc