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

atinc / ngx-tethys / c0ef8457-a839-451f-8b72-80fd73106231

02 Apr 2024 02:27PM UTC coverage: 90.524% (-0.06%) from 90.585%
c0ef8457-a839-451f-8b72-80fd73106231

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @Input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

4987 of 6108 branches covered (81.65%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

202 existing lines in 53 files now uncovered.

12246 of 12929 relevant lines covered (94.72%)

1055.59 hits per line

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

97.25
/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 { DOWN_ARROW, ENTER, isFloat, isNumber, isUndefinedOrNull, UP_ARROW } from 'ngx-tethys/util';
7

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

4✔
27
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
28

29
enum Type {
30
    up,
928✔
31
    down
32
}
33

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

102✔
62
    private autoStepTimer: any;
36✔
63

4✔
64
    private hostFocusControl = useHostFocusControl();
65

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

22✔
68
    displayValue: number | string;
69

70
    disabledUp = false;
71

7✔
72
    disabledDown = false;
6✔
73

6✔
74
    activeValue: string = '';
6✔
75

6✔
76
    /**
77
     * 是否自动聚焦
78
     * @default false
79
     */
80
    @Input({ transform: booleanAttribute }) thyAutoFocus: boolean;
81

129✔
82
    /**
2✔
83
     * 输入框的placeholder
2✔
84
     */
2✔
85
    @Input() thyPlaceholder: string = '';
86

87
    /**
88
     * 是否禁用
241✔
89
     * @default false
241✔
90
     */
241✔
91
    @Input({ transform: booleanAttribute }) thyDisabled: boolean;
241✔
92

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

19✔
105
    get thyMax() {
106
        return this.innerMax;
106✔
107
    }
18✔
108

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

20✔
121
    get thyMin() {
20✔
122
        return this.innerMin;
14✔
123
    }
14✔
124

125
    /**
126
     * 每次改变步数,可以为小数
127
     */
23✔
128
    @Input({ transform: numberAttribute }) thyStep = 1;
23✔
129

15✔
130
    /**
15✔
131
     * 改变步数时的延迟毫秒数,值越小变化的速度越快
132
     * @default 300
133
     */
134
    @Input({ transform: numberAttribute }) thyStepDelay = 300;
7✔
135

1✔
136
    /**
1✔
137
     * 输入框大小
138
     * @type xs | sm | md | lg
6✔
139
     */
1✔
140
    @Input() thySize: InputSize;
1✔
141

142
    /**
5!
143
     * 数值精度
5✔
144
     */
145
    @Input() thyPrecision: number;
146

147
    /**
25✔
148
     * 数值后缀
19✔
149
     */
150
    @Input() thySuffix: string;
25✔
151

152
    /**
153
     * 焦点失去事件
12✔
154
     */
12✔
155
    @Output() thyBlur = new EventEmitter<Event>();
12✔
156

1✔
157
    /**
158
     * 焦点激活事件
11✔
159
     */
160
    @Output() thyFocus = new EventEmitter<Event>();
11✔
161

8✔
162
    private innerMax: number = Infinity;
163

3!
164
    private innerMin: number = -Infinity;
3✔
165

166
    private isFocused: boolean;
11✔
167

11✔
168
    constructor(private cdr: ChangeDetectorRef) {
11✔
169
        super();
11✔
170
    }
11✔
171

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

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

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

4!
UNCOV
197
    ngOnChanges(changes: SimpleChanges) {
×
198
        if (changes.thySuffix && !changes.thySuffix.isFirstChange()) {
199
            const validValue = this.getCurrentValidValue(this.validValue);
4✔
200
            this.updateValidValue(validValue);
201
            this.displayValue = this.formatterValue(validValue);
202
        }
11✔
203
    }
11✔
204

205
    writeValue(value: number | string): void {
206
        const _value = this.getCurrentValidValue(value);
8✔
207
        this.updateValidValue(_value);
208
        this.displayValue = this.formatterValue(_value);
8✔
209
        this.cdr.markForCheck();
2✔
210
    }
211

6✔
212
    updateValidValue(value: number | string): void {
213
        if (this.isNotValid(value)) {
6✔
214
            this.validValue = '';
2✔
215
        } else if (this.validValue !== value) {
216
            this.validValue = value;
6✔
217
        }
218
        this.disabledUp = this.disabledDown = false;
219
        if (value || value === 0) {
9✔
220
            const val = Number(value);
9✔
221
            if (val >= this.thyMax) {
222
                this.disabledUp = true;
223
            }
3✔
224
            if (val <= this.thyMin) {
3✔
225
                this.disabledDown = true;
226
            }
227
        }
265✔
228
    }
265✔
229

164✔
230
    onModelChange(value: string): void {
231
        const parseValue = this.parser(value);
232
        if (this.isInputNumber(value)) {
101✔
233
            this.activeValue = value;
234
        } else {
235
            this.displayValue = parseValue;
236
            this.inputElement.nativeElement.value = parseValue;
307✔
237
        }
238
        const validValue = this.getCurrentValidValue(parseValue);
239
        if (`${this.validValue}` !== `${validValue}`) {
240
            this.updateValidValue(validValue);
241
            this.onChangeFn(validValue);
242
        }
243
    }
272✔
244

272✔
245
    onInputFocus(event?: Event) {
60✔
246
        this.activeValue = this.parser(this.displayValue.toString());
247
        if (!this.isFocused) {
212✔
248
            this.isFocused = true;
212✔
249
            this.thyFocus.emit(event);
104✔
250
        }
251
    }
212✔
252

5✔
253
    onKeyDown(e: KeyboardEvent): void {
254
        if (e.keyCode === UP_ARROW) {
212✔
255
            this.up(e);
3✔
256
            this.stop();
257
        } else if (e.keyCode === DOWN_ARROW) {
212✔
258
            this.down(e);
259
            this.stop();
260
        } else if (e.keyCode === ENTER) {
728✔
261
            this.displayValue = this.formatterValue(this.validValue);
262
        }
263
    }
248✔
264

107✔
265
    stop() {
266
        if (this.autoStepTimer) {
141✔
267
            clearTimeout(this.autoStepTimer);
141✔
268
        }
15✔
269
        this.displayValue = this.toNumber(this.displayValue);
270
    }
126✔
271

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

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

102✔
305
    downStep(value: number): number {
306
        const precisionFactor = this.getPrecisionFactor(value);
307
        const precision = Math.abs(this.getMaxPrecision(value));
308
        const result = ((precisionFactor * value - precisionFactor * this.thyStep) / precisionFactor).toFixed(precision);
309
        return this.toNumber(result);
310
    }
311

312
    getMaxPrecision(value: string | number): number {
313
        if (!isUndefinedOrNull(this.thyPrecision)) {
314
            return this.thyPrecision;
315
        }
316
        const stepPrecision = this.getPrecision(this.thyStep);
317
        const currentValuePrecision = this.getPrecision(value as number);
318
        if (!value) {
319
            return stepPrecision;
320
        }
321
        return Math.max(currentValuePrecision, stepPrecision);
322
    }
323

324
    getPrecisionFactor(activeValue: string | number): number {
325
        const precision = this.getMaxPrecision(activeValue);
326
        return Math.pow(10, precision);
327
    }
328

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

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

348
    down(e: MouseEvent | KeyboardEvent) {
349
        this.inputElement.nativeElement.focus();
350
        this.step(Type.down, e);
351
    }
352

353
    formatterValue(value: number | string) {
354
        const parseValue = this.parser(`${value}`);
355
        if (parseValue) {
356
            return this.thySuffix ? `${parseValue} ${this.thySuffix}` : parseValue;
357
        } else {
358
            return '';
359
        }
360
    }
361

362
    parser(value: string) {
363
        return value
364
            .trim()
365
            .replace(/。/g, '.')
366
            .replace(/[^\w\.-]+/g, '')
367
            .replace(this.thySuffix, '');
368
    }
369

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

386
        return this.toNumber(val);
387
    }
388

389
    isNotValid(num: string | number): boolean {
390
        return isNaN(num as number) || num === '' || num === null || !!(num && num.toString().indexOf('.') === num.toString().length - 1);
391
    }
392

393
    toNumber(num: string | number): number {
394
        if (this.isNotValid(num)) {
395
            return num as number;
396
        }
397
        const numStr = String(num);
398
        if (numStr.indexOf('.') >= 0 && !isUndefinedOrNull(this.thyPrecision)) {
399
            return Number(Number(num).toFixed(this.thyPrecision));
400
        }
401
        return Number(num);
402
    }
403

404
    isInputNumber(value: string) {
405
        return isFloat(value) || /^[-+Ee]$|^([-+.]?[0-9])*(([.]|[.eE])?[eE]?[+-]?)?$|^$/.test(value);
406
    }
407

408
    ngOnDestroy() {
409
        this.hostFocusControl.destroy();
410
    }
411
}
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