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

atinc / ngx-tethys / 99f38d5c-3c6c-4181-b4cb-736c3a4b862d

18 Sep 2023 09:24AM UTC coverage: 90.201% (+0.001%) from 90.2%
99f38d5c-3c6c-4181-b4cb-736c3a4b862d

Pull #2841

circleci

yxb941006
feat(input-number): #INFR-7140 restrict decimal places input 
Pull Request #2841: feat(input-number): #INFR-7140 restrict decimal places input 

5163 of 6383 branches covered (0.0%)

Branch coverage included in aggregate %.

13035 of 13792 relevant lines covered (94.51%)

970.85 hits per line

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

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

8
import { FocusOrigin } from '@angular/cdk/a11y';
9
import {
10
    ChangeDetectorRef,
11
    Component,
12
    ElementRef,
13
    EventEmitter,
1✔
14
    forwardRef,
1✔
15
    Input,
1✔
16
    OnChanges,
2✔
17
    OnDestroy,
18
    OnInit,
19
    Output,
20
    SimpleChanges,
21
    ViewChild
22
} from '@angular/core';
1✔
23
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
24

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

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

857✔
32
/**
33
 * 数字输入框
34
 * @name thy-input-number
94✔
35
 * @order 10
94✔
36
 */
4✔
37
@Component({
4✔
38
    selector: 'thy-input-number',
39
    templateUrl: './input-number.component.html',
40
    providers: [
41
        {
857✔
42
            provide: NG_VALUE_ACCESSOR,
43
            useExisting: forwardRef(() => ThyInputNumberComponent),
44
            multi: true
90✔
45
        }
90✔
46
    ],
90✔
47
    standalone: true,
90✔
48
    imports: [ThyIconComponent, ThyInputDirective, ThyAutofocusDirective, FormsModule, ThyMinDirective, ThyMaxDirective],
90✔
49
    host: {
90✔
50
        class: 'thy-input-number',
90✔
51
        '[attr.tabindex]': 'tabIndex'
90✔
52
    }
90✔
53
})
90✔
54
export class ThyInputNumberComponent
90✔
55
    extends TabIndexDisabledControlValueAccessorMixin
90✔
56
    implements ControlValueAccessor, OnChanges, OnInit, OnDestroy
57
{
58
    @ViewChild('input', { static: true }) inputElement: ElementRef<any>;
90✔
59

60
    private autoStepTimer: any;
61

90✔
62
    private hostFocusControl = useHostFocusControl();
33✔
63

1✔
64
    validValue: number | string;
65

32✔
66
    displayValue: number | string;
25✔
67

22✔
68
    disabledUp = false;
69

70
    disabledDown = false;
71

7✔
72
    activeValue: string = '';
6✔
73

6✔
74
    /**
6✔
75
     * 是否自动聚焦
6✔
76
     * @default false
77
     */
78
    @Input() @InputBoolean() thyAutoFocus: boolean;
79

80
    /**
81
     * 输入框的placeholder
117✔
82
     */
2✔
83
    @Input() thyPlaceholder: string = '';
2✔
84

2✔
85
    /**
86
     * 是否禁用
87
     * @default false
88
     */
214✔
89
    @Input() @InputBoolean() thyDisabled: boolean;
214✔
90

214✔
91
    /**
214✔
92
     * 最大值
93
     * @default Infinity
94
     */
237✔
95
    @Input() set thyMax(value: number) {
137✔
96
        this.innerMax = isNumber(value) ? value : this.innerMax;
97
        if (this.displayValue || this.displayValue === 0) {
100✔
98
            const val = Number(this.displayValue);
99✔
99
            this.disabledUp = val >= this.innerMax;
100
        }
237✔
101
    }
237✔
102

100✔
103
    get thyMax() {
100✔
104
        return this.innerMax;
17✔
105
    }
106

100✔
107
    /**
16✔
108
     * 最小值
109
     * @default -Infinity
110
     */
111
    @Input() set thyMin(value: number) {
112
        this.innerMin = isNumber(value) ? value : this.innerMin;
12✔
113
        if (this.displayValue || this.displayValue === 0) {
12✔
114
            const val = Number(this.displayValue);
12✔
115
            this.disabledDown = val <= this.innerMin;
10✔
116
        }
10✔
117
    }
118

119
    get thyMin() {
120
        return this.innerMin;
4✔
121
    }
4✔
122

3✔
123
    /**
124
     * 每次改变步数,可以为小数
125
     */
1✔
126
    @Input() @InputNumber() thyStep = 1;
1✔
127

128
    /**
129
     * 输入框大小
130
     * @type xs | sm | md | lg
22✔
131
     */
22✔
132
    @Input() thySize: InputSize;
14✔
133

14✔
134
    /**
135
     * 数值精度
136
     */
137
    @Input() thyPrecision: number;
3✔
138

1✔
139
    /**
1✔
140
     * 数值后缀
141
     */
2✔
142
    @Input() thySuffix: string;
1✔
143

1✔
144
    /**
145
     * 焦点失去事件
1!
146
     */
1✔
147
    @Output() thyBlur = new EventEmitter<Event>();
148

149
    /**
150
     * 焦点激活事件
24✔
151
     */
19✔
152
    @Output() thyFocus = new EventEmitter<Event>();
153

24✔
154
    private innerMax: number = Infinity;
155

156
    private innerMin: number = -Infinity;
12✔
157

12✔
158
    private isFocused: boolean;
12✔
159

1✔
160
    constructor(private cdr: ChangeDetectorRef) {
161
        super();
11✔
162
    }
163

11✔
164
    setDisabledState?(isDisabled: boolean): void {
8✔
165
        this.thyDisabled = isDisabled;
166
    }
3!
167

3✔
168
    ngOnInit() {
169
        this.hostFocusControl.focusChanged = (origin: FocusOrigin) => {
11✔
170
            if (this.thyDisabled) {
11✔
171
                return;
11✔
172
            }
11✔
173

11✔
174
            if (origin) {
11!
175
                if (!this.isFocused) {
×
176
                    this.inputElement.nativeElement.focus();
177
                }
11✔
178
            } else {
×
179
                if (this.isFocused) {
180
                    this.displayValue = this.formatterValue(this.validValue);
181
                    this.onTouchedFn();
182
                    this.thyBlur.emit();
8✔
183
                    this.isFocused = false;
8✔
184
                }
8✔
185
            }
8✔
186
        };
187
    }
188

3✔
189
    ngOnChanges(changes: SimpleChanges) {
3✔
190
        if (changes.thySuffix && !changes.thySuffix.isFirstChange()) {
3✔
191
            const validValue = this.getCurrentValidValue(this.validValue);
3✔
192
            this.updateValidValue(validValue);
193
            this.displayValue = this.formatterValue(validValue);
194
        }
22✔
195
    }
18✔
196

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

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

222
    onModelChange(value: string): void {
9✔
223
        const parseValue = this.parser(value);
9✔
224
        const validValue = this.getCurrentValidValue(parseValue);
225
        if (this.validValue !== validValue) {
226
            this.updateValidValue(validValue);
3✔
227
            this.onChangeFn(this.validValue);
3✔
228
        }
229
    }
230

234✔
231
    onInput(input?: ThyInputComponent) {
234✔
232
        const value = input.value;
146✔
233
        if (this.isInputNumber(value)) {
234
            this.activeValue = value;
235
        } else {
88✔
236
            this.displayValue = this.activeValue;
237
            input.value = this.displayValue;
238
        }
239
    }
268✔
240

241
    onInputFocus(event?: Event) {
242
        this.activeValue = this.parser(this.displayValue.toString());
243
        if (!this.isFocused) {
244
            this.isFocused = true;
245
            this.thyFocus.emit(event);
246
        }
239✔
247
    }
239✔
248

47✔
249
    onKeyDown(e: KeyboardEvent): void {
250
        if (e.keyCode === UP_ARROW) {
192✔
251
            this.up(e);
192✔
252
            this.stop();
91✔
253
        } else if (e.keyCode === DOWN_ARROW) {
254
            this.down(e);
192✔
255
            this.stop();
2✔
256
        } else if (e.keyCode === ENTER) {
257
            this.displayValue = this.formatterValue(this.validValue);
192✔
258
        }
2✔
259
    }
260

192✔
261
    stop() {
262
        if (this.autoStepTimer) {
263
            clearTimeout(this.autoStepTimer);
656✔
264
        }
265
        this.displayValue = this.toNumber(this.displayValue);
266
    }
227✔
267

94✔
268
    step(type: Type, e: MouseEvent | KeyboardEvent): void {
269
        this.stop();
133✔
270
        e.preventDefault();
133✔
271
        if (this.thyDisabled) {
12✔
272
            return;
273
        }
121✔
274
        const value = this.validValue as number;
275
        let val;
276
        if (type === Type.up) {
25✔
277
            val = this.upStep(value);
278
        } else if (type === Type.down) {
279
            val = this.downStep(value);
90✔
280
        }
281
        const outOfRange = val > this.thyMax || val < this.thyMin;
1✔
282
        val = this.getCurrentValidValue(val);
283
        this.updateValidValue(val);
284
        this.onChangeFn(this.validValue);
1✔
285
        this.displayValue = this.formatterValue(val);
286
        if (outOfRange) {
287
            return;
288
        }
289
        this.autoStepTimer = setTimeout(() => {
290
            (this[Type[type]] as (e: MouseEvent | KeyboardEvent) => void)(e);
291
        }, 300);
292
    }
293

294
    upStep(value: number): number {
295
        const precisionFactor = this.getPrecisionFactor(value);
296
        const precision = this.getMaxPrecision(value);
297
        const result = ((precisionFactor * value + precisionFactor * this.thyStep) / precisionFactor).toFixed(precision);
298
        return this.toNumber(result);
299
    }
1✔
300

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

1✔
308
    getMaxPrecision(value: string | number): number {
309
        if (!isUndefinedOrNull(this.thyPrecision)) {
310
            return this.thyPrecision;
311
        }
1✔
312
        const stepPrecision = this.getPrecision(this.thyStep);
313
        const currentValuePrecision = this.getPrecision(value as number);
314
        if (!value) {
315
            return stepPrecision;
316
        }
317
        return Math.max(currentValuePrecision, stepPrecision);
318
    }
90✔
319

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

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

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

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

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

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

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

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

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

389
    toNumber(num: string | number): number {
390
        if (this.isNotValid(num)) {
391
            return num as number;
392
        }
393
        const numStr = String(num);
394
        if (numStr.indexOf('.') >= 0 && !isUndefinedOrNull(this.thyPrecision)) {
395
            return Number(Number(num).toFixed(this.thyPrecision));
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