diff --git a/components/input-number-legacy/input-number.component.ts b/components/input-number-legacy/input-number.component.ts
index 0787fc42af5..6821f85733f 100644
--- a/components/input-number-legacy/input-number.component.ts
+++ b/components/input-number-legacy/input-number.component.ts
@@ -444,7 +444,6 @@ export class NzInputNumberLegacyComponent implements ControlValueAccessor, After
.monitor(this.elementRef, true)
.pipe(takeUntil(this.destroy$))
.subscribe(focusOrigin => {
- console.log(focusOrigin);
if (!focusOrigin) {
this.isFocused = false;
this.updateDisplayValue(this.value!);
diff --git a/components/input-number/demo/formatter.ts b/components/input-number/demo/formatter.ts
index b4cbf312b03..13d90d19328 100644
--- a/components/input-number/demo/formatter.ts
+++ b/components/input-number/demo/formatter.ts
@@ -7,15 +7,9 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number';
selector: 'nz-demo-input-number-formatter',
imports: [FormsModule, NzInputNumberModule],
template: `
+
- `${value} %`;
- parserPercent = (value: string): number => +value.replace(' %', '');
- formatterDollar = (value: number): string => `$ ${value}`;
- parserDollar = (value: string): number => +value.replace('$ ', '');
+ dollarValue = 1000;
+ percentValue = 100;
+ formatterDollar = (value: number): string => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+ parserDollar = (value: string): number => parseFloat(value?.replace(/\$\s?|(,*)/g, ''));
+ formatterPercent = (value: number): string => `${value}%`;
+ parserPercent = (value: string): number => parseFloat(value?.replace('%', ''));
}
diff --git a/components/input-number/input-number.component.spec.ts b/components/input-number/input-number.component.spec.ts
index b55c7c9217f..d1a80b95e0b 100644
--- a/components/input-number/input-number.component.spec.ts
+++ b/components/input-number/input-number.component.spec.ts
@@ -3,7 +3,7 @@
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
*/
-import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
+import { DOWN_ARROW, ENTER, UP_ARROW } from '@angular/cdk/keycodes';
import { Component, ElementRef, viewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
@@ -134,19 +134,93 @@ describe('Input number', () => {
});
});
- it('should be update value through user typing', () => {
- component.min = 1;
- component.max = 2;
- fixture.detectChanges();
+ describe('should be update value through user typing', () => {
+ it('normal', () => {
+ input('123');
+ expect(component.value).toBe(123);
+ enter();
+ expect(component.value).toBe(123);
+ blur();
+ expect(component.value).toBe(123);
+
+ input('NonNumber');
+ expect(component.value).toBe(123);
+ enter();
+ expect(component.value).toBe(123);
+ blur();
+ expect(component.value).toBe(123);
+
+ input('');
+ expect(component.value).toBe(null);
+ enter();
+ expect(component.value).toBe(null);
+ blur();
+ expect(component.value).toBe(null);
+ });
- userTypingInput('3');
- expect(component.value).toBe(2);
- userTypingInput('0');
- expect(component.value).toBe(1);
- userTypingInput('1');
- expect(component.value).toBe(1);
- userTypingInput('abc');
- expect(component.value).toBe(null);
+ it('with range', () => {
+ component.min = 1;
+ component.max = 10;
+ fixture.detectChanges();
+
+ input('1');
+ expect(component.value).toBe(1);
+
+ input('99');
+ expect(component.value).toBe(1);
+ blur();
+ expect(component.value).toBe(10);
+
+ input('-99');
+ expect(component.value).toBe(10);
+ blur();
+ expect(component.value).toBe(1);
+
+ input('10');
+ expect(component.value).toBe(10);
+ blur();
+ expect(component.value).toBe(10);
+
+ input('');
+ expect(component.value).toBe(null);
+ blur();
+ expect(component.value).toBe(null);
+ });
+
+ it('with formatter', () => {
+ component.formatter = (value: number): string => `${value}%`;
+ component.parser = (value: string): number => parseFloat(value?.replace('%', ''));
+ fixture.detectChanges();
+
+ const inputElement = getInputElement();
+
+ input('123');
+ fixture.detectChanges();
+ expect(component.value).toBe(123);
+ expect(inputElement.value).toBe('123%');
+ blur();
+ fixture.detectChanges();
+ expect(component.value).toBe(123);
+ expect(inputElement.value).toBe('123%');
+
+ input('NonNumber');
+ fixture.detectChanges();
+ expect(component.value).toBe(123);
+ expect(inputElement.value).toBe('NonNumber');
+ blur();
+ fixture.detectChanges();
+ expect(component.value).toBe(123);
+ expect(inputElement.value).toBe('123%');
+
+ input('');
+ fixture.detectChanges();
+ expect(component.value).toBe(null);
+ expect(inputElement.value).toBe('');
+ blur();
+ fixture.detectChanges();
+ expect(component.value).toBe(null);
+ expect(inputElement.value).toBe('');
+ });
});
it('should be apply out-of-range class', async () => {
@@ -163,56 +237,85 @@ describe('Input number', () => {
expect(hostElement.classList).toContain('ant-input-number-out-of-range');
});
- it('should be set min and max with precision', () => {
- component.precision = 0;
+ describe('should be set min and max with precision', () => {
+ beforeEach(() => {
+ component.precision = 0;
+ component.value = null;
+ });
- // max > 0
- component.min = Number.MIN_SAFE_INTEGER;
- component.max = 1.5;
- fixture.detectChanges();
- userTypingInput('1.1');
- expect(component.value).toBe(1);
- userTypingInput('1.5');
- expect(component.value).toBe(1);
+ it('max > 0', () => {
+ component.min = Number.MIN_SAFE_INTEGER;
+ component.max = 1.5;
+ fixture.detectChanges();
- // max < 0
- component.min = Number.MIN_SAFE_INTEGER;
- component.max = -1.5;
- fixture.detectChanges();
- userTypingInput('-1.1');
- expect(component.value).toBe(-2);
- userTypingInput('-1.5');
- expect(component.value).toBe(-2);
-
- // min > 0
- component.min = 1.5;
- component.max = Number.MAX_SAFE_INTEGER;
- fixture.detectChanges();
- userTypingInput('1.1');
- expect(component.value).toBe(2);
- userTypingInput('1.5');
- expect(component.value).toBe(2);
+ input('1.1');
+ expect(component.value).toBe(1.1);
+ blur();
+ expect(component.value).toBe(1);
+ input('1.5');
+ expect(component.value).toBe(1.5);
+ blur();
+ expect(component.value).toBe(1);
+ });
- // min < 0
- component.min = -1.5;
- component.max = Number.MAX_SAFE_INTEGER;
- fixture.detectChanges();
- userTypingInput('-1.1');
- expect(component.value).toBe(-1);
- userTypingInput('-1.5');
- expect(component.value).toBe(-1);
+ it('max < 0', () => {
+ component.min = Number.MIN_SAFE_INTEGER;
+ component.max = -1.5;
+ fixture.detectChanges();
+
+ input('-1.1');
+ expect(component.value).toBe(null);
+ blur();
+ expect(component.value).toBe(-2);
+ input('-1.5');
+ expect(component.value).toBe(-1.5);
+ blur();
+ expect(component.value).toBe(-2);
+ });
+
+ it('min > 0', () => {
+ component.min = 1.5;
+ component.max = Number.MAX_SAFE_INTEGER;
+ fixture.detectChanges();
+
+ input('1.1');
+ expect(component.value).toBe(null);
+ blur();
+ expect(component.value).toBe(2);
+ input('1.5');
+ expect(component.value).toBe(1.5);
+ blur();
+ expect(component.value).toBe(2);
+ });
+
+ it('min < 0', () => {
+ component.min = -1.5;
+ component.max = Number.MAX_SAFE_INTEGER;
+ fixture.detectChanges();
+
+ input('-1.1');
+ expect(component.value).toBe(-1.1);
+ blur();
+ expect(component.value).toBe(-1);
+ input('-1.5');
+ expect(component.value).toBe(-1.5);
+ blur();
+ expect(component.value).toBe(-1);
+ });
});
- it('should set precision', async () => {
+ it('should set value with precision', async () => {
component.precision = 1;
- component.value = 1.23;
fixture.detectChanges();
- await fixture.whenStable();
+
+ input('1.23');
+ expect(component.value).toBe(1.23);
+ blur();
expect(component.value).toBe(1.2);
- component.value = 1.25;
- fixture.detectChanges();
- await fixture.whenStable();
+ input('1.25');
+ expect(component.value).toBe(1.25);
+ blur();
expect(component.value).toBe(1.3);
});
@@ -284,16 +387,23 @@ describe('Input number', () => {
function upStepByKeyboard(): void {
hostElement.dispatchEvent(new KeyboardEvent('keydown', { keyCode: UP_ARROW }));
}
-
function downStepByKeyboard(): void {
hostElement.dispatchEvent(new KeyboardEvent('keydown', { keyCode: DOWN_ARROW }));
}
+ function enter(): void {
+ hostElement.dispatchEvent(new KeyboardEvent('keydown', { keyCode: ENTER }));
+ }
- function userTypingInput(text: string): void {
- const input = hostElement.querySelector('input')!;
- input.value = text;
- input.dispatchEvent(new Event('input'));
- input.dispatchEvent(new Event('change'));
+ function getInputElement(): HTMLInputElement {
+ return fixture.nativeElement.querySelector('input')!;
+ }
+ function input(text: string): void {
+ const element = getInputElement();
+ element.value = text;
+ element.dispatchEvent(new Event('input'));
+ }
+ function blur(): void {
+ getInputElement().dispatchEvent(new Event('blur'));
}
});
@@ -359,6 +469,8 @@ describe('Input number with affixes or addons', () => {
[nzBordered]="bordered"
[nzKeyboard]="keyboard"
[nzControls]="controls"
+ [nzParser]="parser"
+ [nzFormatter]="formatter"
[(ngModel)]="value"
[disabled]="controlDisabled"
/>
@@ -378,6 +490,8 @@ class InputNumberTestComponent {
bordered = true;
keyboard = true;
controls = true;
+ parser: ((value: string) => number) | undefined = undefined;
+ formatter: ((value: number) => string) | undefined = undefined;
value: number | null = null;
controlDisabled = false;
diff --git a/components/input-number/input-number.component.ts b/components/input-number/input-number.component.ts
index c903406ccaf..4e7f34852f6 100644
--- a/components/input-number/input-number.component.ts
+++ b/components/input-number/input-number.component.ts
@@ -5,7 +5,7 @@
import { FocusMonitor } from '@angular/cdk/a11y';
import { Directionality } from '@angular/cdk/bidi';
-import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
+import { DOWN_ARROW, ENTER, UP_ARROW } from '@angular/cdk/keycodes';
import { NgTemplateOutlet } from '@angular/common';
import {
afterNextRender,
@@ -34,7 +34,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NzFormItemFeedbackIconComponent, NzFormStatusService } from 'ng-zorro-antd/core/form';
import { NzSizeLDSType, NzStatus, NzValidateStatus, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types';
-import { getStatusClassNames, isNil } from 'ng-zorro-antd/core/util';
+import { getStatusClassNames, isNil, isNotNil } from 'ng-zorro-antd/core/util';
import { NzIconModule } from 'ng-zorro-antd/icon';
import {
NzInputAddonAfterDirective,
@@ -147,13 +147,13 @@ import { NZ_SPACE_COMPACT_ITEM_TYPE, NZ_SPACE_COMPACT_SIZE, NzSpaceCompactItemDi
[attr.aria-valuemin]="nzMin()"
[attr.aria-valuemax]="nzMax()"
[attr.id]="nzId()"
- [value]="displayValue()"
[attr.step]="nzStep()"
+ [attr.value]="displayValue()"
+ [value]="displayValue()"
[placeholder]="nzPlaceHolder() ?? ''"
[disabled]="finalDisabled()"
[readOnly]="nzReadOnly()"
- (input)="displayValue.set(input.value)"
- (change)="onInputChange($event)"
+ (input)="onInput(input.value)"
/>
@@ -183,21 +183,8 @@ export class NzInputNumberComponent implements OnInit, ControlValueAccessor {
readonly nzMin = input(Number.MIN_SAFE_INTEGER, { transform: numberAttribute });
readonly nzMax = input(Number.MAX_SAFE_INTEGER, { transform: numberAttribute });
readonly nzPrecision = input(null);
- readonly nzParser = input<(value: string) => number>(value => {
- const parsedValue = defaultParser(value);
- const precision = this.nzPrecision();
- if (!isNil(precision)) {
- return +parsedValue.toFixed(precision);
- }
- return parsedValue;
- });
- readonly nzFormatter = input<(value: number) => string>(value => {
- const precision = this.nzPrecision();
- if (!isNil(precision)) {
- return value.toFixed(precision);
- }
- return value.toString();
- });
+ readonly nzParser = input<((value: string) => number) | null>();
+ readonly nzFormatter = input<((value: number) => string) | null>();
readonly nzDisabled = input(false, { transform: booleanAttribute });
readonly nzReadOnly = input(false, { transform: booleanAttribute });
readonly nzAutoFocus = input(false, { transform: booleanAttribute });
@@ -219,6 +206,13 @@ export class NzInputNumberComponent implements OnInit, ControlValueAccessor {
private directionality = inject(Directionality);
private nzFormStatusService = inject(NzFormStatusService, { optional: true });
private autoStepTimer: ReturnType | null = null;
+ private defaultFormater = (value: number): string => {
+ const precision = this.nzPrecision();
+ if (isNotNil(precision)) {
+ return value.toFixed(precision);
+ }
+ return value.toString();
+ };
protected value = signal(null);
protected displayValue = signal('');
@@ -307,6 +301,7 @@ export class NzInputNumberComponent implements OnInit, ControlValueAccessor {
this.focused.set(!!origin);
if (!origin) {
+ this.fixValue();
this.onTouched();
}
});
@@ -329,7 +324,10 @@ export class NzInputNumberComponent implements OnInit, ControlValueAccessor {
}
writeValue(value: number | null): void {
- untracked(() => this.setValue(value));
+ untracked(() => {
+ this.value.set(value);
+ this.setValue(value);
+ });
}
registerOnChange(fn: OnChangeType): void {
@@ -364,10 +362,17 @@ export class NzInputNumberComponent implements OnInit, ControlValueAccessor {
if (!up) {
step = -step;
}
+
const places = getDecimalPlaces(step);
- const multiple = Math.pow(10, places);
- // Convert floating point numbers to integers to avoid floating point math errors
- this.setValue((Math.round((this.value() || 0) * multiple) + Math.round(step * multiple)) / multiple, true);
+ const multiple = 10 ** places;
+ const nextValue = getRangeValue(
+ // Convert floating point numbers to integers to avoid floating point math errors
+ (Math.round((this.value() || 0) * multiple) + Math.round(step * multiple)) / multiple,
+ this.nzMin(),
+ this.nzMax(),
+ this.nzPrecision()
+ );
+ this.setValue(nextValue);
this.nzOnStep.emit({
type: up ? 'up' : 'down',
@@ -378,27 +383,78 @@ export class NzInputNumberComponent implements OnInit, ControlValueAccessor {
this.focus();
}
- private setValue(value: number | string | null, userTyping?: boolean): void {
- let parsedValue: number | null = null;
+ private setValue(value: number | null): void {
+ const formatter = this.nzFormatter() ?? this.defaultFormater;
+ const precision = this.nzPrecision();
- if (!isNil(value)) {
- parsedValue = this.nzParser()(value.toString());
+ if (isNotNil(precision)) {
+ value &&= +value.toFixed(precision);
+ }
- // If the user is typing, we need to make sure the value is in the range.
- // Instead, we allow values to be set out of range programmatically,
- // and display out-of-range values as errors.
- if (userTyping) {
- if (Number.isNaN(parsedValue)) {
- parsedValue = null;
- } else {
- parsedValue = getRangeValueWithPrecision(parsedValue, this.nzMin(), this.nzMax(), this.nzPrecision());
- }
+ const formatedValue = value === null ? '' : formatter(value);
+ this.displayValue.set(formatedValue);
+ this.updateValue(value);
+ }
+
+ private setValueByTyping(value: string): void {
+ if (value === '') {
+ this.displayValue.set('');
+ this.updateValue(null);
+ return;
+ }
+
+ const parser = this.nzParser() ?? defaultParser;
+ const parsedValue = parser(value);
+
+ if (isNotCompleteNumber(value) || Number.isNaN(parsedValue)) {
+ this.displayValue.set(value);
+ return;
+ }
+
+ const formattedValue = this.nzFormatter()?.(parsedValue) ?? parsedValue.toString();
+ this.displayValue.set(formattedValue);
+
+ if (!isInRange(parsedValue, this.nzMin(), this.nzMax())) {
+ return;
+ }
+
+ this.updateValue(parsedValue);
+ }
+
+ private updateValue(value: number | null): void {
+ if (this.value() !== value) {
+ this.value.set(value);
+ this.onChange(value);
+ }
+ }
+
+ private fixValue(): void {
+ const displayValue = this.displayValue();
+
+ if (displayValue === '') {
+ return;
+ }
+
+ const parser = this.nzParser() ?? defaultParser;
+ let fixedValue: number | null = parser(displayValue);
+
+ // If parsing fails, revert to the previous value
+ if (Number.isNaN(fixedValue)) {
+ fixedValue = this.value();
+ } else {
+ const precision = this.nzPrecision();
+ // fix precision
+ if (isNotNil(precision) && getDecimalPlaces(fixedValue) !== precision) {
+ fixedValue = +fixedValue.toFixed(precision);
+ }
+
+ // fix range
+ if (!isInRange(fixedValue, this.nzMin(), this.nzMax())) {
+ fixedValue = getRangeValue(fixedValue, this.nzMin(), this.nzMax(), precision);
}
}
- this.value.set(parsedValue);
- this.displayValue.set(parsedValue === null ? '' : this.nzFormatter()(parsedValue));
- this.onChange(parsedValue);
+ this.setValue(fixedValue);
}
protected stopAutoStep(): void {
@@ -434,12 +490,14 @@ export class NzInputNumberComponent implements OnInit, ControlValueAccessor {
event.preventDefault();
this.nzKeyboard() && this.step(event, false);
break;
+ case ENTER:
+ this.fixValue();
+ break;
}
}
- protected onInputChange(value: Event): void {
- const target = value.target as HTMLInputElement;
- this.setValue(target.value, true);
+ protected onInput(value: string): void {
+ this.setValueByTyping(value);
}
}
@@ -455,35 +513,29 @@ const STEP_DELAY = 600;
function defaultParser(value: string): number {
return +value.trim().replace(/。/g, '.');
- // [Legacy] We still support auto convert `$ 123,456` to `123456`
- // .replace(/[^\w.-]+/g, '');
}
function isInRange(value: number, min: number, max: number): boolean {
return value >= min && value <= max;
}
-function getRangeValue(value: number, min: number, max: number): number {
- if (value < min) {
- return min;
- }
-
- if (value > max) {
- return max;
- }
-
- return value;
-}
-
/**
* if max > 0, round down with precision. Example: input= 3.5, max= 3.5, precision=0; output= 3
- * if max < 0, round up with precision. Example: input=-3.5, max=-3.5, precision=0; output=-4
- * if min > 0, round up with precision. Example: input= 3.5, min= 3.5, precision=0; output= 4
+ * if max < 0, round up with precision. Example: input=-3.5, max=-3.5, precision=0; output=-4
+ * if min > 0, round up with precision. Example: input= 3.5, min= 3.5, precision=0; output= 4
* if min < 0, round down with precision. Example: input=-3.5, min=-3.5, precision=0; output=-3
*/
-function getRangeValueWithPrecision(value: number, min: number, max: number, precision: number | null): number {
+function getRangeValue(value: number, min: number, max: number, precision: number | null = null): number {
if (precision === null) {
- return getRangeValue(value, min, max);
+ if (value < min) {
+ return min;
+ }
+
+ if (value > max) {
+ return max;
+ }
+
+ return value;
}
const fixedValue = +value.toFixed(precision);
@@ -503,3 +555,7 @@ function getRangeValueWithPrecision(value: number, min: number, max: number, pre
function getDecimalPlaces(num: number): number {
return num.toString().split('.')[1]?.length || 0;
}
+
+function isNotCompleteNumber(value: string | number): boolean {
+ return /[.。]$/.test(value.toString());
+}