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

atinc / ngx-tethys / b221c8fd-b7c0-45a5-901c-a8eb335069fe

16 May 2025 01:55AM UTC coverage: 90.276% (+0.002%) from 90.274%
b221c8fd-b7c0-45a5-901c-a8eb335069fe

Pull #3420

circleci

invalid-email-address
fix: fix review
Pull Request #3420: refactor(input-number): migrate to signal for input number #TINFR-1479

5607 of 6873 branches covered (81.58%)

Branch coverage included in aggregate %.

35 of 36 new or added lines in 1 file covered. (97.22%)

1 existing line in 1 file now uncovered.

13406 of 14188 relevant lines covered (94.49%)

920.14 hits per line

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

98.47
/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';
110✔
24

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

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

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

56
    private autoStepTimer: any;
110✔
57

112✔
58
    private hostFocusControl = useHostFocusControl();
112✔
59

112✔
60
    validValue: number | string;
112✔
61

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

64
    disabledUp = signal(false);
110✔
65

66
    disabledDown = signal(false);
67

110✔
68
    activeValue: string = '';
50✔
69

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

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

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

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

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

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

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

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

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

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

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

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

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

141
    private isFocused: boolean;
5!
142

5✔
143
    constructor() {
144
        super();
145

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

34✔
154
        effect(() => {
34✔
155
            const min = this.thyMin();
1✔
156
            if (this.displayValue() || this.displayValue() === 0) {
157
                const val = Number(this.displayValue());
33✔
158
                this.disabledDown.set(val <= min);
159
            }
33✔
160
        });
19✔
161

162
        effect(() => {
14!
163
            const suffix = this.thySuffix();
14✔
164
            const validValue = this.getCurrentValidValue(this.validValue);
165
            this.updateValidValue(validValue);
33✔
166
            this.displayValue.set(this.formatterValue(validValue));
33✔
167
        });
33✔
168
    }
33✔
169

33✔
170
    setDisabledState?(isDisabled: boolean): void {
33✔
171
        this.thyDisabled = isDisabled;
33✔
172
    }
2✔
173

174
    ngOnInit() {
31✔
175
        this.hostFocusControl.focusChanged = (origin: FocusOrigin) => {
20✔
176
            if (this.thyDisabled) {
177
                return;
178
            }
179

19✔
180
            if (origin) {
19✔
181
                if (!this.isFocused) {
19✔
182
                    this.inputElement().nativeElement.focus();
19✔
183
                }
184
            } else {
185
                if (this.isFocused) {
14✔
186
                    this.displayValue.set(this.formatterValue(this.validValue));
14✔
187
                    this.onTouchedFn();
14✔
188
                    this.thyBlur.emit();
14✔
189
                    this.isFocused = false;
190
                }
191
            }
66✔
192
        };
66✔
193
    }
62✔
194

195
    writeValue(value: number | string): void {
4✔
196
        const _value = this.getCurrentValidValue(value);
4✔
197
        this.updateValidValue(_value);
4!
NEW
UNCOV
198
        this.displayValue.set(this.formatterValue(_value));
×
199
    }
200

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

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

289✔
235
    onInputFocus(event?: Event) {
236
        this.activeValue = this.parser(this.displayValue().toString());
237
        if (!this.isFocused) {
238
            this.isFocused = true;
469✔
239
            this.thyFocus.emit(event);
240
        }
241
    }
242

243
    onKeyDown(e: KeyboardEvent): void {
244
        if (e.keyCode === UP_ARROW) {
245
            this.up(e);
419✔
246
            this.stop();
419✔
247
        } else if (e.keyCode === DOWN_ARROW) {
283✔
248
            this.down(e);
249
            this.stop();
136✔
250
        } else if (e.keyCode === ENTER) {
136✔
251
            this.displayValue.set(this.formatterValue(this.validValue));
2✔
252
        }
253
    }
136✔
254

6✔
255
    stop() {
256
        if (this.autoStepTimer) {
136✔
257
            clearTimeout(this.autoStepTimer);
4✔
258
        }
259
        this.displayValue.set(this.toNumber(this.displayValue()));
136✔
260
    }
261

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

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

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

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

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

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

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

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

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

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

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

379
        return this.toNumber(val);
380
    }
381

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

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

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

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