From df55d8882c9f36bc6a0cd8a4d752e03070658ff7 Mon Sep 17 00:00:00 2001 From: HyperLife1119 Date: Wed, 4 Dec 2024 07:45:11 +0800 Subject: [PATCH] feat: redesign the input-number (#8901) * refactor(module:input-number): rename to input-number-legacy * docs: support deprecated tag * refactor(module:core): deprecated NzFormPatchModule * refactor(module:color-picker): fix input number parser type * feat(module:input): add addon & affix directives * feat(module:input-number): redesign the input-number --- .../color-picker/color-format.component.ts | 2 +- components/core/form/nz-form-patch.module.ts | 3 + components/input-number-legacy/demo/addon.md | 14 + components/input-number-legacy/demo/addon.ts | 49 + components/input-number-legacy/demo/basic.md | 15 + components/input-number-legacy/demo/basic.ts | 19 + .../input-number-legacy/demo/borderless.md | 15 + .../input-number-legacy/demo/borderless.ts | 14 + components/input-number-legacy/demo/digit.md | 15 + components/input-number-legacy/demo/digit.ts | 22 + .../input-number-legacy/demo/disabled.md | 16 + .../input-number-legacy/demo/disabled.ts | 33 + .../input-number-legacy/demo/formatter.md | 15 + .../input-number-legacy/demo/formatter.ts | 42 + .../demo/group.md | 0 .../demo/group.ts | 38 +- .../demo/precision.md | 0 .../demo/precision.ts | 24 +- components/input-number-legacy/demo/prefix.md | 14 + components/input-number-legacy/demo/prefix.ts | 25 + components/input-number-legacy/demo/size.md | 15 + components/input-number-legacy/demo/size.ts | 37 + components/input-number-legacy/demo/status.md | 14 + components/input-number-legacy/demo/status.ts | 24 + .../input-number-legacy/doc/index.en-US.md | 68 ++ .../input-number-legacy/doc/index.zh-CN.md | 69 ++ components/input-number-legacy/index.ts | 6 + .../input-number-group-slot.component.ts | 3 + .../input-number-group.component.ts | 25 +- .../input-number-group.spec.ts | 46 +- .../input-number.component.ts | 551 ++++++++++++ .../input-number.module.ts | 27 + .../input-number.spec.ts | 51 +- .../input-number-legacy/ng-package.json | 5 + components/input-number-legacy/public-api.ts | 9 + components/input-number/demo/addon.md | 2 +- components/input-number/demo/addon.ts | 70 +- components/input-number/demo/basic.ts | 2 +- components/input-number/demo/borderless.md | 3 +- components/input-number/demo/borderless.ts | 2 +- components/input-number/demo/digit.md | 3 +- components/input-number/demo/digit.ts | 12 +- components/input-number/demo/disabled.md | 4 +- components/input-number/demo/disabled.ts | 16 +- components/input-number/demo/formatter.md | 3 +- components/input-number/demo/formatter.ts | 18 +- components/input-number/demo/handler-icon.md | 14 + components/input-number/demo/handler-icon.ts | 20 + components/input-number/demo/keyboard.md | 15 + components/input-number/demo/keyboard.ts | 26 + components/input-number/demo/out-of-range.md | 14 + components/input-number/demo/out-of-range.ts | 14 + components/input-number/demo/prefix.md | 2 +- components/input-number/demo/prefix.ts | 36 +- components/input-number/demo/size.ts | 6 +- components/input-number/demo/status.md | 4 +- components/input-number/demo/status.ts | 31 +- components/input-number/doc/index.en-US.md | 64 +- components/input-number/doc/index.zh-CN.md | 62 +- .../input-number.component.spec.ts | 384 ++++++++ .../input-number/input-number.component.ts | 849 +++++++++--------- .../input-number/input-number.module.ts | 25 +- components/input-number/public-api.ts | 2 - components/input-number/style/patch.less | 2 +- components/input/input-addon.directive.ts | 18 + components/input/input-affix.directive.ts | 18 + components/input/public-api.ts | 11 +- .../space/space-compact.component.spec.ts | 9 + .../_site/doc/app/side/side.component.html | 34 +- .../site/_site/doc/app/side/side.component.ts | 3 +- 70 files changed, 2388 insertions(+), 735 deletions(-) create mode 100644 components/input-number-legacy/demo/addon.md create mode 100644 components/input-number-legacy/demo/addon.ts create mode 100755 components/input-number-legacy/demo/basic.md create mode 100644 components/input-number-legacy/demo/basic.ts create mode 100644 components/input-number-legacy/demo/borderless.md create mode 100644 components/input-number-legacy/demo/borderless.ts create mode 100755 components/input-number-legacy/demo/digit.md create mode 100644 components/input-number-legacy/demo/digit.ts create mode 100755 components/input-number-legacy/demo/disabled.md create mode 100644 components/input-number-legacy/demo/disabled.ts create mode 100755 components/input-number-legacy/demo/formatter.md create mode 100644 components/input-number-legacy/demo/formatter.ts rename components/{input-number => input-number-legacy}/demo/group.md (100%) rename components/{input-number => input-number-legacy}/demo/group.ts (56%) rename components/{input-number => input-number-legacy}/demo/precision.md (100%) rename components/{input-number => input-number-legacy}/demo/precision.ts (55%) create mode 100644 components/input-number-legacy/demo/prefix.md create mode 100644 components/input-number-legacy/demo/prefix.ts create mode 100755 components/input-number-legacy/demo/size.md create mode 100644 components/input-number-legacy/demo/size.ts create mode 100644 components/input-number-legacy/demo/status.md create mode 100644 components/input-number-legacy/demo/status.ts create mode 100755 components/input-number-legacy/doc/index.en-US.md create mode 100755 components/input-number-legacy/doc/index.zh-CN.md create mode 100644 components/input-number-legacy/index.ts rename components/{input-number => input-number-legacy}/input-number-group-slot.component.ts (92%) rename components/{input-number => input-number-legacy}/input-number-group.component.ts (93%) rename components/{input-number => input-number-legacy}/input-number-group.spec.ts (93%) create mode 100644 components/input-number-legacy/input-number.component.ts create mode 100644 components/input-number-legacy/input-number.module.ts rename components/{input-number => input-number-legacy}/input-number.spec.ts (95%) create mode 100644 components/input-number-legacy/ng-package.json create mode 100644 components/input-number-legacy/public-api.ts create mode 100644 components/input-number/demo/handler-icon.md create mode 100644 components/input-number/demo/handler-icon.ts create mode 100644 components/input-number/demo/keyboard.md create mode 100644 components/input-number/demo/keyboard.ts create mode 100644 components/input-number/demo/out-of-range.md create mode 100644 components/input-number/demo/out-of-range.ts create mode 100644 components/input-number/input-number.component.spec.ts create mode 100644 components/input/input-addon.directive.ts create mode 100644 components/input/input-affix.directive.ts diff --git a/components/color-picker/color-format.component.ts b/components/color-picker/color-format.component.ts index 482b784f6dc..53e2830c5f4 100644 --- a/components/color-picker/color-format.component.ts +++ b/components/color-picker/color-format.component.ts @@ -162,7 +162,7 @@ export class NzColorFormatComponent implements OnChanges, OnInit, OnDestroy { }>; formatterPercent = (value: number): string => `${value} %`; - parserPercent = (value: string): string => value.replace(' %', ''); + parserPercent = (value: string): number => +value.replace(' %', ''); constructor(private formBuilder: FormBuilder) { this.validateForm = this.formBuilder.nonNullable.group({ diff --git a/components/core/form/nz-form-patch.module.ts b/components/core/form/nz-form-patch.module.ts index c1eca3df242..a25dcf366e8 100644 --- a/components/core/form/nz-form-patch.module.ts +++ b/components/core/form/nz-form-patch.module.ts @@ -7,6 +7,9 @@ import { NgModule } from '@angular/core'; import { NzFormItemFeedbackIconComponent } from './nz-form-item-feedback-icon.component'; +/** + * @deprecated Will be removed in v20. Use NzFormItemFeedbackIconComponent directly + */ @NgModule({ imports: [NzFormItemFeedbackIconComponent], exports: [NzFormItemFeedbackIconComponent] diff --git a/components/input-number-legacy/demo/addon.md b/components/input-number-legacy/demo/addon.md new file mode 100644 index 00000000000..c45680cd912 --- /dev/null +++ b/components/input-number-legacy/demo/addon.md @@ -0,0 +1,14 @@ +--- +order: 7 +title: + zh-CN: 前置/后置标签 + en-US: Pre / Post tab +--- + +## zh-CN + +用于配置一些固定组合。 + +## en-US + +Using pre & post tabs example. diff --git a/components/input-number-legacy/demo/addon.ts b/components/input-number-legacy/demo/addon.ts new file mode 100644 index 00000000000..52f9f3e6369 --- /dev/null +++ b/components/input-number-legacy/demo/addon.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzCascaderModule } from 'ng-zorro-antd/cascader'; +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; +import { NzSelectModule } from 'ng-zorro-antd/select'; +import { NzSpaceModule } from 'ng-zorro-antd/space'; + +@Component({ + selector: 'nz-demo-input-number-legacy-addon', + standalone: true, + imports: [FormsModule, NzCascaderModule, NzInputNumberLegacyModule, NzSelectModule, NzSpaceModule], + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` +}) +export class NzDemoInputNumberLegacyAddonComponent { + value = 100; +} diff --git a/components/input-number-legacy/demo/basic.md b/components/input-number-legacy/demo/basic.md new file mode 100755 index 00000000000..14266de7bc8 --- /dev/null +++ b/components/input-number-legacy/demo/basic.md @@ -0,0 +1,15 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +数字输入框。 + +## en-US + +Numeric-only input box. + diff --git a/components/input-number-legacy/demo/basic.ts b/components/input-number-legacy/demo/basic.ts new file mode 100644 index 00000000000..402c0f5938b --- /dev/null +++ b/components/input-number-legacy/demo/basic.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; + +@Component({ + selector: 'nz-demo-input-number-legacy-basic', + standalone: true, + imports: [FormsModule, NzInputNumberLegacyModule], + template: `` +}) +export class NzDemoInputNumberLegacyBasicComponent { + value = 3; +} diff --git a/components/input-number-legacy/demo/borderless.md b/components/input-number-legacy/demo/borderless.md new file mode 100644 index 00000000000..4f3ea028a8a --- /dev/null +++ b/components/input-number-legacy/demo/borderless.md @@ -0,0 +1,15 @@ +--- +order: 0 +title: + zh-CN: 无边框 + en-US: Borderless +--- + +## zh-CN + +没有边框。 + +## en-US + +Borderless input number. + diff --git a/components/input-number-legacy/demo/borderless.ts b/components/input-number-legacy/demo/borderless.ts new file mode 100644 index 00000000000..df0d262b45a --- /dev/null +++ b/components/input-number-legacy/demo/borderless.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; + +@Component({ + selector: 'nz-demo-input-number-legacy-borderless', + standalone: true, + imports: [FormsModule, NzInputNumberLegacyModule], + template: `` +}) +export class NzDemoInputNumberLegacyBorderlessComponent { + value = 3; +} diff --git a/components/input-number-legacy/demo/digit.md b/components/input-number-legacy/demo/digit.md new file mode 100755 index 00000000000..94345c264c1 --- /dev/null +++ b/components/input-number-legacy/demo/digit.md @@ -0,0 +1,15 @@ +--- +order: 3 +title: + zh-CN: 小数 + en-US: Decimals +--- + +## zh-CN + +和原生的数字输入框一样,value 的精度由 `nzStep` 的小数位数决定。 + +## en-US + +A numeric-only input box whose values can be increased or decreased using a decimal step. The number of decimals (also known as precision) is determined by the `nzStep` prop. + diff --git a/components/input-number-legacy/demo/digit.ts b/components/input-number-legacy/demo/digit.ts new file mode 100644 index 00000000000..6c2b26e779d --- /dev/null +++ b/components/input-number-legacy/demo/digit.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; + +@Component({ + selector: 'nz-demo-input-number-legacy-digit', + standalone: true, + imports: [FormsModule, NzInputNumberLegacyModule], + template: ` + + ` +}) +export class NzDemoInputNumberLegacyDigitComponent { + value = 0; +} diff --git a/components/input-number-legacy/demo/disabled.md b/components/input-number-legacy/demo/disabled.md new file mode 100755 index 00000000000..8b33efef108 --- /dev/null +++ b/components/input-number-legacy/demo/disabled.md @@ -0,0 +1,16 @@ +--- +order: 2 +title: + zh-CN: 不可用 + en-US: Disabled +--- + +## zh-CN + +点击按钮切换可用状态。 + +## en-US + +Click the button to toggle between available and disabled states. + + diff --git a/components/input-number-legacy/demo/disabled.ts b/components/input-number-legacy/demo/disabled.ts new file mode 100644 index 00000000000..c29c3779912 --- /dev/null +++ b/components/input-number-legacy/demo/disabled.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; + +@Component({ + selector: 'nz-demo-input-number-legacy-disabled', + standalone: true, + imports: [FormsModule, NzButtonModule, NzInputNumberLegacyModule], + template: ` + +
+
+ + ` +}) +export class NzDemoInputNumberLegacyDisabledComponent { + value = 3; + isDisabled = false; + + toggleDisabled(): void { + this.isDisabled = !this.isDisabled; + } +} diff --git a/components/input-number-legacy/demo/formatter.md b/components/input-number-legacy/demo/formatter.md new file mode 100755 index 00000000000..c4c2d0432f5 --- /dev/null +++ b/components/input-number-legacy/demo/formatter.md @@ -0,0 +1,15 @@ +--- +order: 4 +title: + zh-CN: 格式化展示 + en-US: Formatter +--- + +## zh-CN + +通过 `nzFormatter` 格式化数字,以展示具有具体含义的数据,往往需要配合 `nzParser` 一起使用。 + +## en-US + +Display value within it's situation with `nzFormatter`, and we usually use `nzParser` at the same time. + diff --git a/components/input-number-legacy/demo/formatter.ts b/components/input-number-legacy/demo/formatter.ts new file mode 100644 index 00000000000..21bb46d49a6 --- /dev/null +++ b/components/input-number-legacy/demo/formatter.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; + +@Component({ + selector: 'nz-demo-input-number-legacy-formatter', + standalone: true, + imports: [FormsModule, NzInputNumberLegacyModule], + template: ` + + + `, + styles: [ + ` + nz-input-number { + margin-right: 8px; + } + ` + ] +}) +export class NzDemoInputNumberLegacyFormatterComponent { + demoValue = 100; + formatterPercent = (value: number): string => `${value} %`; + parserPercent = (value: string): string => value.replace(' %', ''); + formatterDollar = (value: number): string => `$ ${value}`; + parserDollar = (value: string): string => value.replace('$ ', ''); +} diff --git a/components/input-number/demo/group.md b/components/input-number-legacy/demo/group.md similarity index 100% rename from components/input-number/demo/group.md rename to components/input-number-legacy/demo/group.md diff --git a/components/input-number/demo/group.ts b/components/input-number-legacy/demo/group.ts similarity index 56% rename from components/input-number/demo/group.ts rename to components/input-number-legacy/demo/group.ts index 55d2e5176a5..3f93ab89680 100644 --- a/components/input-number/demo/group.ts +++ b/components/input-number-legacy/demo/group.ts @@ -4,42 +4,52 @@ import { FormsModule } from '@angular/forms'; import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzDatePickerModule } from 'ng-zorro-antd/date-picker'; import { NzGridModule } from 'ng-zorro-antd/grid'; -import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; import { NzSelectModule } from 'ng-zorro-antd/select'; import { NzSpaceModule } from 'ng-zorro-antd/space'; @Component({ - selector: 'nz-demo-input-number-group', + selector: 'nz-demo-input-number-legacy-group', standalone: true, imports: [ FormsModule, NzButtonModule, NzDatePickerModule, NzGridModule, - NzInputNumberModule, + NzInputNumberLegacyModule, NzSelectModule, - NzSpaceModule + NzSpaceModule, + NzIconModule ], template: `
- +
- +
- - + + - + - + @@ -49,17 +59,17 @@ import { NzSpaceModule } from 'ng-zorro-antd/space'; - + - + - +
` }) -export class NzDemoInputNumberGroupComponent {} +export class NzDemoInputNumberLegacyGroupComponent {} diff --git a/components/input-number/demo/precision.md b/components/input-number-legacy/demo/precision.md similarity index 100% rename from components/input-number/demo/precision.md rename to components/input-number-legacy/demo/precision.md diff --git a/components/input-number/demo/precision.ts b/components/input-number-legacy/demo/precision.ts similarity index 55% rename from components/input-number/demo/precision.ts rename to components/input-number-legacy/demo/precision.ts index 62b98b582fc..985eade88f0 100644 --- a/components/input-number/demo/precision.ts +++ b/components/input-number-legacy/demo/precision.ts @@ -1,36 +1,40 @@ import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; @Component({ - selector: 'nz-demo-input-number-precision', + selector: 'nz-demo-input-number-legacy-precision', standalone: true, - imports: [FormsModule, NzInputNumberModule], + imports: [FormsModule, NzInputNumberLegacyModule], template: ` - - + - + + > `, styles: [ ` - nz-input-number { + nz-input-number-legacy { margin-right: 8px; } ` ] }) -export class NzDemoInputNumberPrecisionComponent { +export class NzDemoInputNumberLegacyPrecisionComponent { toFixedValue = 2; cutValue = 2; customFnValue = 2; diff --git a/components/input-number-legacy/demo/prefix.md b/components/input-number-legacy/demo/prefix.md new file mode 100644 index 00000000000..c1207bfc413 --- /dev/null +++ b/components/input-number-legacy/demo/prefix.md @@ -0,0 +1,14 @@ +--- +order: 8 +title: + zh-CN: 前缀 + en-US: Prefix +--- + +## zh-CN + +在数字输入框上添加前缀图标。 + +## en-US + +Add a prefix inside input. diff --git a/components/input-number-legacy/demo/prefix.ts b/components/input-number-legacy/demo/prefix.ts new file mode 100644 index 00000000000..d6530b7b86f --- /dev/null +++ b/components/input-number-legacy/demo/prefix.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; +import { NzSpaceModule } from 'ng-zorro-antd/space'; + +@Component({ + selector: 'nz-demo-input-number-legacy-prefix', + standalone: true, + imports: [FormsModule, NzInputNumberLegacyModule, NzSpaceModule], + template: ` + + + + + + + + + + + + ` +}) +export class NzDemoInputNumberLegacyPrefixComponent {} diff --git a/components/input-number-legacy/demo/size.md b/components/input-number-legacy/demo/size.md new file mode 100755 index 00000000000..98f9e9537f7 --- /dev/null +++ b/components/input-number-legacy/demo/size.md @@ -0,0 +1,15 @@ +--- +order: 1 +title: + zh-CN: 三种大小 + en-US: Sizes +--- + +## zh-CN + +三种大小的数字输入框,当 `nzSize` 分别为 `large` 和 `small` 时,输入框高度为 `40px` 和 `24px` ,默认高度为 `32px`。 + +## en-US + +There are three sizes available to a numeric input box. By default, the `nzSize` is `32px`. The two additional sizes are `large` and `small` which means `40px` and `24px`, respectively. + diff --git a/components/input-number-legacy/demo/size.ts b/components/input-number-legacy/demo/size.ts new file mode 100644 index 00000000000..00e3581522a --- /dev/null +++ b/components/input-number-legacy/demo/size.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; + +@Component({ + selector: 'nz-demo-input-number-legacy-size', + standalone: true, + imports: [FormsModule, NzInputNumberLegacyModule], + template: ` + + + + `, + styles: [ + ` + nz-input-number-legacy { + margin-right: 8px; + } + ` + ] +}) +export class NzDemoInputNumberLegacySizeComponent { + value = 3; +} diff --git a/components/input-number-legacy/demo/status.md b/components/input-number-legacy/demo/status.md new file mode 100644 index 00000000000..e1b70f036ff --- /dev/null +++ b/components/input-number-legacy/demo/status.md @@ -0,0 +1,14 @@ +--- +order: 6 +title: + zh-CN: 自定义状态 + en-US: Status +--- + +## zh-CN + +使用 `nzStatus` 为 InputNumber 添加状态,可选 `error` 或者 `warning`。 + +## en-US + +Add status to InputNumber with `nzStatus`, which could be `error` or `warning`. \ No newline at end of file diff --git a/components/input-number-legacy/demo/status.ts b/components/input-number-legacy/demo/status.ts new file mode 100644 index 00000000000..873d126a6af --- /dev/null +++ b/components/input-number-legacy/demo/status.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; +import { NzSpaceModule } from 'ng-zorro-antd/space'; + +@Component({ + selector: 'nz-demo-input-number-legacy-status', + standalone: true, + imports: [FormsModule, NzInputNumberLegacyModule, NzSpaceModule], + template: ` + + + + + + + + + + + ` +}) +export class NzDemoInputNumberLegacyStatusComponent {} diff --git a/components/input-number-legacy/doc/index.en-US.md b/components/input-number-legacy/doc/index.en-US.md new file mode 100755 index 00000000000..8fa8e3b286f --- /dev/null +++ b/components/input-number-legacy/doc/index.en-US.md @@ -0,0 +1,68 @@ +--- +category: Components +type: Data Entry +title: InputNumberLegacy +cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg +tag: deprecated +--- + +Enter a number within certain range with the mouse or keyboard. + +> ⚠️ `InputNumberLegacy` has been deprecated in `v19.0.0`, please use the new version of `InputNumber` component. + +## When To Use + +When a numeric value needs to be provided. + +```ts +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; +``` + +## API + +### nz-input-number-legacy + +| property | description | type | default | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `[ngModel]` | current value, double binding | `number \| string` \| `string` | - | +| `[nzAutoFocus]` | get focus when component mounted | `boolean` | `false` | +| `[nzDisabled]` | disable the input | `boolean` | `false` | +| `[nzReadOnly]` | If readonly the input | `boolean` | `false` | +| `[nzMax]` | max value | `number` | `Infinity` | +| `[nzMin]` | min value | `number` | `-Infinity` | +| `[nzFormatter]` | Specifies the format of the value presented | `(value: number \| string) => string \| number` | - | +| `[nzParser]` | Specifies the value extracted from nzFormatter | `(value: string) => string \| number` | `(value: string) => value.trim().replace(/。/g, '.').replace(/[^\w\.-]+/g, '')` | +| `[nzPrecision]` | precision of input value | `number` | - | +| `[nzPrecisionMode]` | The method for calculating the precision of input value | `'cut' \| 'toFixed' \| ((value: number \| string, precision?: number) => number)` | `'toFixed'` | +| `[nzSize]` | width of input box | `'large' \| 'small' \| 'default'` | `'default'` | +| `[nzStatus]` | Set validation status | `'error' \| 'warning'` | - | +| `[nzStep]` | The number to which the current value is increased or decreased. It can be an integer or decimal. | `number \| string` | `1` | +| `[nzInputMode]` | enumerated attribute that hints at the type of data that might be entered by the user, [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode) | `string` | `decimal` | +| `[nzPlaceHolder]` | Placeholder of select | `string` | - | +| `[nzId]` | input id attribute inside the component | `string` | - | +| `(ngModelChange)` | The callback triggered when the value is changed | `EventEmitter` | - | +| `(nzFocus)` | focus callback | `EventEmitter` | - | +| `(nzBlur)` | blur callback | `EventEmitter` | - | + +### nz-input-number-group + +| Property | Description | Type | Default | +| ----------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------- | ----------- | +| `[nzAddOnAfter]` | The label text displayed after (on the right side of) the input number field, can work with `nzAddOnBefore` | `string \| TemplateRef` | - | +| `[nzAddOnBefore]` | The label text displayed before (on the left side of) the input number field, can work with `nzAddOnAfter` | `string \| TemplateRef` | - | +| `[nzPrefix]` | The prefix icon for the Input Number, can work with `nzSuffix` | `string \| TemplateRef` | - | +| `[nzSuffix]` | The suffix icon for the Input Number, can work with `nzPrefix` | `string \| TemplateRef` | - | +| `[nzPrefixIcon]` | The prefix icon for the Input Number | `string` | - | +| `[nzSuffixIcon]` | The suffix icon for the Input Number | `string` | - | +| `[nzCompact]` | Whether use compact style | `boolean` | `false` | +| `[nzSize]` | The size of `nz-input-number-group` specifies the size of the included `nz-input-number` fields | `'large' \| 'small' \| 'default'` | `'default'` | +| `[nzStatus]` | Set validation status | `'error' \| 'warning'` | - | + +#### Methods + +You can get instance by `ViewChild` + +| Name | Description | +| ------- | ------------ | +| focus() | get focus | +| blur() | remove focus | diff --git a/components/input-number-legacy/doc/index.zh-CN.md b/components/input-number-legacy/doc/index.zh-CN.md new file mode 100755 index 00000000000..81e507f033e --- /dev/null +++ b/components/input-number-legacy/doc/index.zh-CN.md @@ -0,0 +1,69 @@ +--- +category: Components +subtitle: 数字输入框 +type: 数据录入 +title: InputNumberLegacy +cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg +tag: deprecated +--- + +通过鼠标或键盘,输入范围内的数值。 + +> ⚠️ `InputNumberLegacy` 已在 `v19.0.0` 中废弃,请使用新版 `InputNumber` 组件。 + +## 何时使用 + +当需要获取标准数值时。 + +```ts +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; +``` + +## API + +### nz-input-number-legacy + +| 成员 | 说明 | 类型 | 默认值 | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `[ngModel]` | 当前值,可双向绑定 | `number \| string` \| `string` | - | +| `[nzAutoFocus]` | 自动获取焦点 | `boolean` | `false` | +| `[nzDisabled]` | 禁用 | `boolean` | `false` | +| `[nzReadOnly]` | 只读 | `boolean` | `false` | +| `[nzMax]` | 最大值 | `number` | `Infinity` | +| `[nzMin]` | 最小值 | `number` | `-Infinity` | +| `[nzFormatter]` | 指定输入框展示值的格式 | `(value: number \| string) => string \| number` | - | +| `[nzParser]` | 指定从 nzFormatter 里转换回数字的方式,和 nzFormatter 搭配使用 | `(value: string) => string \| number` | `(value: string) => value.trim().replace(/。/g, '.').replace(/[^\w\.-]+/g, '')` | +| `[nzPrecision]` | 数值精度 | `number` | - | +| `[nzPrecisionMode]` | 数值精度的取值方式 | `'cut' \| 'toFixed' \| ((value: number \| string, precision?: number) => number)` | `'toFixed'` | +| `[nzSize]` | 输入框大小 | `'large' \| 'small' \| 'default'` | `'default'` | +| `[nzStatus]` | 设置校验状态 | `'error' \| 'warning'` | - | +| `[nzStep]` | 每次改变步数,可以为小数 | `number \| string` | `1` | +| `[nzInputMode]` | 提供了用户在编辑元素或其内容时可能输入的数据类型的提示,详见[MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/inputmode) | `string` | `decimal` | +| `[nzPlaceHolder]` | 选择框默认文字 | `string` | - | +| `[nzId]` | 组件内部 input 的 id 值 | `string` | - | +| `(ngModelChange)` | 数值改变时回调 | `EventEmitter` | - | +| `(nzFocus)` | focus 时回调 | `EventEmitter` | - | +| `(nzBlur)` | blur 时回调 | `EventEmitter` | - | + +### nz-input-number-group + +| 参数 | 说明 | 类型 | 默认值 | +| ----------------- | -------------------------------------------------------------------- | --------------------------------- | ----------- | +| `[nzAddOnAfter]` | 带标签的 input-number,设置后置标签,可以与 `nzAddOnBefore` 配合使用 | `string \| TemplateRef` | - | +| `[nzAddOnBefore]` | 带标签的 input-number,设置前置标签,可以与 `nzAddOnAfter` 配合使用 | `string \| TemplateRef` | - | +| `[nzPrefix]` | 带有前缀图标的 input-number,可以与 `nzSuffix` 配合使用 | `string \| TemplateRef` | - | +| `[nzSuffix]` | 带有后缀图标的 input-number,可以与 `nzPrefix` 配合使用 | `string \| TemplateRef` | - | +| `[nzPrefixIcon]` | 带有前缀图标的 input-number | `string` | - | +| `[nzSuffixIcon]` | 带有后缀图标的 input-number | `string` | - | +| `[nzCompact]` | 是否用紧凑模式 | `boolean` | `false` | +| `[nzSize]` | `nz-input-number-group` 中所有的 `nz-input-number` 的大小 | `'large' \| 'small' \| 'default'` | `'default'` | +| `[nzStatus]` | 设置校验状态 | `'error' \| 'warning'` | - | + +#### 方法 + +通过 `ViewChild` 等方法获得实例后调用 + +| 名称 | 描述 | +| ------- | -------- | +| focus() | 获取焦点 | +| blur() | 移除焦点 | diff --git a/components/input-number-legacy/index.ts b/components/input-number-legacy/index.ts new file mode 100644 index 00000000000..97717c1c837 --- /dev/null +++ b/components/input-number-legacy/index.ts @@ -0,0 +1,6 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at /~https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export * from './public-api'; diff --git a/components/input-number/input-number-group-slot.component.ts b/components/input-number-legacy/input-number-group-slot.component.ts similarity index 92% rename from components/input-number/input-number-group-slot.component.ts rename to components/input-number-legacy/input-number-group-slot.component.ts index 2e6bfde1c7a..5c4953de664 100644 --- a/components/input-number/input-number-group-slot.component.ts +++ b/components/input-number-legacy/input-number-group-slot.component.ts @@ -8,6 +8,9 @@ import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewEncapsulati import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; import { NzIconModule } from 'ng-zorro-antd/icon'; +/** + * @deprecated Deprecated in v19.0.0. It is recommended to use the new version ``. + */ @Component({ selector: '[nz-input-number-group-slot]', preserveWhitespaces: false, diff --git a/components/input-number/input-number-group.component.ts b/components/input-number-legacy/input-number-group.component.ts similarity index 93% rename from components/input-number/input-number-group.component.ts rename to components/input-number-legacy/input-number-group.component.ts index 444cc4f438a..e4853ff7363 100644 --- a/components/input-number/input-number-group.component.ts +++ b/components/input-number-legacy/input-number-group.component.ts @@ -32,11 +32,14 @@ import { distinctUntilChanged, map, mergeMap, startWith, switchMap, takeUntil } import { NzFormNoStatusService, NzFormPatchModule, NzFormStatusService } from 'ng-zorro-antd/core/form'; import { NgClassInterface, NzSizeLDSType, NzStatus, NzValidateStatus } from 'ng-zorro-antd/core/types'; import { getStatusClassNames } from 'ng-zorro-antd/core/util'; -import { NZ_SPACE_COMPACT_ITEM_TYPE } from 'ng-zorro-antd/space'; +import { NZ_SPACE_COMPACT_ITEM_TYPE, NzSpaceCompactItemDirective } from 'ng-zorro-antd/space'; import { NzInputNumberGroupSlotComponent } from './input-number-group-slot.component'; -import { NzInputNumberComponent } from './input-number.component'; +import { NzInputNumberLegacyComponent } from './input-number.component'; +/** + * @deprecated Deprecated in v19.0.0. It is recommended to use the new version ``. + */ @Directive({ selector: `nz-input-number-group[nzSuffix], nz-input-number-group[nzPrefix]`, standalone: true @@ -45,6 +48,9 @@ export class NzInputNumberGroupWhitSuffixOrPrefixDirective { constructor(public elementRef: ElementRef) {} } +/** + * @deprecated Deprecated in v19.0.0. It is recommended to use the new version ``. + */ @Component({ selector: 'nz-input-number-group', exportAs: 'nzInputNumberGroup', @@ -111,6 +117,9 @@ export class NzInputNumberGroupWhitSuffixOrPrefixDirective { } `, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [NzFormNoStatusService, { provide: NZ_SPACE_COMPACT_ITEM_TYPE, useValue: 'input-number' }], host: { '[class.ant-input-number-group]': 'nzCompact', '[class.ant-input-number-group-compact]': 'nzCompact', @@ -125,13 +134,11 @@ export class NzInputNumberGroupWhitSuffixOrPrefixDirective { '[class.ant-input-number-affix-wrapper-lg]': `!isAddOn && isAffix && isLarge`, '[class.ant-input-number-affix-wrapper-sm]': `!isAddOn && isAffix && isSmall` }, - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [NzFormNoStatusService, { provide: NZ_SPACE_COMPACT_ITEM_TYPE, useValue: 'input-number' }] + hostDirectives: [NzSpaceCompactItemDirective] }) export class NzInputNumberGroupComponent implements AfterContentInit, OnChanges, OnInit, OnDestroy { - @ContentChildren(NzInputNumberComponent, { descendants: true }) - listOfNzInputNumberComponent!: QueryList; + @ContentChildren(NzInputNumberLegacyComponent, { descendants: true }) + listOfNzInputNumberComponent!: QueryList; @Input() nzAddOnBeforeIcon?: string | null = null; @Input() nzAddOnAfterIcon?: string | null = null; @Input() nzPrefixIcon?: string | null = null; @@ -213,10 +220,10 @@ export class NzInputNumberGroupComponent implements AfterContentInit, OnChanges, listOfInputChange$ .pipe( switchMap(list => - merge(...[listOfInputChange$, ...list.map((input: NzInputNumberComponent) => input.disabled$)]) + merge(...[listOfInputChange$, ...list.map((input: NzInputNumberLegacyComponent) => input.disabled$)]) ), mergeMap(() => listOfInputChange$), - map(list => list.some((input: NzInputNumberComponent) => input.nzDisabled)), + map(list => list.some((input: NzInputNumberLegacyComponent) => input.nzDisabled)), takeUntil(this.destroy$) ) .subscribe(disabled => { diff --git a/components/input-number/input-number-group.spec.ts b/components/input-number-legacy/input-number-group.spec.ts similarity index 93% rename from components/input-number/input-number-group.spec.ts rename to components/input-number-legacy/input-number-group.spec.ts index 3f6fbe08b19..54f3b272a4e 100644 --- a/components/input-number/input-number-group.spec.ts +++ b/components/input-number-legacy/input-number-group.spec.ts @@ -9,10 +9,10 @@ import { NzSizeLDSType, NzStatus } from 'ng-zorro-antd/core/types'; import { NzGridModule } from 'ng-zorro-antd/grid'; import { NzIconModule } from 'ng-zorro-antd/icon'; import { provideNzIconsTesting } from 'ng-zorro-antd/icon/testing'; -import { NzInputNumberGroupComponent } from 'ng-zorro-antd/input-number/input-number-group.component'; -import { NzInputNumberModule } from 'ng-zorro-antd/input-number/input-number.module'; import { NzFormControlStatusType, NzFormModule } from '../form'; +import { NzInputNumberGroupComponent } from './input-number-group.component'; +import { NzInputNumberLegacyModule } from './input-number.module'; describe('input-number-group', () => { beforeEach(waitForAsync(() => { @@ -199,7 +199,9 @@ describe('input-number-group', () => { inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)).nativeElement; }); it('should size work', () => { - expect(inputNumberGroupElement.querySelector('nz-input-number')!.classList).toContain('ant-input-number-lg'); + expect(inputNumberGroupElement.querySelector('nz-input-number-legacy')!.classList).toContain( + 'ant-input-number-lg' + ); }); }); describe('mix', () => { @@ -337,10 +339,10 @@ describe('input-number-group', () => { @Component({ standalone: true, - imports: [NzInputNumberModule], + imports: [NzInputNumberLegacyModule], template: ` - + beforeTemplate afterTemplate @@ -356,10 +358,10 @@ export class NzTestInputNumberGroupAddonComponent { @Component({ standalone: true, - imports: [NzInputNumberModule], + imports: [NzInputNumberLegacyModule], template: ` - + beforeTemplate afterTemplate @@ -376,11 +378,11 @@ export class NzTestInputNumberGroupAffixComponent { @Component({ standalone: true, - imports: [NzInputNumberModule], + imports: [NzInputNumberLegacyModule], template: ` - - + + ` }) @@ -391,14 +393,14 @@ export class NzTestInputNumberGroupMultipleComponent { @Component({ standalone: true, - imports: [FormsModule, NzGridModule, NzInputNumberModule], + imports: [FormsModule, NzGridModule, NzInputNumberLegacyModule], template: `
- +
- +
` @@ -407,10 +409,10 @@ export class NzTestInputNumberGroupColComponent {} @Component({ standalone: true, - imports: [NzInputNumberModule], + imports: [NzInputNumberLegacyModule], template: ` - + ` }) @@ -418,18 +420,18 @@ export class NzTestInputNumberGroupMixComponent {} @Component({ standalone: true, - imports: [NzIconModule, NzInputNumberModule], + imports: [NzIconModule, NzInputNumberLegacyModule], template: ` @if (!isAddon) { - + } @else { - + } ` @@ -441,11 +443,11 @@ export class NzTestInputNumberGroupWithStatusComponent { @Component({ standalone: true, - imports: [BidiModule, NzInputNumberModule], + imports: [BidiModule, NzInputNumberLegacyModule], template: `
- +
` @@ -456,13 +458,13 @@ export class NzTestInputNumberGroupWithDirComponent { @Component({ standalone: true, - imports: [NzFormModule, NzInputNumberModule], + imports: [NzFormModule, NzInputNumberLegacyModule], template: `
- + diff --git a/components/input-number-legacy/input-number.component.ts b/components/input-number-legacy/input-number.component.ts new file mode 100644 index 00000000000..722bb3a577c --- /dev/null +++ b/components/input-number-legacy/input-number.component.ts @@ -0,0 +1,551 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at /~https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { FocusMonitor } from '@angular/cdk/a11y'; +import { Direction, Directionality } from '@angular/cdk/bidi'; +import { DOWN_ARROW, ENTER, UP_ARROW } from '@angular/cdk/keycodes'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + OnChanges, + OnDestroy, + OnInit, + Output, + Renderer2, + SimpleChanges, + ViewChild, + ViewEncapsulation, + booleanAttribute, + computed, + forwardRef, + inject, + numberAttribute, + signal +} from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Subject, merge } from 'rxjs'; +import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; + +import { NzFormNoStatusService, NzFormPatchModule, NzFormStatusService } from 'ng-zorro-antd/core/form'; +import { NzDestroyService } from 'ng-zorro-antd/core/services'; +import { + NgClassInterface, + NzSizeLDSType, + NzStatus, + NzValidateStatus, + OnChangeType, + OnTouchedType +} from 'ng-zorro-antd/core/types'; +import { fromEventOutsideAngular, getStatusClassNames, isNotNil } from 'ng-zorro-antd/core/util'; +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NZ_SPACE_COMPACT_ITEM_TYPE, NZ_SPACE_COMPACT_SIZE, NzSpaceCompactItemDirective } from 'ng-zorro-antd/space'; + +/** + * @deprecated Deprecated in v19.0.0. It is recommended to use the new version ``. + */ +@Component({ + selector: 'nz-input-number-legacy', + exportAs: 'nzInputNumberLegacy', + template: ` +
+ + + + + + +
+
+ +
+ @if (hasFeedback && !!status && !nzFormNoStatusService) { + + } + `, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NzInputNumberLegacyComponent), + multi: true + }, + { provide: NZ_SPACE_COMPACT_ITEM_TYPE, useValue: 'input-number' }, + NzDestroyService + ], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + class: 'ant-input-number', + '[class.ant-input-number-in-form-item]': '!!nzFormStatusService', + '[class.ant-input-number-focused]': 'isFocused', + '[class.ant-input-number-lg]': `finalSize() === 'large'`, + '[class.ant-input-number-sm]': `finalSize() === 'small'`, + '[class.ant-input-number-disabled]': 'nzDisabled', + '[class.ant-input-number-readonly]': 'nzReadOnly', + '[class.ant-input-number-rtl]': `dir === 'rtl'`, + '[class.ant-input-number-borderless]': `nzBorderless` + }, + imports: [NzIconModule, FormsModule, NzFormPatchModule], + standalone: true, + hostDirectives: [NzSpaceCompactItemDirective] +}) +export class NzInputNumberLegacyComponent implements ControlValueAccessor, AfterViewInit, OnChanges, OnInit, OnDestroy { + displayValue?: string | number; + isFocused = false; + disabled$ = new Subject(); + disabledUp = false; + disabledDown = false; + dir: Direction = 'ltr'; + // status + prefixCls: string = 'ant-input-number'; + status: NzValidateStatus = ''; + statusCls: NgClassInterface = {}; + hasFeedback: boolean = false; + onChange: OnChangeType = () => {}; + onTouched: OnTouchedType = () => {}; + + @Output() readonly nzBlur = new EventEmitter(); + @Output() readonly nzFocus = new EventEmitter(); + /** The native `` element. */ + @ViewChild('upHandler', { static: true }) upHandler!: ElementRef; + /** The native `` element. */ + @ViewChild('downHandler', { static: true }) downHandler!: ElementRef; + /** The native `` element. */ + @ViewChild('inputElement', { static: true }) inputElement!: ElementRef; + @Input() nzSize: NzSizeLDSType = 'default'; + @Input({ transform: numberAttribute }) nzMin: number = -Infinity; + @Input({ transform: numberAttribute }) nzMax: number = Infinity; + @Input() nzParser = (value: string): string => + value + .trim() + .replace(/。/g, '.') + .replace(/[^\w\.-]+/g, ''); + @Input() nzPrecision?: number; + @Input() nzPrecisionMode: 'cut' | 'toFixed' | ((value: number | string, precision?: number) => number) = 'toFixed'; + @Input() nzPlaceHolder = ''; + @Input() nzStatus: NzStatus = ''; + @Input({ transform: numberAttribute }) nzStep = 1; + @Input() nzInputMode: string = 'decimal'; + @Input() nzId: string | null = null; + @Input({ transform: booleanAttribute }) nzDisabled = false; + @Input({ transform: booleanAttribute }) nzReadOnly = false; + @Input({ transform: booleanAttribute }) nzAutoFocus = false; + @Input({ transform: booleanAttribute }) nzBorderless: boolean = false; + @Input() nzFormatter: (value: number) => string | number = value => value; + + protected finalSize = computed(() => { + if (this.compactSize) { + return this.compactSize(); + } + return this.size(); + }); + + private size = signal(this.nzSize); + private compactSize = inject(NZ_SPACE_COMPACT_SIZE, { optional: true }); + private autoStepTimer?: ReturnType; + private parsedValue?: string | number; + private value?: number; + private isNzDisableFirstChange: boolean = true; + + onModelChange(value: string): void { + this.parsedValue = this.nzParser(value); + this.inputElement.nativeElement.value = `${this.parsedValue}`; + const validValue = this.getCurrentValidValue(this.parsedValue); + this.setValue(validValue); + } + + getCurrentValidValue(value: string | number): number { + let val = value; + if (val === '') { + val = ''; + } else if (!this.isNotCompleteNumber(val)) { + val = `${this.getValidValue(val)}`; + } else { + val = this.value!; + } + return this.toNumber(val); + } + + // '1.' '1x' 'xx' '' => are not complete numbers + isNotCompleteNumber(num: string | number): boolean { + return ( + isNaN(num as number) || + num === '' || + num === null || + !!(num && num.toString().indexOf('.') === num.toString().length - 1) + ); + } + + getValidValue(value?: string | number): string | number | undefined { + let val = parseFloat(value as string); + // /~https://github.com/ant-design/ant-design/issues/7358 + if (isNaN(val)) { + return value; + } + if (val < this.nzMin) { + val = this.nzMin; + } + if (val > this.nzMax) { + val = this.nzMax; + } + return val; + } + + toNumber(num: string | number): number { + if (this.isNotCompleteNumber(num)) { + return num as number; + } + const numStr = String(num); + if (numStr.indexOf('.') >= 0 && isNotNil(this.nzPrecision)) { + if (typeof this.nzPrecisionMode === 'function') { + return this.nzPrecisionMode(num, this.nzPrecision); + } else if (this.nzPrecisionMode === 'cut') { + const numSplit = numStr.split('.'); + numSplit[1] = numSplit[1].slice(0, this.nzPrecision); + return Number(numSplit.join('.')); + } + return Number(Number(num).toFixed(this.nzPrecision)); + } + return Number(num); + } + + getRatio(e: KeyboardEvent): number { + let ratio = 1; + if (e.metaKey || e.ctrlKey) { + ratio = 0.1; + } else if (e.shiftKey) { + ratio = 10; + } + return ratio; + } + + down(e: MouseEvent | KeyboardEvent, ratio?: number): void { + if (!this.isFocused) { + this.focus(); + } + this.step('down', e, ratio); + } + + up(e: MouseEvent | KeyboardEvent, ratio?: number): void { + if (!this.isFocused) { + this.focus(); + } + this.step('up', e, ratio); + } + + getPrecision(value: number): number { + const valueString = value.toString(); + if (valueString.indexOf('e-') >= 0) { + return parseInt(valueString.slice(valueString.indexOf('e-') + 2), 10); + } + let precision = 0; + if (valueString.indexOf('.') >= 0) { + precision = valueString.length - valueString.indexOf('.') - 1; + } + return precision; + } + + // step={1.0} value={1.51} + // press + + // then value should be 2.51, rather than 2.5 + // if this.props.precision is undefined + // /~https://github.com/react-component/input-number/issues/39 + getMaxPrecision(currentValue: string | number, ratio: number): number { + if (isNotNil(this.nzPrecision)) { + return this.nzPrecision; + } + const ratioPrecision = this.getPrecision(ratio); + const stepPrecision = this.getPrecision(this.nzStep); + const currentValuePrecision = this.getPrecision(currentValue as number); + if (!currentValue) { + return ratioPrecision + stepPrecision; + } + return Math.max(currentValuePrecision, ratioPrecision + stepPrecision); + } + + getPrecisionFactor(currentValue: string | number, ratio: number): number { + const precision = this.getMaxPrecision(currentValue, ratio); + return Math.pow(10, precision); + } + + upStep(val: string | number, rat: number): number { + const precisionFactor = this.getPrecisionFactor(val, rat); + const precision = Math.abs(this.getMaxPrecision(val, rat)); + let result; + if (typeof val === 'number') { + result = ((precisionFactor * val + precisionFactor * this.nzStep * rat) / precisionFactor).toFixed(precision); + } else { + result = this.nzMin === -Infinity ? this.nzStep : this.nzMin; + } + return this.toNumber(result); + } + + downStep(val: string | number, rat: number): number { + const precisionFactor = this.getPrecisionFactor(val, rat); + const precision = Math.abs(this.getMaxPrecision(val, rat)); + let result; + if (typeof val === 'number') { + result = ((precisionFactor * val - precisionFactor * this.nzStep * rat) / precisionFactor).toFixed(precision); + } else { + result = this.nzMin === -Infinity ? -this.nzStep : this.nzMin; + } + return this.toNumber(result); + } + + step(type: T, e: MouseEvent | KeyboardEvent, ratio: number = 1): void { + this.stop(); + e.preventDefault(); + if (this.nzDisabled) { + return; + } + const value = this.getCurrentValidValue(this.parsedValue!) || 0; + let val = 0; + if (type === 'up') { + val = this.upStep(value, ratio); + } else if (type === 'down') { + val = this.downStep(value, ratio); + } + const outOfRange = val > this.nzMax || val < this.nzMin; + if (val > this.nzMax) { + val = this.nzMax; + } else if (val < this.nzMin) { + val = this.nzMin; + } + this.setValue(val); + this.updateDisplayValue(val); + this.isFocused = true; + if (outOfRange) { + return; + } + this.autoStepTimer = setTimeout(() => { + (this[type] as (e: MouseEvent | KeyboardEvent, ratio: number) => void)(e, ratio); + }, 300); + } + + stop(): void { + if (this.autoStepTimer) { + clearTimeout(this.autoStepTimer); + } + } + + setValue(value: number): void { + if (`${this.value}` !== `${value}`) { + this.onChange(value); + } + this.value = value; + this.parsedValue = value; + this.disabledUp = this.disabledDown = false; + if (value || value === 0) { + const val = Number(value); + if (val >= this.nzMax) { + this.disabledUp = true; + } + if (val <= this.nzMin) { + this.disabledDown = true; + } + } + } + + updateDisplayValue(value: number): void { + const displayValue = isNotNil(this.nzFormatter(value)) ? this.nzFormatter(value) : ''; + this.displayValue = displayValue; + this.inputElement.nativeElement.value = `${displayValue}`; + } + + writeValue(value: number): void { + this.value = value; + this.setValue(value); + this.updateDisplayValue(value); + this.cdr.markForCheck(); + } + + registerOnChange(fn: OnChangeType): void { + this.onChange = fn; + } + + registerOnTouched(fn: OnTouchedType): void { + this.onTouched = fn; + } + + setDisabledState(disabled: boolean): void { + this.nzDisabled = (this.isNzDisableFirstChange && this.nzDisabled) || disabled; + this.isNzDisableFirstChange = false; + this.disabled$.next(this.nzDisabled); + this.cdr.markForCheck(); + } + + focus(): void { + this.focusMonitor.focusVia(this.inputElement, 'keyboard'); + } + + blur(): void { + this.inputElement.nativeElement.blur(); + } + + nzFormStatusService = inject(NzFormStatusService, { optional: true }); + nzFormNoStatusService = inject(NzFormNoStatusService, { optional: true }); + + constructor( + private ngZone: NgZone, + private elementRef: ElementRef, + private cdr: ChangeDetectorRef, + private focusMonitor: FocusMonitor, + private renderer: Renderer2, + private directionality: Directionality, + private destroy$: NzDestroyService + ) {} + + ngOnInit(): void { + this.nzFormStatusService?.formStatusChanges + .pipe( + distinctUntilChanged((pre, cur) => { + return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; + }), + takeUntil(this.destroy$) + ) + .subscribe(({ status, hasFeedback }) => { + this.setStatusStyles(status, hasFeedback); + }); + + this.focusMonitor + .monitor(this.elementRef, true) + .pipe(takeUntil(this.destroy$)) + .subscribe(focusOrigin => { + console.log(focusOrigin); + if (!focusOrigin) { + this.isFocused = false; + this.updateDisplayValue(this.value!); + this.nzBlur.emit(); + Promise.resolve().then(() => this.onTouched()); + } else { + this.isFocused = true; + this.nzFocus.emit(); + } + }); + + this.dir = this.directionality.value; + this.directionality.change.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { + this.dir = direction; + }); + + this.setupHandlersListeners(); + + fromEventOutsideAngular(this.inputElement.nativeElement, 'keyup') + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.stop()); + + fromEventOutsideAngular(this.inputElement.nativeElement, 'keydown') + .pipe(takeUntil(this.destroy$)) + .subscribe(event => { + const { keyCode } = event; + + if (keyCode !== UP_ARROW && keyCode !== DOWN_ARROW && keyCode !== ENTER) { + return; + } + + this.ngZone.run(() => { + if (keyCode === UP_ARROW) { + const ratio = this.getRatio(event); + this.up(event, ratio); + this.stop(); + } else if (keyCode === DOWN_ARROW) { + const ratio = this.getRatio(event); + this.down(event, ratio); + this.stop(); + } else { + this.updateDisplayValue(this.value!); + } + + this.cdr.markForCheck(); + }); + }); + } + + ngOnChanges({ nzStatus, nzDisabled, nzFormatter, nzSize }: SimpleChanges): void { + if (nzFormatter && !nzFormatter.isFirstChange()) { + const validValue = this.getCurrentValidValue(this.parsedValue!); + this.setValue(validValue); + this.updateDisplayValue(validValue); + } + if (nzDisabled) { + this.disabled$.next(this.nzDisabled); + } + if (nzStatus) { + this.setStatusStyles(this.nzStatus, this.hasFeedback); + } + if (nzSize) { + this.size.set(nzSize.currentValue); + } + } + + ngAfterViewInit(): void { + if (this.nzAutoFocus) { + this.focus(); + } + } + + ngOnDestroy(): void { + this.focusMonitor.stopMonitoring(this.elementRef); + } + + private setupHandlersListeners(): void { + merge( + fromEventOutsideAngular(this.upHandler.nativeElement, 'mouseup'), + fromEventOutsideAngular(this.upHandler.nativeElement, 'mouseleave'), + fromEventOutsideAngular(this.downHandler.nativeElement, 'mouseup'), + fromEventOutsideAngular(this.downHandler.nativeElement, 'mouseleave') + ) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.stop()); + } + + private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void { + // set inner status + this.status = status; + this.hasFeedback = hasFeedback; + this.cdr.markForCheck(); + // render status if nzStatus is set + this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback); + Object.keys(this.statusCls).forEach(status => { + if (this.statusCls[status]) { + this.renderer.addClass(this.elementRef.nativeElement, status); + } else { + this.renderer.removeClass(this.elementRef.nativeElement, status); + } + }); + } +} diff --git a/components/input-number-legacy/input-number.module.ts b/components/input-number-legacy/input-number.module.ts new file mode 100644 index 00000000000..384d2963b0a --- /dev/null +++ b/components/input-number-legacy/input-number.module.ts @@ -0,0 +1,27 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at /~https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { NgModule } from '@angular/core'; + +import { NzInputNumberGroupSlotComponent } from './input-number-group-slot.component'; +import { + NzInputNumberGroupComponent, + NzInputNumberGroupWhitSuffixOrPrefixDirective +} from './input-number-group.component'; +import { NzInputNumberLegacyComponent } from './input-number.component'; + +/** + * @deprecated Deprecated in v19.0.0. It is recommended to use the new version ``. + */ +@NgModule({ + imports: [ + NzInputNumberLegacyComponent, + NzInputNumberGroupComponent, + NzInputNumberGroupWhitSuffixOrPrefixDirective, + NzInputNumberGroupSlotComponent + ], + exports: [NzInputNumberLegacyComponent, NzInputNumberGroupComponent, NzInputNumberGroupWhitSuffixOrPrefixDirective] +}) +export class NzInputNumberLegacyModule {} diff --git a/components/input-number/input-number.spec.ts b/components/input-number-legacy/input-number.spec.ts similarity index 95% rename from components/input-number/input-number.spec.ts rename to components/input-number-legacy/input-number.spec.ts index 23dfb370347..c51e5863600 100644 --- a/components/input-number/input-number.spec.ts +++ b/components/input-number-legacy/input-number.spec.ts @@ -9,8 +9,8 @@ import { createKeyboardEvent, createMouseEvent, dispatchEvent, dispatchFakeEvent import { NzSizeLDSType, NzStatus } from 'ng-zorro-antd/core/types'; import { NzFormControlStatusType, NzFormModule } from 'ng-zorro-antd/form'; -import { NzInputNumberComponent } from './input-number.component'; -import { NzInputNumberModule } from './input-number.module'; +import { NzInputNumberLegacyComponent } from './input-number.component'; +import { NzInputNumberLegacyModule } from './input-number.module'; describe('input number', () => { describe('input number basic', () => { @@ -32,7 +32,7 @@ describe('input number', () => { fixture = TestBed.createComponent(NzTestInputNumberBasicComponent); fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; - inputNumber = fixture.debugElement.query(By.directive(NzInputNumberComponent)); + inputNumber = fixture.debugElement.query(By.directive(NzInputNumberLegacyComponent)); inputElement = inputNumber.nativeElement.querySelector('input'); upArrowEvent = createKeyboardEvent('keydown', UP_ARROW, inputElement, 'ArrowUp'); downArrowEvent = createKeyboardEvent('keydown', DOWN_ARROW, inputElement, 'ArrowDown'); @@ -459,7 +459,7 @@ describe('input number', () => { it('should be in pristine, untouched, and valid states and be enable initially', fakeAsync(() => { fixture.detectChanges(); flush(); - const inputNumber = fixture.debugElement.query(By.directive(NzInputNumberComponent)); + const inputNumber = fixture.debugElement.query(By.directive(NzInputNumberLegacyComponent)); const inputElement = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; expect(inputNumber.nativeElement.classList).not.toContain('ant-input-number-disabled'); expect(inputElement.disabled).toBeFalsy(); @@ -471,7 +471,7 @@ describe('input number', () => { testComponent.disable(); fixture.detectChanges(); flush(); - const inputNumber = fixture.debugElement.query(By.directive(NzInputNumberComponent)); + const inputNumber = fixture.debugElement.query(By.directive(NzInputNumberLegacyComponent)); const inputElement = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; expect(inputNumber.nativeElement.classList).toContain('ant-input-number-disabled'); expect(inputElement.disabled).toBeTruthy(); @@ -480,7 +480,7 @@ describe('input number', () => { testComponent.disabled = true; fixture.detectChanges(); flush(); - const inputNumber = fixture.debugElement.query(By.directive(NzInputNumberComponent)); + const inputNumber = fixture.debugElement.query(By.directive(NzInputNumberLegacyComponent)); const inputElement = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; const upHandler = inputNumber.nativeElement.querySelector('.ant-input-number-handler-up'); expect(inputNumber.nativeElement.classList).toContain('ant-input-number-disabled'); @@ -525,7 +525,7 @@ describe('input number', () => { fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; - inputNumber = fixture.debugElement.query(By.directive(NzInputNumberComponent)); + inputNumber = fixture.debugElement.query(By.directive(NzInputNumberLegacyComponent)); inputElement = inputNumber.nativeElement.querySelector('input'); })); it('should readOnly work', () => { @@ -553,7 +553,7 @@ describe('input number', () => { fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; - inputNumber = fixture.debugElement.query(By.directive(NzInputNumberComponent)); + inputNumber = fixture.debugElement.query(By.directive(NzInputNumberLegacyComponent)); })); it('should status work', () => { fixture.detectChanges(); @@ -581,7 +581,7 @@ describe('input number', () => { fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; - inputNumber = fixture.debugElement.query(By.directive(NzInputNumberComponent)); + inputNumber = fixture.debugElement.query(By.directive(NzInputNumberLegacyComponent)); })); it('should className correct', () => { const feedbackElement = fixture.nativeElement.querySelector('nz-form-item-feedback-icon'); @@ -612,9 +612,9 @@ describe('input number', () => { }); @Component({ standalone: true, - imports: [FormsModule, NzInputNumberModule], + imports: [FormsModule, NzInputNumberLegacyModule], template: ` - { [nzPrecision]="precision" [nzPrecisionMode]="precisionMode" [nzBorderless]="!bordered" - > + > ` }) export class NzTestInputNumberBasicComponent { - @ViewChild(NzInputNumberComponent, { static: false }) nzInputNumberComponent!: NzInputNumberComponent; + @ViewChild(NzInputNumberLegacyComponent, { static: false }) nzInputNumberComponent!: NzInputNumberLegacyComponent; value?: number | string; autofocus = false; disabled = false; @@ -644,7 +644,7 @@ export class NzTestInputNumberBasicComponent { step = 1; bordered = true; precision?: number = 2; - precisionMode!: NzInputNumberComponent['nzPrecisionMode']; + precisionMode!: NzInputNumberLegacyComponent['nzPrecisionMode']; formatter = (value: number): string => (value !== null ? `${value}` : ''); parser = (value: string): string => value; modelChange = jasmine.createSpy('change callback'); @@ -652,20 +652,25 @@ export class NzTestInputNumberBasicComponent { @Component({ standalone: true, - imports: [NzInputNumberModule], - template: `` + imports: [NzInputNumberLegacyModule], + template: `` }) export class NzTestReadOnlyInputNumberBasicComponent { - @ViewChild(NzInputNumberComponent, { static: false }) nzInputNumberComponent!: NzInputNumberComponent; + @ViewChild(NzInputNumberLegacyComponent, { static: false }) nzInputNumberComponent!: NzInputNumberLegacyComponent; readonly = false; } @Component({ standalone: true, - imports: [ReactiveFormsModule, NzInputNumberModule], + imports: [ReactiveFormsModule, NzInputNumberLegacyModule], template: ` - + ` }) @@ -684,8 +689,8 @@ export class NzTestInputNumberFormComponent { @Component({ standalone: true, - imports: [NzInputNumberModule], - template: `` + imports: [NzInputNumberLegacyModule], + template: `` }) export class NzTestInputNumberStatusComponent { status: NzStatus = 'error'; @@ -693,12 +698,12 @@ export class NzTestInputNumberStatusComponent { @Component({ standalone: true, - imports: [NzFormModule, NzInputNumberModule], + imports: [NzFormModule, NzInputNumberLegacyModule], template: `
- +
diff --git a/components/input-number-legacy/ng-package.json b/components/input-number-legacy/ng-package.json new file mode 100644 index 00000000000..789c95e4962 --- /dev/null +++ b/components/input-number-legacy/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/components/input-number-legacy/public-api.ts b/components/input-number-legacy/public-api.ts new file mode 100644 index 00000000000..9434308324b --- /dev/null +++ b/components/input-number-legacy/public-api.ts @@ -0,0 +1,9 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at /~https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export * from './input-number.component'; +export * from './input-number-group-slot.component'; +export * from './input-number-group.component'; +export * from './input-number.module'; diff --git a/components/input-number/demo/addon.md b/components/input-number/demo/addon.md index c45680cd912..2fbf61127d7 100644 --- a/components/input-number/demo/addon.md +++ b/components/input-number/demo/addon.md @@ -1,5 +1,5 @@ --- -order: 7 +order: 2 title: zh-CN: 前置/后置标签 en-US: Pre / Post tab diff --git a/components/input-number/demo/addon.ts b/components/input-number/demo/addon.ts index e4fcdf606b4..dcebfd9614a 100644 --- a/components/input-number/demo/addon.ts +++ b/components/input-number/demo/addon.ts @@ -2,47 +2,49 @@ import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { NzCascaderModule } from 'ng-zorro-antd/cascader'; +import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; import { NzSelectModule } from 'ng-zorro-antd/select'; -import { NzSpaceModule } from 'ng-zorro-antd/space'; @Component({ selector: 'nz-demo-input-number-addon', standalone: true, - imports: [FormsModule, NzCascaderModule, NzInputNumberModule, NzSelectModule, NzSpaceModule], + imports: [FormsModule, NzSelectModule, NzCascaderModule, NzInputNumberModule, NzIconModule], template: ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ` + + + + $ + + + + + + + + + + + + + + + + + + + + + + + `, + styles: [ + ` + nz-input-number { + display: block; + margin-bottom: 8px; + } + ` + ] }) export class NzDemoInputNumberAddonComponent { value = 100; diff --git a/components/input-number/demo/basic.ts b/components/input-number/demo/basic.ts index a1dfb9eb37b..0db488fbe1d 100644 --- a/components/input-number/demo/basic.ts +++ b/components/input-number/demo/basic.ts @@ -7,7 +7,7 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; selector: 'nz-demo-input-number-basic', standalone: true, imports: [FormsModule, NzInputNumberModule], - template: `` + template: `` }) export class NzDemoInputNumberBasicComponent { value = 3; diff --git a/components/input-number/demo/borderless.md b/components/input-number/demo/borderless.md index 4f3ea028a8a..9370e4415b3 100644 --- a/components/input-number/demo/borderless.md +++ b/components/input-number/demo/borderless.md @@ -1,5 +1,5 @@ --- -order: 0 +order: 7 title: zh-CN: 无边框 en-US: Borderless @@ -12,4 +12,3 @@ title: ## en-US Borderless input number. - diff --git a/components/input-number/demo/borderless.ts b/components/input-number/demo/borderless.ts index c142bf8ab71..5f36dc703eb 100644 --- a/components/input-number/demo/borderless.ts +++ b/components/input-number/demo/borderless.ts @@ -7,7 +7,7 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; selector: 'nz-demo-input-number-borderless', standalone: true, imports: [FormsModule, NzInputNumberModule], - template: `` + template: `` }) export class NzDemoInputNumberBorderlessComponent { value = 3; diff --git a/components/input-number/demo/digit.md b/components/input-number/demo/digit.md index 94345c264c1..58ab9d95c52 100755 --- a/components/input-number/demo/digit.md +++ b/components/input-number/demo/digit.md @@ -1,5 +1,5 @@ --- -order: 3 +order: 4 title: zh-CN: 小数 en-US: Decimals @@ -12,4 +12,3 @@ title: ## en-US A numeric-only input box whose values can be increased or decreased using a decimal step. The number of decimals (also known as precision) is determined by the `nzStep` prop. - diff --git a/components/input-number/demo/digit.ts b/components/input-number/demo/digit.ts index ebdd548cb58..58e3d1ea9cf 100644 --- a/components/input-number/demo/digit.ts +++ b/components/input-number/demo/digit.ts @@ -7,16 +7,8 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; selector: 'nz-demo-input-number-digit', standalone: true, imports: [FormsModule, NzInputNumberModule], - template: ` - - ` + template: ` ` }) export class NzDemoInputNumberDigitComponent { - value = 0; + value = 0.1; } diff --git a/components/input-number/demo/disabled.md b/components/input-number/demo/disabled.md index 8b33efef108..51c8c962b88 100755 --- a/components/input-number/demo/disabled.md +++ b/components/input-number/demo/disabled.md @@ -1,5 +1,5 @@ --- -order: 2 +order: 3 title: zh-CN: 不可用 en-US: Disabled @@ -12,5 +12,3 @@ title: ## en-US Click the button to toggle between available and disabled states. - - diff --git a/components/input-number/demo/disabled.ts b/components/input-number/demo/disabled.ts index f14f6afe1c7..789351c563c 100644 --- a/components/input-number/demo/disabled.ts +++ b/components/input-number/demo/disabled.ts @@ -9,25 +9,13 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; standalone: true, imports: [FormsModule, NzButtonModule, NzInputNumberModule], template: ` - +

- + ` }) export class NzDemoInputNumberDisabledComponent { value = 3; isDisabled = false; - - toggleDisabled(): void { - this.isDisabled = !this.isDisabled; - } } diff --git a/components/input-number/demo/formatter.md b/components/input-number/demo/formatter.md index c4c2d0432f5..7e497274db1 100755 --- a/components/input-number/demo/formatter.md +++ b/components/input-number/demo/formatter.md @@ -1,5 +1,5 @@ --- -order: 4 +order: 5 title: zh-CN: 格式化展示 en-US: Formatter @@ -12,4 +12,3 @@ title: ## en-US Display value within it's situation with `nzFormatter`, and we usually use `nzParser` at the same time. - diff --git a/components/input-number/demo/formatter.ts b/components/input-number/demo/formatter.ts index 7499cb58d99..e7da4c7f84f 100644 --- a/components/input-number/demo/formatter.ts +++ b/components/input-number/demo/formatter.ts @@ -10,20 +10,18 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; template: ` + /> + /> `, styles: [ ` @@ -36,7 +34,7 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; export class NzDemoInputNumberFormatterComponent { demoValue = 100; formatterPercent = (value: number): string => `${value} %`; - parserPercent = (value: string): string => value.replace(' %', ''); + parserPercent = (value: string): number => +value.replace(' %', ''); formatterDollar = (value: number): string => `$ ${value}`; - parserDollar = (value: string): string => value.replace('$ ', ''); + parserDollar = (value: string): number => +value.replace('$ ', ''); } diff --git a/components/input-number/demo/handler-icon.md b/components/input-number/demo/handler-icon.md new file mode 100644 index 00000000000..3a71dccfe80 --- /dev/null +++ b/components/input-number/demo/handler-icon.md @@ -0,0 +1,14 @@ +--- +order: 11 +title: + zh-CN: 自定义图标 + en-US: Custom handler icon +--- + +## zh-CN + +自定义箭头图标。 + +## en-US + +Custom arrow icon. diff --git a/components/input-number/demo/handler-icon.ts b/components/input-number/demo/handler-icon.ts new file mode 100644 index 00000000000..2e723532fce --- /dev/null +++ b/components/input-number/demo/handler-icon.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; + +@Component({ + selector: 'nz-demo-input-number-handler-icon', + standalone: true, + imports: [FormsModule, NzInputNumberModule, NzIconModule], + template: ` + + + + + ` +}) +export class NzDemoInputNumberHandlerIconComponent { + value = 3; +} diff --git a/components/input-number/demo/keyboard.md b/components/input-number/demo/keyboard.md new file mode 100644 index 00000000000..e2ebbf87d3a --- /dev/null +++ b/components/input-number/demo/keyboard.md @@ -0,0 +1,15 @@ +--- +order: 6 +title: + zh-CN: 键盘行为 + en-US: Keyboard behavior +--- + +## zh-CN + +使用 `nzKeyboard` 属性可以控制键盘行为。 + +## en-US + +Use the `nzKeyboard` property to control keyboard behavior. +` diff --git a/components/input-number/demo/keyboard.ts b/components/input-number/demo/keyboard.ts new file mode 100644 index 00000000000..272fd599e9b --- /dev/null +++ b/components/input-number/demo/keyboard.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzCheckboxModule } from 'ng-zorro-antd/checkbox'; +import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; + +@Component({ + selector: 'nz-demo-input-number-keyboard', + standalone: true, + imports: [FormsModule, NzInputNumberModule, NzCheckboxModule], + template: ` + + + `, + styles: [ + ` + nz-input-number { + margin-right: 8px; + } + ` + ] +}) +export class NzDemoInputNumberKeyboardComponent { + keyboard = true; + value = 3; +} diff --git a/components/input-number/demo/out-of-range.md b/components/input-number/demo/out-of-range.md new file mode 100644 index 00000000000..18f31133485 --- /dev/null +++ b/components/input-number/demo/out-of-range.md @@ -0,0 +1,14 @@ +--- +order: 8 +title: + zh-CN: 超出边界 + en-US: Out of range +--- + +## zh-CN + +当通过受控将 `value` 超出边界时,提供警告样式。 + +## en-US + +When the `value` is out of range in controlled mode, a warning style is provided. diff --git a/components/input-number/demo/out-of-range.ts b/components/input-number/demo/out-of-range.ts new file mode 100644 index 00000000000..dbdc71138aa --- /dev/null +++ b/components/input-number/demo/out-of-range.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; + +@Component({ + selector: 'nz-demo-input-number-out-of-range', + standalone: true, + imports: [FormsModule, NzInputNumberModule], + template: `` +}) +export class NzDemoInputNumberOutOfRangeComponent { + value = 99; +} diff --git a/components/input-number/demo/prefix.md b/components/input-number/demo/prefix.md index c1207bfc413..b3b39508e72 100644 --- a/components/input-number/demo/prefix.md +++ b/components/input-number/demo/prefix.md @@ -1,5 +1,5 @@ --- -order: 8 +order: 9 title: zh-CN: 前缀 en-US: Prefix diff --git a/components/input-number/demo/prefix.ts b/components/input-number/demo/prefix.ts index 00e77f449dc..9a113b133cd 100644 --- a/components/input-number/demo/prefix.ts +++ b/components/input-number/demo/prefix.ts @@ -1,25 +1,33 @@ import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; -import { NzSpaceModule } from 'ng-zorro-antd/space'; @Component({ selector: 'nz-demo-input-number-prefix', standalone: true, - imports: [FormsModule, NzInputNumberModule, NzSpaceModule], + imports: [FormsModule, NzInputNumberModule, NzIconModule], template: ` - - - - - - - - - - - - ` + + + + + + + + + + + + + `, + styles: [ + ` + nz-input-number { + margin-bottom: 8px; + } + ` + ] }) export class NzDemoInputNumberPrefixComponent {} diff --git a/components/input-number/demo/size.ts b/components/input-number/demo/size.ts index 04e92564495..aad2dec7116 100644 --- a/components/input-number/demo/size.ts +++ b/components/input-number/demo/size.ts @@ -8,9 +8,9 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; standalone: true, imports: [FormsModule, NzInputNumberModule], template: ` - - - + + + `, styles: [ ` diff --git a/components/input-number/demo/status.md b/components/input-number/demo/status.md index e1b70f036ff..9a1fd3445fe 100644 --- a/components/input-number/demo/status.md +++ b/components/input-number/demo/status.md @@ -1,5 +1,5 @@ --- -order: 6 +order: 10 title: zh-CN: 自定义状态 en-US: Status @@ -11,4 +11,4 @@ title: ## en-US -Add status to InputNumber with `nzStatus`, which could be `error` or `warning`. \ No newline at end of file +Add status to InputNumber with `nzStatus`, which could be `error` or `warning`. diff --git a/components/input-number/demo/status.ts b/components/input-number/demo/status.ts index a86373687c0..70638b49a36 100644 --- a/components/input-number/demo/status.ts +++ b/components/input-number/demo/status.ts @@ -1,24 +1,29 @@ import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; -import { NzSpaceModule } from 'ng-zorro-antd/space'; @Component({ selector: 'nz-demo-input-number-status', standalone: true, - imports: [FormsModule, NzInputNumberModule, NzSpaceModule], + imports: [FormsModule, NzInputNumberModule, NzIconModule], template: ` - - - - - - - - - - - ` + + + + + + + + + `, + styles: [ + ` + nz-input-number { + margin-bottom: 8px; + } + ` + ] }) export class NzDemoInputNumberStatusComponent {} diff --git a/components/input-number/doc/index.en-US.md b/components/input-number/doc/index.en-US.md index 5c0365ed32b..59c57d1d34a 100755 --- a/components/input-number/doc/index.en-US.md +++ b/components/input-number/doc/index.en-US.md @@ -3,6 +3,7 @@ category: Components type: Data Entry title: InputNumber cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg +tag: 19.0.0 --- Enter a number within certain range with the mouse or keyboard. @@ -12,48 +13,33 @@ Enter a number within certain range with the mouse or keyboard. When a numeric value needs to be provided. ```ts -import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; +import { NzInputNumberLegacy } from 'ng-zorro-antd/input-number'; ``` ## API ### nz-input-number -| property | description | type | default | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| `[ngModel]` | current value, double binding | `number \| string` \| `string` | - | -| `[nzAutoFocus]` | get focus when component mounted | `boolean` | `false` | -| `[nzDisabled]` | disable the input | `boolean` | `false` | -| `[nzReadOnly]` | If readonly the input | `boolean` | `false` | -| `[nzMax]` | max value | `number` | `Infinity` | -| `[nzMin]` | min value | `number` | `-Infinity` | -| `[nzFormatter]` | Specifies the format of the value presented | `(value: number \| string) => string \| number` | - | -| `[nzParser]` | Specifies the value extracted from nzFormatter | `(value: string) => string \| number` | `(value: string) => value.trim().replace(/。/g, '.').replace(/[^\w\.-]+/g, '')` | -| `[nzPrecision]` | precision of input value | `number` | - | -| `[nzPrecisionMode]` | The method for calculating the precision of input value | `'cut' \| 'toFixed' \| ((value: number \| string, precision?: number) => number)` | `'toFixed'` | -| `[nzSize]` | width of input box | `'large' \| 'small' \| 'default'` | `'default'` | -| `[nzStatus]` | Set validation status | `'error' \| 'warning'` | - | -| `[nzStep]` | The number to which the current value is increased or decreased. It can be an integer or decimal. | `number \| string` | `1` | -| `[nzInputMode]` | enumerated attribute that hints at the type of data that might be entered by the user, [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode) | `string` | `decimal` | -| `[nzPlaceHolder]` | Placeholder of select | `string` | - | -| `[nzId]` | input id attribute inside the component | `string` | - | -| `(ngModelChange)` | The callback triggered when the value is changed | `EventEmitter` | - | -| `(nzFocus)` | focus callback | `EventEmitter` | - | -| `(nzBlur)` | blur callback | `EventEmitter` | - | - -### nz-input-number-group - -| Property | Description | Type | Default | -| ----------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------- | ----------- | -| `[nzAddOnAfter]` | The label text displayed after (on the right side of) the input number field, can work with `nzAddOnBefore` | `string \| TemplateRef` | - | -| `[nzAddOnBefore]` | The label text displayed before (on the left side of) the input number field, can work with `nzAddOnAfter` | `string \| TemplateRef` | - | -| `[nzPrefix]` | The prefix icon for the Input Number, can work with `nzSuffix` | `string \| TemplateRef` | - | -| `[nzSuffix]` | The suffix icon for the Input Number, can work with `nzPrefix` | `string \| TemplateRef` | - | -| `[nzPrefixIcon]` | The prefix icon for the Input Number | `string` | - | -| `[nzSuffixIcon]` | The suffix icon for the Input Number | `string` | - | -| `[nzCompact]` | Whether use compact style | `boolean` | `false` | -| `[nzSize]` | The size of `nz-input-number-group` specifies the size of the included `nz-input-number` fields | `'large' \| 'small' \| 'default'` | `'default'` | -| `[nzStatus]` | Set validation status | `'error' \| 'warning'` | - | +| property | description | type | default | +| ----------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `[ngModel]` | current value, two way binding | `number` | - | +| `[nzId]` | ID of the input box | `string` | - | +| `[nzPlaceHolder]` | placeholder | `string` | - | +| `[nzAutoFocus]` | auto focus | `boolean` | `false` | +| `[nzBordered]` | whether to have border | `boolean` | `true` | +| `[nzControls]` | whether to show up and down buttons | `boolean` | `true` | +| `[nzDisabled]` | whether to disable | `boolean` | `false` | +| `[nzFormatter]` | specify the format of the displayed value | `(value: number) => string` | - | +| `[nzMax]` | maximum value | `number` | [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) | +| `[nzMin]` | minimum value | `number` | [Number.MIN_SAFE_INTEGER](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) | +| `[nzParser]` | specify how to convert back to a number from `formatter`, used with `formatter` | `(value: string) => number` | - | +| `[nzPrecision]` | numerical precision, the `formatter` configuration takes precedence | `number` | - | +| `[nzReadOnly]` | whether to read only | `boolean` | `false` | +| `[nzStatus]` | status, optional `error` `warning` | `string` | - | +| `[nzSize]` | input box size, optional `large` `default` `small` | `string` | `default` | +| `[nzStep]` | step of each change, can be a decimal | `number` | `1` | +| `(nzOnStep)` | callback when clicking the up and down arrows | `EventEmitter<{ value: number, offset: number, type: 'up' \| 'down' }>` | - | +| `(ngModelChange)` | callback function when the value changes | `EventEmitter` | - | #### Methods @@ -63,3 +49,9 @@ You can get instance by `ViewChild` | ------- | ------------ | | focus() | get focus | | blur() | remove focus | + +## FAQ + +### Why can the `value` exceed the `min` and `max` range in controlled mode? + +In controlled mode, developers may store related data by themselves. If the component constrains the data back to the range, it will cause the displayed data to be inconsistent with the actual stored data. This leads to potential data problems in some scenarios such as form fields. diff --git a/components/input-number/doc/index.zh-CN.md b/components/input-number/doc/index.zh-CN.md index 5fa58b79038..92359e9ca3e 100755 --- a/components/input-number/doc/index.zh-CN.md +++ b/components/input-number/doc/index.zh-CN.md @@ -4,6 +4,7 @@ subtitle: 数字输入框 type: 数据录入 title: InputNumber cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg +tag: 19.0.0 --- 通过鼠标或键盘,输入范围内的数值。 @@ -20,41 +21,26 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; ### nz-input-number -| 成员 | 说明 | 类型 | 默认值 | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| `[ngModel]` | 当前值,可双向绑定 | `number \| string` \| `string` | - | -| `[nzAutoFocus]` | 自动获取焦点 | `boolean` | `false` | -| `[nzDisabled]` | 禁用 | `boolean` | `false` | -| `[nzReadOnly]` | 只读 | `boolean` | `false` | -| `[nzMax]` | 最大值 | `number` | `Infinity` | -| `[nzMin]` | 最小值 | `number` | `-Infinity` | -| `[nzFormatter]` | 指定输入框展示值的格式 | `(value: number \| string) => string \| number` | - | -| `[nzParser]` | 指定从 nzFormatter 里转换回数字的方式,和 nzFormatter 搭配使用 | `(value: string) => string \| number` | `(value: string) => value.trim().replace(/。/g, '.').replace(/[^\w\.-]+/g, '')` | -| `[nzPrecision]` | 数值精度 | `number` | - | -| `[nzPrecisionMode]` | 数值精度的取值方式 | `'cut' \| 'toFixed' \| ((value: number \| string, precision?: number) => number)` | `'toFixed'` | -| `[nzSize]` | 输入框大小 | `'large' \| 'small' \| 'default'` | `'default'` | -| `[nzStatus]` | 设置校验状态 | `'error' \| 'warning'` | - | -| `[nzStep]` | 每次改变步数,可以为小数 | `number \| string` | `1` | -| `[nzInputMode]` | 提供了用户在编辑元素或其内容时可能输入的数据类型的提示,详见[MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/inputmode) | `string` | `decimal` | -| `[nzPlaceHolder]` | 选择框默认文字 | `string` | - | -| `[nzId]` | 组件内部 input 的 id 值 | `string` | - | -| `(ngModelChange)` | 数值改变时回调 | `EventEmitter` | - | -| `(nzFocus)` | focus 时回调 | `EventEmitter` | - | -| `(nzBlur)` | blur 时回调 | `EventEmitter` | - | - -### nz-input-number-group - -| 参数 | 说明 | 类型 | 默认值 | -| ----------------- | -------------------------------------------------------------------- | --------------------------------- | ----------- | -| `[nzAddOnAfter]` | 带标签的 input-number,设置后置标签,可以与 `nzAddOnBefore` 配合使用 | `string \| TemplateRef` | - | -| `[nzAddOnBefore]` | 带标签的 input-number,设置前置标签,可以与 `nzAddOnAfter` 配合使用 | `string \| TemplateRef` | - | -| `[nzPrefix]` | 带有前缀图标的 input-number,可以与 `nzSuffix` 配合使用 | `string \| TemplateRef` | - | -| `[nzSuffix]` | 带有后缀图标的 input-number,可以与 `nzPrefix` 配合使用 | `string \| TemplateRef` | - | -| `[nzPrefixIcon]` | 带有前缀图标的 input-number | `string` | - | -| `[nzSuffixIcon]` | 带有后缀图标的 input-number | `string` | - | -| `[nzCompact]` | 是否用紧凑模式 | `boolean` | `false` | -| `[nzSize]` | `nz-input-number-group` 中所有的 `nz-input-number` 的大小 | `'large' \| 'small' \| 'default'` | `'default'` | -| `[nzStatus]` | 设置校验状态 | `'error' \| 'warning'` | - | +| 成员 | 说明 | 类型 | 默认值 | +| ----------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `[ngModel]` | 当前值,可双向绑定 | `number` | - | +| `[nzId]` | 输入框的 ID | `string` | - | +| `[nzPlaceHolder]` | 占位符 | `string` | - | +| `[nzAutoFocus]` | 自动获取焦点 | `boolean` | `false` | +| `[nzBordered]` | 是否有边框 | `boolean` | `true` | +| `[nzControls]` | 是否显示增减按钮 | `boolean` | `true` | +| `[nzDisabled]` | 是否禁用 | `boolean` | `false` | +| `[nzFormatter]` | 指定输入框展示值的格式 | `(value: number) => string` | - | +| `[nzMax]` | 最大值 | `number` | [Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) | +| `[nzMin]` | 最小值 | `number` | [Number.MIN_SAFE_INTEGER](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) | +| `[nzParser]` | 指定从 `formatter` 里转换回数字的方式,和 `formatter` 搭配使用 | `(value: string) => number` | - | +| `[nzPrecision]` | 数值精度,配置 `formatter` 时会以 `formatter` 为准 | `number` | - | +| `[nzReadOnly]` | 是否只读 | `boolean` | `false` | +| `[nzStatus]` | 状态,可选 `error` `warning` | `string` | - | +| `[nzSize]` | 输入框大小,可选 `large` `default` `small` | `string` | `default` | +| `[nzStep]` | 每次改变步数,可以是小数 | `number` | `1` | +| `(nzOnStep)` | 点击上下箭头的回调 | `EventEmitter<{ value: number, offset: number, type: 'up' \| 'down' }>` | - | +| `(ngModelChange)` | 值变化时的回调函数 | `EventEmitter` | - | #### 方法 @@ -64,3 +50,9 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; | ------- | -------- | | focus() | 获取焦点 | | blur() | 移除焦点 | + +## FAQ + +### 为何受控模式下,`value` 可以超出 `min` 和 `max` 范围? + +在受控模式下,开发者可能自行存储相关数据。如果组件将数据约束回范围内,会导致展示数据与实际存储数据不一致的情况。这使得一些如表单场景存在潜在的数据问题。 diff --git a/components/input-number/input-number.component.spec.ts b/components/input-number/input-number.component.spec.ts new file mode 100644 index 00000000000..3530a25a969 --- /dev/null +++ b/components/input-number/input-number.component.spec.ts @@ -0,0 +1,384 @@ +import { DOWN_ARROW, 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'; + +import { NzSizeLDSType, NzStatus } from 'ng-zorro-antd/core/types'; +import { provideNzIconsTesting } from 'ng-zorro-antd/icon/testing'; + +import { NzInputNumberComponent } from './input-number.component'; +import { NzInputNumberModule } from './input-number.module'; + +describe('Input number', () => { + let component: InputNumberTestComponent; + let fixture: ComponentFixture; + let hostElement: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideNzIconsTesting()] + }); + fixture = TestBed.createComponent(InputNumberTestComponent); + component = fixture.componentInstance; + hostElement = fixture.nativeElement.querySelector('nz-input-number'); + fixture.autoDetectChanges(); + }); + + it('should set id', () => { + component.id = 'test-id'; + fixture.detectChanges(); + expect(hostElement.querySelector('input')!.id).toBe('test-id'); + }); + + it('should be apply size class', () => { + component.size = 'large'; + fixture.detectChanges(); + expect(hostElement.classList).toContain('ant-input-number-lg'); + component.size = 'small'; + fixture.detectChanges(); + expect(hostElement.classList).toContain('ant-input-number-sm'); + }); + + it('should be set placeholder', () => { + component.placeholder = 'Enter a number'; + fixture.detectChanges(); + expect(hostElement.querySelector('input')!.placeholder).toBe('Enter a number'); + }); + + it('should be set status', () => { + component.status = 'error'; + fixture.detectChanges(); + expect(hostElement.classList).toContain('ant-input-number-status-error'); + component.status = 'warning'; + fixture.detectChanges(); + expect(hostElement.classList).toContain('ant-input-number-status-warning'); + }); + + it('should be set step', () => { + component.step = 5; + fixture.detectChanges(); + expect(hostElement.querySelector('input')!.step).toBe('5'); + upStepByKeyboard(); + expect(component.value).toBe(5); + upStepByKeyboard(); + expect(component.value).toBe(10); + downStepByKeyboard(); + expect(component.value).toBe(5); + }); + + it('should be update value through the handler', () => { + component.min = 1; + component.max = 2; + fixture.detectChanges(); + upStepByHandler(); + expect(component.value).toBe(1); + upStepByHandler(); + expect(component.value).toBe(2); + upStepByHandler(); + expect(component.value).toBe(2); + downStepByHandler(); + expect(component.value).toBe(1); + downStepByHandler(); + expect(component.value).toBe(1); + }); + + it('should be update value through the handler with floating numbers', () => { + component.step = 0.1; + fixture.detectChanges(); + upStepByHandler(); + expect(component.value).toBe(0.1); + upStepByHandler(); + expect(component.value).toBe(0.2); + upStepByHandler(); + expect(component.value).toBe(0.3); + downStepByHandler(); + expect(component.value).toBe(0.2); + downStepByHandler(); + expect(component.value).toBe(0.1); + downStepByHandler(); + expect(component.value).toBe(0); + }); + + it('should be update value through the handler with hold shift key', () => { + upStepByHandler({ shiftKey: true }); + expect(component.value).toBe(10); + upStepByHandler({ shiftKey: true }); + expect(component.value).toBe(20); + downStepByHandler({ shiftKey: true }); + expect(component.value).toBe(10); + downStepByHandler({ shiftKey: true }); + expect(component.value).toBe(0); + }); + + it('should be update value through user typing', () => { + component.min = 1; + component.max = 2; + fixture.detectChanges(); + + 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('should be apply out-of-range class', async () => { + component.min = 1; + component.max = 2; + component.value = 3; + fixture.detectChanges(); + await fixture.whenStable(); + expect(hostElement.classList).toContain('ant-input-number-out-of-range'); + + component.value = 0; + fixture.detectChanges(); + await fixture.whenStable(); + expect(hostElement.classList).toContain('ant-input-number-out-of-range'); + }); + + it('should be set min and max with precision', () => { + component.precision = 0; + + // 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); + + // 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); + + // 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('should set precision', async () => { + component.precision = 1; + component.value = 1.23; + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.value).toBe(1.2); + + component.value = 1.25; + fixture.detectChanges(); + await fixture.whenStable(); + expect(component.value).toBe(1.3); + }); + + it('should be set disabled', () => { + component.disabled = true; + fixture.detectChanges(); + expect(hostElement.querySelector('input')!.disabled).toBeTruthy(); + expect(hostElement.classList).toContain('ant-input-number-disabled'); + }); + + it('should be set readonly', () => { + component.readonly = true; + fixture.detectChanges(); + expect(hostElement.querySelector('input')!.readOnly).toBeTruthy(); + expect(hostElement.classList).toContain('ant-input-number-readonly'); + }); + + it('should be focus / blur', async () => { + await fixture.whenStable(); + component.inputNumber().focus(); + expect(document.activeElement).toBe(hostElement.querySelector('input')); + component.inputNumber().blur(); + expect(document.activeElement).not.toBe(hostElement.querySelector('input')); + }); + + it('should be set bordered', () => { + component.bordered = false; + fixture.detectChanges(); + expect(hostElement.classList).toContain('ant-input-number-borderless'); + }); + + it('should be set keyboard', () => { + upStepByKeyboard(); + expect(component.value).toBe(1); + downStepByKeyboard(); + expect(component.value).toBe(0); + + component.keyboard = false; + fixture.detectChanges(); + upStepByKeyboard(); + expect(component.value).toBe(0); + }); + + it('should be hide controls', () => { + component.controls = false; + fixture.detectChanges(); + expect(hostElement.querySelector('.ant-input-number-handler-wrap')).toBeNull(); + }); + + function upStepByHandler(eventInit?: MouseEventInit): void { + const handler = hostElement.querySelector('.ant-input-number-handler-up')!; + handler.dispatchEvent(new MouseEvent('mousedown', eventInit)); + handler.dispatchEvent(new MouseEvent('mouseup')); + } + function downStepByHandler(eventInit?: MouseEventInit): void { + const handler = hostElement.querySelector('.ant-input-number-handler-down')!; + handler.dispatchEvent(new MouseEvent('mousedown', eventInit)); + handler.dispatchEvent(new MouseEvent('mouseup')); + } + + function upStepByKeyboard(): void { + hostElement.dispatchEvent(new KeyboardEvent('keydown', { keyCode: UP_ARROW })); + } + + function downStepByKeyboard(): void { + hostElement.dispatchEvent(new KeyboardEvent('keydown', { keyCode: DOWN_ARROW })); + } + + function userTypingInput(text: string): void { + const input = hostElement.querySelector('input')!; + input.value = text; + input.dispatchEvent(new Event('input')); + input.dispatchEvent(new Event('change')); + } +}); + +describe('Input number with affixes or addons', () => { + let component: InputNumberWithAffixesAndAddonsTestComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideNzIconsTesting()] + }); + fixture = TestBed.createComponent(InputNumberWithAffixesAndAddonsTestComponent); + component = fixture.componentInstance; + fixture.autoDetectChanges(); + }); + + it('should be apply affix classes', () => { + expect(component.withAffixes().nativeElement.classList).toContain('ant-input-number-affix-wrapper'); + }); + + it('should be apply addon classes', () => { + expect(component.withAddons().nativeElement.classList).toContain('ant-input-number-group-wrapper'); + }); + + it('should be apply mix classes', () => { + expect(component.withMix().nativeElement.classList).toContain('ant-input-number-group-wrapper'); + expect(component.withMix().nativeElement.querySelector('.ant-input-number-affix-wrapper')).toBeTruthy(); + }); + + it('should be apply disabled class', () => { + component.disabled = true; + fixture.detectChanges(); + expect(component.withAffixes().nativeElement.classList).toContain('ant-input-number-affix-wrapper-disabled'); + }); + + it('should be apply readonly class', () => { + component.readonly = true; + fixture.detectChanges(); + expect(component.withAffixes().nativeElement.classList).toContain('ant-input-number-affix-wrapper-readonly'); + }); + + it('should be apply borderless class', () => { + component.bordered = false; + fixture.detectChanges(); + expect(component.withAffixes().nativeElement.classList).toContain('ant-input-number-affix-wrapper-borderless'); + }); +}); + +@Component({ + standalone: true, + imports: [NzInputNumberModule, FormsModule], + template: ` + + ` +}) +class InputNumberTestComponent { + id: string | null = null; + size: NzSizeLDSType = 'default'; + placeholder: string | null = null; + status: NzStatus = ''; + step = 1; + min = Number.MIN_SAFE_INTEGER; + max = Number.MAX_SAFE_INTEGER; + precision: null | number = null; + disabled = false; + readonly = false; + bordered = true; + keyboard = true; + controls = true; + + value: number | null = null; + inputNumber = viewChild.required(NzInputNumberComponent); +} + +@Component({ + standalone: true, + imports: [NzInputNumberModule], + template: ` + + Prefix + Suffix + + + + Before + After + + + + Prefix + Suffix + Before + After + + ` +}) +class InputNumberWithAffixesAndAddonsTestComponent { + disabled = false; + readonly = false; + bordered = true; + + withAffixes = viewChild.required('withAffixes', { read: ElementRef }); + withAddons = viewChild.required('withAddons', { read: ElementRef }); + withMix = viewChild.required('withMix', { read: ElementRef }); +} diff --git a/components/input-number/input-number.component.ts b/components/input-number/input-number.component.ts index 5c0b573ec75..fe0ea5579b1 100644 --- a/components/input-number/input-number.component.ts +++ b/components/input-number/input-number.component.ts @@ -4,95 +4,158 @@ */ import { FocusMonitor } from '@angular/cdk/a11y'; -import { Direction, Directionality } from '@angular/cdk/bidi'; -import { DOWN_ARROW, ENTER, UP_ARROW } from '@angular/cdk/keycodes'; +import { Directionality } from '@angular/cdk/bidi'; +import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes'; +import { NgTemplateOutlet } from '@angular/common'; import { - AfterViewInit, + afterNextRender, + booleanAttribute, ChangeDetectionStrategy, - ChangeDetectorRef, Component, - ElementRef, - EventEmitter, - Input, - NgZone, - OnChanges, - OnDestroy, - OnInit, - Output, - Renderer2, - SimpleChanges, - ViewChild, - ViewEncapsulation, - booleanAttribute, computed, + contentChild, + DestroyRef, + ElementRef, forwardRef, inject, + Injector, + input, numberAttribute, - signal + OnInit, + output, + signal, + viewChild, + ViewEncapsulation } from '@angular/core'; -import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Subject, merge } from 'rxjs'; -import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { NzFormNoStatusService, NzFormPatchModule, NzFormStatusService } from 'ng-zorro-antd/core/form'; -import { NzDestroyService } from 'ng-zorro-antd/core/services'; -import { - NgClassInterface, - NzSizeLDSType, - NzStatus, - NzValidateStatus, - OnChangeType, - OnTouchedType -} from 'ng-zorro-antd/core/types'; -import { fromEventOutsideAngular, getStatusClassNames, isNotNil } from 'ng-zorro-antd/core/util'; +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 { NzIconModule } from 'ng-zorro-antd/icon'; +import { + NzInputAddonAfterDirective, + NzInputAddonBeforeDirective, + NzInputPrefixDirective, + NzInputSuffixDirective +} from 'ng-zorro-antd/input'; import { NZ_SPACE_COMPACT_ITEM_TYPE, NZ_SPACE_COMPACT_SIZE, NzSpaceCompactItemDirective } from 'ng-zorro-antd/space'; @Component({ selector: 'nz-input-number', exportAs: 'nzInputNumber', + standalone: true, + imports: [NzIconModule, NzFormItemFeedbackIconComponent, NgTemplateOutlet], template: ` -
- - - - - - -
-
- -
- @if (hasFeedback && !!status && !nzFormNoStatusService) { - + @if (hasAddon()) { + + } @else if (hasAffix()) { + + } @else { + } + + +
+ @if (addonBefore()) { +
+ +
+ } + + @if (hasAffix()) { + + } @else { + + } + + @if (addonAfter()) { +
+ +
+ } +
+
+ + +
+ +
+
+ + + @if (prefix()) { + + + + } + + @if (suffix()) { + + + + } + + + +
+ +
+
+ + + @if (nzControls()) { +
+ + + + + + + + + + +
+ } + +
+ +
+ @if (hasFeedback() && finalStatus().signal()) { + + } +
`, providers: [ { @@ -100,448 +163,346 @@ import { NZ_SPACE_COMPACT_ITEM_TYPE, NZ_SPACE_COMPACT_SIZE, NzSpaceCompactItemDi useExisting: forwardRef(() => NzInputNumberComponent), multi: true }, - { provide: NZ_SPACE_COMPACT_ITEM_TYPE, useValue: 'input-number' }, - NzDestroyService + { provide: NZ_SPACE_COMPACT_ITEM_TYPE, useValue: 'input-number' } ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { - class: 'ant-input-number', - '[class.ant-input-number-in-form-item]': '!!nzFormStatusService', - '[class.ant-input-number-focused]': 'isFocused', - '[class.ant-input-number-lg]': `finalSize() === 'large'`, - '[class.ant-input-number-sm]': `finalSize() === 'small'`, - '[class.ant-input-number-disabled]': 'nzDisabled', - '[class.ant-input-number-readonly]': 'nzReadOnly', - '[class.ant-input-number-rtl]': `dir === 'rtl'`, - '[class.ant-input-number-borderless]': `nzBorderless` + '[class]': 'class()', + '(keydown)': 'onKeyDown($event)' }, - imports: [NzIconModule, FormsModule, NzFormPatchModule], - standalone: true, hostDirectives: [NzSpaceCompactItemDirective] }) -export class NzInputNumberComponent implements ControlValueAccessor, AfterViewInit, OnChanges, OnInit, OnDestroy { - displayValue?: string | number; - isFocused = false; - disabled$ = new Subject(); - disabledUp = false; - disabledDown = false; - dir: Direction = 'ltr'; - // status - prefixCls: string = 'ant-input-number'; - status: NzValidateStatus = ''; - statusCls: NgClassInterface = {}; - hasFeedback: boolean = false; - onChange: OnChangeType = () => {}; - onTouched: OnTouchedType = () => {}; - - @Output() readonly nzBlur = new EventEmitter(); - @Output() readonly nzFocus = new EventEmitter(); - /** The native `` element. */ - @ViewChild('upHandler', { static: true }) upHandler!: ElementRef; - /** The native `` element. */ - @ViewChild('downHandler', { static: true }) downHandler!: ElementRef; - /** The native `` element. */ - @ViewChild('inputElement', { static: true }) inputElement!: ElementRef; - @Input() nzSize: NzSizeLDSType = 'default'; - @Input({ transform: numberAttribute }) nzMin: number = -Infinity; - @Input({ transform: numberAttribute }) nzMax: number = Infinity; - @Input() nzParser = (value: string): string => - value - .trim() - .replace(/。/g, '.') - .replace(/[^\w\.-]+/g, ''); - @Input() nzPrecision?: number; - @Input() nzPrecisionMode: 'cut' | 'toFixed' | ((value: number | string, precision?: number) => number) = 'toFixed'; - @Input() nzPlaceHolder = ''; - @Input() nzStatus: NzStatus = ''; - @Input({ transform: numberAttribute }) nzStep = 1; - @Input() nzInputMode: string = 'decimal'; - @Input() nzId: string | null = null; - @Input({ transform: booleanAttribute }) nzDisabled = false; - @Input({ transform: booleanAttribute }) nzReadOnly = false; - @Input({ transform: booleanAttribute }) nzAutoFocus = false; - @Input({ transform: booleanAttribute }) nzBorderless: boolean = false; - @Input() nzFormatter: (value: number) => string | number = value => value; +export class NzInputNumberComponent implements OnInit, ControlValueAccessor { + readonly nzId = input(null); + readonly nzSize = input('default'); + readonly nzPlaceHolder = input(null); + readonly nzStatus = input(''); + readonly nzStep = input(1, { transform: numberAttribute }); + 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 nzDisabled = input(false, { transform: booleanAttribute }); + readonly nzReadOnly = input(false, { transform: booleanAttribute }); + readonly nzAutoFocus = input(false, { transform: booleanAttribute }); + readonly nzBordered = input(true, { transform: booleanAttribute }); + readonly nzKeyboard = input(true, { transform: booleanAttribute }); + readonly nzControls = input(true, { transform: booleanAttribute }); + + readonly nzOnStep = output<{ value: number; offset: number; type: 'up' | 'down' }>(); + + private onChange: OnChangeType = () => {}; + private onTouched: OnTouchedType = () => {}; + private compactSize = inject(NZ_SPACE_COMPACT_SIZE, { optional: true }); + private inputRef = viewChild.required>('input'); + private hostRef = viewChild>('inputNumberHost'); + private elementRef = inject(ElementRef); + private injector = inject(Injector); + private focusMonitor = inject(FocusMonitor); + private directionality = inject(Directionality); + private nzFormStatusService = inject(NzFormStatusService, { optional: true }); + private autoStepTimer: ReturnType | null = null; + + protected value = signal(null); + protected displayValue = signal(''); + + protected dir = toSignal(this.directionality.change, { initialValue: this.directionality.value }); + protected focused = signal(false); + protected hasFeedback = signal(false); + // TODO: migrate to linkedSignal + protected finalStatus = computed(() => ({ + signal: signal(this.nzStatus()) + })); + // TODO: migrate to linkedSignal + protected finalDisabled = computed(() => ({ + signal: signal(this.nzDisabled()) + })); + + protected prefix = contentChild(NzInputPrefixDirective); + protected suffix = contentChild(NzInputSuffixDirective); + protected addonBefore = contentChild(NzInputAddonBeforeDirective); + protected addonAfter = contentChild(NzInputAddonAfterDirective); + protected hasAffix = computed(() => !!this.prefix() || !!this.suffix()); + protected hasAddon = computed(() => !!this.addonBefore() || !!this.addonAfter()); + + protected class = computed(() => { + if (this.hasAddon()) { + return this.groupWrapperClass(); + } + if (this.hasAffix()) { + return this.affixWrapperClass(); + } + return this.inputNumberClass(); + }); + protected inputNumberClass = computed(() => { + return { + 'ant-input-number': true, + 'ant-input-number-lg': this.finalSize() === 'large', + 'ant-input-number-sm': this.finalSize() === 'small', + 'ant-input-number-disabled': this.finalDisabled().signal(), + 'ant-input-number-readonly': this.nzReadOnly(), + 'ant-input-number-borderless': !this.nzBordered(), + 'ant-input-number-focused': this.focused(), + 'ant-input-number-rtl': this.dir() === 'rtl', + 'ant-input-number-in-form-item': !!this.nzFormStatusService, + 'ant-input-number-out-of-range': this.value() !== null && !isInRange(this.value()!, this.nzMin(), this.nzMax()), + ...getStatusClassNames('ant-input-number', this.finalStatus().signal(), this.hasFeedback()) + }; + }); + protected affixWrapperClass = computed(() => { + return { + 'ant-input-number-affix-wrapper': true, + 'ant-input-number-affix-wrapper-disabled': this.finalDisabled().signal(), + 'ant-input-number-affix-wrapper-readonly': this.nzReadOnly(), + 'ant-input-number-affix-wrapper-borderless': !this.nzBordered(), + 'ant-input-number-affix-wrapper-focused': this.focused(), + 'ant-input-number-affix-wrapper-rtl': this.dir() === 'rtl', + ...getStatusClassNames('ant-input-number-affix-wrapper', this.finalStatus().signal(), this.hasFeedback()) + }; + }); + protected groupWrapperClass = computed(() => { + return { + 'ant-input-number-group-wrapper': true, + 'ant-input-number-group-wrapper-rtl': this.dir() === 'rtl', + ...getStatusClassNames('ant-input-number-group-wrapper', this.finalStatus().signal(), this.hasFeedback()) + }; + }); protected finalSize = computed(() => { if (this.compactSize) { return this.compactSize(); } - return this.size(); + return this.nzSize(); }); - private size = signal(this.nzSize); - private compactSize = inject(NZ_SPACE_COMPACT_SIZE, { optional: true }); - private autoStepTimer?: ReturnType; - private parsedValue?: string | number; - private value?: number; - private isNzDisableFirstChange: boolean = true; - - onModelChange(value: string): void { - this.parsedValue = this.nzParser(value); - this.inputElement.nativeElement.value = `${this.parsedValue}`; - const validValue = this.getCurrentValidValue(this.parsedValue); - this.setValue(validValue); - } + protected upDisabled = computed(() => { + return !isNil(this.value()) && this.value()! >= this.nzMax(); + }); + protected downDisabled = computed(() => { + return !isNil(this.value()) && this.value()! <= this.nzMin(); + }); - getCurrentValidValue(value: string | number): number { - let val = value; - if (val === '') { - val = ''; - } else if (!this.isNotCompleteNumber(val)) { - val = `${this.getValidValue(val)}`; - } else { - val = this.value!; - } - return this.toNumber(val); - } + constructor() { + const destroyRef = inject(DestroyRef); - // '1.' '1x' 'xx' '' => are not complete numbers - isNotCompleteNumber(num: string | number): boolean { - return ( - isNaN(num as number) || - num === '' || - num === null || - !!(num && num.toString().indexOf('.') === num.toString().length - 1) - ); - } + afterNextRender(() => { + const hostRef = this.hostRef(); + const element = hostRef ? hostRef : this.elementRef; - getValidValue(value?: string | number): string | number | undefined { - let val = parseFloat(value as string); - // /~https://github.com/ant-design/ant-design/issues/7358 - if (isNaN(val)) { - return value; - } - if (val < this.nzMin) { - val = this.nzMin; - } - if (val > this.nzMax) { - val = this.nzMax; - } - return val; - } + this.focusMonitor + .monitor(element, true) + .pipe(takeUntilDestroyed(destroyRef)) + .subscribe(origin => { + this.focused.set(!!origin); - toNumber(num: string | number): number { - if (this.isNotCompleteNumber(num)) { - return num as number; - } - const numStr = String(num); - if (numStr.indexOf('.') >= 0 && isNotNil(this.nzPrecision)) { - if (typeof this.nzPrecisionMode === 'function') { - return this.nzPrecisionMode(num, this.nzPrecision); - } else if (this.nzPrecisionMode === 'cut') { - const numSplit = numStr.split('.'); - numSplit[1] = numSplit[1].slice(0, this.nzPrecision); - return Number(numSplit.join('.')); - } - return Number(Number(num).toFixed(this.nzPrecision)); - } - return Number(num); - } + if (!origin) { + this.onTouched(); + } + }); - getRatio(e: KeyboardEvent): number { - let ratio = 1; - if (e.metaKey || e.ctrlKey) { - ratio = 0.1; - } else if (e.shiftKey) { - ratio = 10; - } - return ratio; + destroyRef.onDestroy(() => { + this.focusMonitor.stopMonitoring(element); + }); + }); + + this.nzFormStatusService?.formStatusChanges.pipe(takeUntilDestroyed()).subscribe(({ status, hasFeedback }) => { + this.finalStatus().signal.set(status); + this.hasFeedback.set(hasFeedback); + }); } - down(e: MouseEvent | KeyboardEvent, ratio?: number): void { - if (!this.isFocused) { - this.focus(); + ngOnInit(): void { + if (this.nzAutoFocus()) { + afterNextRender(() => this.focus(), { injector: this.injector }); } - this.step('down', e, ratio); } - up(e: MouseEvent | KeyboardEvent, ratio?: number): void { - if (!this.isFocused) { - this.focus(); - } - this.step('up', e, ratio); + writeValue(value: number | null): void { + this.setValue(value); } - getPrecision(value: number): number { - const valueString = value.toString(); - if (valueString.indexOf('e-') >= 0) { - return parseInt(valueString.slice(valueString.indexOf('e-') + 2), 10); - } - let precision = 0; - if (valueString.indexOf('.') >= 0) { - precision = valueString.length - valueString.indexOf('.') - 1; - } - return precision; + registerOnChange(fn: OnChangeType): void { + this.onChange = fn; } - // step={1.0} value={1.51} - // press + - // then value should be 2.51, rather than 2.5 - // if this.props.precision is undefined - // /~https://github.com/react-component/input-number/issues/39 - getMaxPrecision(currentValue: string | number, ratio: number): number { - if (isNotNil(this.nzPrecision)) { - return this.nzPrecision; - } - const ratioPrecision = this.getPrecision(ratio); - const stepPrecision = this.getPrecision(this.nzStep); - const currentValuePrecision = this.getPrecision(currentValue as number); - if (!currentValue) { - return ratioPrecision + stepPrecision; - } - return Math.max(currentValuePrecision, ratioPrecision + stepPrecision); + registerOnTouched(fn: OnTouchedType): void { + this.onTouched = fn; } - getPrecisionFactor(currentValue: string | number, ratio: number): number { - const precision = this.getMaxPrecision(currentValue, ratio); - return Math.pow(10, precision); + setDisabledState(disabled: boolean): void { + this.finalDisabled().signal.set(disabled); } - upStep(val: string | number, rat: number): number { - const precisionFactor = this.getPrecisionFactor(val, rat); - const precision = Math.abs(this.getMaxPrecision(val, rat)); - let result; - if (typeof val === 'number') { - result = ((precisionFactor * val + precisionFactor * this.nzStep * rat) / precisionFactor).toFixed(precision); - } else { - result = this.nzMin === -Infinity ? this.nzStep : this.nzMin; - } - return this.toNumber(result); + focus(): void { + this.inputRef().nativeElement.focus(); } - downStep(val: string | number, rat: number): number { - const precisionFactor = this.getPrecisionFactor(val, rat); - const precision = Math.abs(this.getMaxPrecision(val, rat)); - let result; - if (typeof val === 'number') { - result = ((precisionFactor * val - precisionFactor * this.nzStep * rat) / precisionFactor).toFixed(precision); - } else { - result = this.nzMin === -Infinity ? -this.nzStep : this.nzMin; - } - return this.toNumber(result); + blur(): void { + this.inputRef().nativeElement.blur(); } - step(type: T, e: MouseEvent | KeyboardEvent, ratio: number = 1): void { - this.stop(); - e.preventDefault(); - if (this.nzDisabled) { + private step(event: MouseEvent | KeyboardEvent, up: boolean): void { + // Ignore step since out of range + if ((up && this.upDisabled()) || (!up && this.downDisabled())) { return; } - const value = this.getCurrentValidValue(this.parsedValue!) || 0; - let val = 0; - if (type === 'up') { - val = this.upStep(value, ratio); - } else if (type === 'down') { - val = this.downStep(value, ratio); - } - const outOfRange = val > this.nzMax || val < this.nzMin; - if (val > this.nzMax) { - val = this.nzMax; - } else if (val < this.nzMin) { - val = this.nzMin; - } - this.setValue(val); - this.updateDisplayValue(val); - this.isFocused = true; - if (outOfRange) { - return; - } - this.autoStepTimer = setTimeout(() => { - (this[type] as (e: MouseEvent | KeyboardEvent, ratio: number) => void)(e, ratio); - }, 300); - } - stop(): void { - if (this.autoStepTimer) { - clearTimeout(this.autoStepTimer); + // When hold the shift key, the step is 10 times + let step = event.shiftKey ? this.nzStep() * 10 : this.nzStep(); + 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); + + this.nzOnStep.emit({ + type: up ? 'up' : 'down', + value: this.value()!, + offset: this.nzStep() + }); + + this.focus(); } - setValue(value: number): void { - if (`${this.value}` !== `${value}`) { - this.onChange(value); - } - this.value = value; - this.parsedValue = value; - this.disabledUp = this.disabledDown = false; - if (value || value === 0) { - const val = Number(value); - if (val >= this.nzMax) { - this.disabledUp = true; - } - if (val <= this.nzMin) { - this.disabledDown = true; + private setValue(value: number | string | null, userTyping?: boolean): void { + let parsedValue: number | null = null; + + if (!isNil(value)) { + parsedValue = this.nzParser()(value.toString()); + + // 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()); + } } } - } - updateDisplayValue(value: number): void { - const displayValue = isNotNil(this.nzFormatter(value)) ? this.nzFormatter(value) : ''; - this.displayValue = displayValue; - this.inputElement.nativeElement.value = `${displayValue}`; + this.value.set(parsedValue); + this.displayValue.set(parsedValue === null ? '' : this.nzFormatter()(parsedValue)); + this.onChange(parsedValue); } - writeValue(value: number): void { - this.value = value; - this.setValue(value); - this.updateDisplayValue(value); - this.cdr.markForCheck(); + protected stopAutoStep(): void { + if (this.autoStepTimer !== null) { + clearTimeout(this.autoStepTimer); + this.autoStepTimer = null; + } } - registerOnChange(fn: OnChangeType): void { - this.onChange = fn; - } + protected onStepMouseDown(event: MouseEvent | KeyboardEvent, up: boolean): void { + event.preventDefault(); + this.stopAutoStep(); - registerOnTouched(fn: OnTouchedType): void { - this.onTouched = fn; - } + this.step(event, up); - setDisabledState(disabled: boolean): void { - this.nzDisabled = (this.isNzDisableFirstChange && this.nzDisabled) || disabled; - this.isNzDisableFirstChange = false; - this.disabled$.next(this.nzDisabled); - this.cdr.markForCheck(); - } + // Loop step for interval + const loopStep: () => void = () => { + this.step(event, up); + this.autoStepTimer = setTimeout(loopStep, STEP_INTERVAL); + }; - focus(): void { - this.focusMonitor.focusVia(this.inputElement, 'keyboard'); + // First time press will wait some time to trigger loop step update + this.autoStepTimer = setTimeout(loopStep, STEP_DELAY); } - blur(): void { - this.inputElement.nativeElement.blur(); + protected onKeyDown(event: KeyboardEvent): void { + switch (event.keyCode) { + case UP_ARROW: + event.preventDefault(); + this.nzKeyboard() && this.step(event, true); + break; + case DOWN_ARROW: + event.preventDefault(); + this.nzKeyboard() && this.step(event, false); + break; + } } - nzFormStatusService = inject(NzFormStatusService, { optional: true }); - nzFormNoStatusService = inject(NzFormNoStatusService, { optional: true }); - - constructor( - private ngZone: NgZone, - private elementRef: ElementRef, - private cdr: ChangeDetectorRef, - private focusMonitor: FocusMonitor, - private renderer: Renderer2, - private directionality: Directionality, - private destroy$: NzDestroyService - ) {} - - ngOnInit(): void { - this.nzFormStatusService?.formStatusChanges - .pipe( - distinctUntilChanged((pre, cur) => { - return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; - }), - takeUntil(this.destroy$) - ) - .subscribe(({ status, hasFeedback }) => { - this.setStatusStyles(status, hasFeedback); - }); + protected onInputChange(value: Event): void { + const target = value.target as HTMLInputElement; + this.setValue(target.value, true); + } +} - this.focusMonitor - .monitor(this.elementRef, true) - .pipe(takeUntil(this.destroy$)) - .subscribe(focusOrigin => { - if (!focusOrigin) { - this.isFocused = false; - this.updateDisplayValue(this.value!); - this.nzBlur.emit(); - Promise.resolve().then(() => this.onTouched()); - } else { - this.isFocused = true; - this.nzFocus.emit(); - } - }); +/** + * When click and hold on a button - the speed of auto changing the value. + */ +const STEP_INTERVAL = 200; - this.dir = this.directionality.value; - this.directionality.change.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { - this.dir = direction; - }); +/** + * When click and hold on a button - the delay before auto changing the value. + */ +const STEP_DELAY = 600; - this.setupHandlersListeners(); +function defaultParser(value: string): number { + return +value.trim().replace(/。/g, '.'); + // [Legacy] We still support auto convert `$ 123,456` to `123456` + // .replace(/[^\w.-]+/g, ''); +} - fromEventOutsideAngular(this.inputElement.nativeElement, 'keyup') - .pipe(takeUntil(this.destroy$)) - .subscribe(() => this.stop()); +function isInRange(value: number, min: number, max: number): boolean { + return value >= min && value <= max; +} - fromEventOutsideAngular(this.inputElement.nativeElement, 'keydown') - .pipe(takeUntil(this.destroy$)) - .subscribe(event => { - const { keyCode } = event; +function getRangeValue(value: number, min: number, max: number): number { + if (value < min) { + return min; + } - if (keyCode !== UP_ARROW && keyCode !== DOWN_ARROW && keyCode !== ENTER) { - return; - } + if (value > max) { + return max; + } - this.ngZone.run(() => { - if (keyCode === UP_ARROW) { - const ratio = this.getRatio(event); - this.up(event, ratio); - this.stop(); - } else if (keyCode === DOWN_ARROW) { - const ratio = this.getRatio(event); - this.down(event, ratio); - this.stop(); - } else { - this.updateDisplayValue(this.value!); - } + return value; +} - this.cdr.markForCheck(); - }); - }); +/** + * 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 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 { + if (precision === null) { + return getRangeValue(value, min, max); } - ngOnChanges({ nzStatus, nzDisabled, nzFormatter, nzSize }: SimpleChanges): void { - if (nzFormatter && !nzFormatter.isFirstChange()) { - const validValue = this.getCurrentValidValue(this.parsedValue!); - this.setValue(validValue); - this.updateDisplayValue(validValue); - } - if (nzDisabled) { - this.disabled$.next(this.nzDisabled); - } - if (nzStatus) { - this.setStatusStyles(this.nzStatus, this.hasFeedback); - } - if (nzSize) { - this.size.set(nzSize.currentValue); - } - } + const fixedValue = +value.toFixed(precision); + const multiple = Math.pow(10, precision); - ngAfterViewInit(): void { - if (this.nzAutoFocus) { - this.focus(); - } + if (fixedValue < min) { + return Math.ceil(min * multiple) / multiple; } - ngOnDestroy(): void { - this.focusMonitor.stopMonitoring(this.elementRef); + if (fixedValue > max) { + return Math.floor(max * multiple) / multiple; } - private setupHandlersListeners(): void { - merge( - fromEventOutsideAngular(this.upHandler.nativeElement, 'mouseup'), - fromEventOutsideAngular(this.upHandler.nativeElement, 'mouseleave'), - fromEventOutsideAngular(this.downHandler.nativeElement, 'mouseup'), - fromEventOutsideAngular(this.downHandler.nativeElement, 'mouseleave') - ) - .pipe(takeUntil(this.destroy$)) - .subscribe(() => this.stop()); - } + return fixedValue; +} - private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void { - // set inner status - this.status = status; - this.hasFeedback = hasFeedback; - this.cdr.markForCheck(); - // render status if nzStatus is set - this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback); - Object.keys(this.statusCls).forEach(status => { - if (this.statusCls[status]) { - this.renderer.addClass(this.elementRef.nativeElement, status); - } else { - this.renderer.removeClass(this.elementRef.nativeElement, status); - } - }); - } +function getDecimalPlaces(num: number): number { + return num.toString().split('.')[1]?.length || 0; } diff --git a/components/input-number/input-number.module.ts b/components/input-number/input-number.module.ts index d3e0c525bea..dcd73daa834 100644 --- a/components/input-number/input-number.module.ts +++ b/components/input-number/input-number.module.ts @@ -5,20 +5,29 @@ import { NgModule } from '@angular/core'; -import { NzInputNumberGroupSlotComponent } from './input-number-group-slot.component'; import { - NzInputNumberGroupComponent, - NzInputNumberGroupWhitSuffixOrPrefixDirective -} from './input-number-group.component'; + NzInputAddonAfterDirective, + NzInputAddonBeforeDirective, + NzInputPrefixDirective, + NzInputSuffixDirective +} from 'ng-zorro-antd/input'; + import { NzInputNumberComponent } from './input-number.component'; @NgModule({ imports: [ NzInputNumberComponent, - NzInputNumberGroupComponent, - NzInputNumberGroupWhitSuffixOrPrefixDirective, - NzInputNumberGroupSlotComponent + NzInputAddonBeforeDirective, + NzInputAddonAfterDirective, + NzInputPrefixDirective, + NzInputSuffixDirective ], - exports: [NzInputNumberComponent, NzInputNumberGroupComponent, NzInputNumberGroupWhitSuffixOrPrefixDirective] + exports: [ + NzInputNumberComponent, + NzInputAddonBeforeDirective, + NzInputAddonAfterDirective, + NzInputPrefixDirective, + NzInputSuffixDirective + ] }) export class NzInputNumberModule {} diff --git a/components/input-number/public-api.ts b/components/input-number/public-api.ts index 9434308324b..ef0ae535ce3 100644 --- a/components/input-number/public-api.ts +++ b/components/input-number/public-api.ts @@ -4,6 +4,4 @@ */ export * from './input-number.component'; -export * from './input-number-group-slot.component'; -export * from './input-number-group.component'; export * from './input-number.module'; diff --git a/components/input-number/style/patch.less b/components/input-number/style/patch.less index 78d69682962..13b7a5751ea 100644 --- a/components/input-number/style/patch.less +++ b/components/input-number/style/patch.less @@ -1,6 +1,6 @@ .@{ant-prefix}-input-number { &-affix-wrapper { - > nz-input-number.@{ant-prefix}-input-number { + > nz-input-number-legacy.@{ant-prefix}-input-number { width: 100%; border: none; outline: none; diff --git a/components/input/input-addon.directive.ts b/components/input/input-addon.directive.ts new file mode 100644 index 00000000000..3aa6107dddf --- /dev/null +++ b/components/input/input-addon.directive.ts @@ -0,0 +1,18 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at /~https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { Directive } from '@angular/core'; + +@Directive({ + selector: '[nzInputAddonBefore]', + standalone: true +}) +export class NzInputAddonBeforeDirective {} + +@Directive({ + selector: '[nzInputAddonAfter]', + standalone: true +}) +export class NzInputAddonAfterDirective {} diff --git a/components/input/input-affix.directive.ts b/components/input/input-affix.directive.ts new file mode 100644 index 00000000000..e3f82fd7ffd --- /dev/null +++ b/components/input/input-affix.directive.ts @@ -0,0 +1,18 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at /~https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { Directive } from '@angular/core'; + +@Directive({ + selector: '[nzInputPrefix]', + standalone: true +}) +export class NzInputPrefixDirective {} + +@Directive({ + selector: '[nzInputSuffix]', + standalone: true +}) +export class NzInputSuffixDirective {} diff --git a/components/input/public-api.ts b/components/input/public-api.ts index a35ed1cdc8b..f7781869137 100644 --- a/components/input/public-api.ts +++ b/components/input/public-api.ts @@ -3,11 +3,12 @@ * found in the LICENSE file at /~https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ -export * from './input-group.component'; -export * from './input.module'; -export * from './input-group.component'; +export * from './autosize.directive'; +export * from './input-addon.directive'; +export * from './input-affix.directive'; export * from './input-group-slot.component'; +export * from './input-group.component'; +export * from './input-otp.component'; export * from './input.directive'; -export * from './autosize.directive'; +export * from './input.module'; export * from './textarea-count.component'; -export * from './input-otp.component'; diff --git a/components/space/space-compact.component.spec.ts b/components/space/space-compact.component.spec.ts index 6c384cb763d..e69ba9f176e 100644 --- a/components/space/space-compact.component.spec.ts +++ b/components/space/space-compact.component.spec.ts @@ -8,6 +8,7 @@ import { NzSizeLDSType } from 'ng-zorro-antd/core/types'; import { NzDatePickerModule } from 'ng-zorro-antd/date-picker'; import { NzInputModule } from 'ng-zorro-antd/input'; import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; +import { NzInputNumberLegacyModule } from 'ng-zorro-antd/input-number-legacy'; import { NzSelectModule } from 'ng-zorro-antd/select'; import { NzTimePickerModule } from 'ng-zorro-antd/time-picker'; import { NzTreeSelectModule } from 'ng-zorro-antd/tree-select'; @@ -33,6 +34,7 @@ describe('Space compact', () => { const nzInput = spaceCompactElement.querySelector('input[nz-input]'); const nzInputGroup = spaceCompactElement.querySelector('nz-input-group'); const nzInputNumber = spaceCompactElement.querySelector('nz-input-number'); + const nzInputNumberLegacy = spaceCompactElement.querySelector('nz-input-number-legacy'); const nzDatePicker = spaceCompactElement.querySelector('nz-date-picker'); const nzRangePicker = spaceCompactElement.querySelector('nz-range-picker'); const nzTimePicker = spaceCompactElement.querySelector('nz-time-picker'); @@ -43,6 +45,7 @@ describe('Space compact', () => { expect(nzInput).toBeTruthy(); expect(nzInputNumber).toBeTruthy(); + expect(nzInputNumberLegacy).toBeTruthy(); expect(nzInputGroup).toBeTruthy(); expect(nzDatePicker).toBeTruthy(); expect(nzRangePicker).toBeTruthy(); @@ -56,6 +59,7 @@ describe('Space compact', () => { expect(nzInputGroup!.classList).toContain('ant-input-compact-item'); expect(nzInputNumber!.classList).toContain('ant-input-number-compact-item'); + expect(nzInputNumberLegacy!.classList).toContain('ant-input-number-compact-item'); expect(nzDatePicker!.classList).toContain('ant-picker-compact-item'); expect(nzRangePicker!.classList).toContain('ant-picker-compact-item'); @@ -96,6 +100,7 @@ describe('Space compact', () => { const spaceCompactElement: HTMLElement = fixture.nativeElement; const nzInput = spaceCompactElement.querySelector('input[nz-input]'); const nzInputNumber = spaceCompactElement.querySelector('nz-input-number'); + const nzInputNumberLegacy = spaceCompactElement.querySelector('nz-input-number-legacy'); const nzDatePicker = spaceCompactElement.querySelector('nz-date-picker'); const nzRangePicker = spaceCompactElement.querySelector('nz-range-picker'); const nzTimePicker = spaceCompactElement.querySelector('nz-time-picker'); @@ -109,6 +114,7 @@ describe('Space compact', () => { expect(nzInput!.classList).toContain('ant-input-sm'); expect(nzInputNumber!.classList).toContain('ant-input-number-sm'); + expect(nzInputNumberLegacy!.classList).toContain('ant-input-number-sm'); expect(nzDatePicker!.classList).toContain('ant-picker-small'); expect(nzRangePicker!.classList).toContain('ant-picker-small'); expect(nzTimePicker!.classList).toContain('ant-picker-small'); @@ -122,6 +128,7 @@ describe('Space compact', () => { expect(nzInput!.classList).toContain('ant-input-lg'); expect(nzInputNumber!.classList).toContain('ant-input-number-lg'); + expect(nzInputNumberLegacy!.classList).toContain('ant-input-number-lg'); expect(nzDatePicker!.classList).toContain('ant-picker-large'); expect(nzRangePicker!.classList).toContain('ant-picker-large'); expect(nzTimePicker!.classList).toContain('ant-picker-large'); @@ -204,6 +211,7 @@ describe('Space compact direction', () => { NzButtonModule, NzInputModule, NzInputNumberModule, + NzInputNumberLegacyModule, NzSelectModule, NzCascaderModule, NzTreeSelectModule, @@ -217,6 +225,7 @@ describe('Space compact direction', () => { } + diff --git a/scripts/site/_site/doc/app/side/side.component.html b/scripts/site/_site/doc/app/side/side.component.html index 67cf45d35c3..befaf179419 100644 --- a/scripts/site/_site/doc/app/side/side.component.html +++ b/scripts/site/_site/doc/app/side/side.component.html @@ -1,4 +1,10 @@ -