From b038fa26703ab010dab1b03946986e6f5c6ee66c Mon Sep 17 00:00:00 2001 From: simplejason Date: Thu, 2 Jun 2022 18:26:49 +0800 Subject: [PATCH] feat(module:input-number): support input number group (#7488) * feat(module:input-number): support input number group feat(module:input-number): add nzCompact * feat(module:input-number): add tests * feat(module:input-number): fix some errors * feat(module:input-number): fix demos --- components/input-number/demo/addon.md | 14 + components/input-number/demo/addon.ts | 41 ++ components/input-number/demo/group.md | 18 + components/input-number/demo/group.ts | 47 ++ components/input-number/demo/module | 8 +- components/input-number/demo/prefix.md | 14 + components/input-number/demo/prefix.ts | 19 + components/input-number/demo/status.ts | 22 +- components/input-number/doc/index.en-US.md | 14 + components/input-number/doc/index.zh-CN.md | 14 + .../input-number-group-slot.component.ts | 28 ++ .../input-number-group.component.ts | 256 +++++++++++ .../input-number/input-number-group.spec.ts | 400 ++++++++++++++++++ .../input-number/input-number.component.ts | 26 +- .../input-number/input-number.module.ts | 17 +- components/input-number/public-api.ts | 2 + components/input-number/style/entry.less | 1 + components/input-number/style/patch.less | 13 + 18 files changed, 932 insertions(+), 22 deletions(-) create mode 100644 components/input-number/demo/addon.md create mode 100644 components/input-number/demo/addon.ts create mode 100644 components/input-number/demo/group.md create mode 100644 components/input-number/demo/group.ts create mode 100644 components/input-number/demo/prefix.md create mode 100644 components/input-number/demo/prefix.ts create mode 100644 components/input-number/input-number-group-slot.component.ts create mode 100644 components/input-number/input-number-group.component.ts create mode 100644 components/input-number/input-number-group.spec.ts create mode 100644 components/input-number/style/patch.less diff --git a/components/input-number/demo/addon.md b/components/input-number/demo/addon.md new file mode 100644 index 0000000000..c45680cd91 --- /dev/null +++ b/components/input-number/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/demo/addon.ts b/components/input-number/demo/addon.ts new file mode 100644 index 0000000000..25d557438b --- /dev/null +++ b/components/input-number/demo/addon.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-input-number-addon', + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` +}) +export class NzDemoInputNumberAddonComponent { + demoValue = 100; +} diff --git a/components/input-number/demo/group.md b/components/input-number/demo/group.md new file mode 100644 index 0000000000..53f5343b0c --- /dev/null +++ b/components/input-number/demo/group.md @@ -0,0 +1,18 @@ +--- +order: 9 +title: + zh-CN: 输入框组合 + en-US: Input Number Group +--- + +## zh-CN + +数字输入框的组合展现。 + +注意:使用 `nzCompact` 模式时,不需要通过 `nz-col` 来控制宽度。 + +## en-US + +InputNumber.Group example. + +Note: You don't need `nz-col` to control the width in the `nzCompact` mode. \ No newline at end of file diff --git a/components/input-number/demo/group.ts b/components/input-number/demo/group.ts new file mode 100644 index 0000000000..87a38ff8c2 --- /dev/null +++ b/components/input-number/demo/group.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-input-number-group', + template: ` + + +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ` +}) +export class NzDemoInputNumberGroupComponent {} diff --git a/components/input-number/demo/module b/components/input-number/demo/module index c0b4e30026..28b62df768 100644 --- a/components/input-number/demo/module +++ b/components/input-number/demo/module @@ -1,4 +1,10 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzSpaceModule } from 'ng-zorro-antd/space'; +import { NzSelectModule } from 'ng-zorro-antd/select'; +import { NzCascaderModule } from 'ng-zorro-antd/cascader'; +import { NzGridModule } from 'ng-zorro-antd/grid'; +import { NzDatePickerModule } from 'ng-zorro-antd/date-picker'; -export const moduleList = [ NzInputNumberModule, NzButtonModule ]; +export const moduleList = [ NzInputNumberModule, NzButtonModule, NzIconModule, NzSpaceModule, NzSelectModule, NzCascaderModule, NzGridModule, NzDatePickerModule ]; diff --git a/components/input-number/demo/prefix.md b/components/input-number/demo/prefix.md new file mode 100644 index 0000000000..c1207bfc41 --- /dev/null +++ b/components/input-number/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/demo/prefix.ts b/components/input-number/demo/prefix.ts new file mode 100644 index 0000000000..3723528b38 --- /dev/null +++ b/components/input-number/demo/prefix.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-input-number-prefix', + template: ` + + + + + + + + + + + + ` +}) +export class NzDemoInputNumberPrefixComponent {} diff --git a/components/input-number/demo/status.ts b/components/input-number/demo/status.ts index d719907100..1681fb56d9 100644 --- a/components/input-number/demo/status.ts +++ b/components/input-number/demo/status.ts @@ -3,16 +3,16 @@ import { Component } from '@angular/core'; @Component({ selector: 'nz-demo-input-number-status', template: ` - - - `, - styles: [ - ` - nz-input-number { - width: 100%; - 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 cf99147382..db68ee06e4 100755 --- a/components/input-number/doc/index.en-US.md +++ b/components/input-number/doc/index.en-US.md @@ -41,6 +41,20 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; | `(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` diff --git a/components/input-number/doc/index.zh-CN.md b/components/input-number/doc/index.zh-CN.md index 73f8eef0cc..993866c5e4 100755 --- a/components/input-number/doc/index.zh-CN.md +++ b/components/input-number/doc/index.zh-CN.md @@ -42,6 +42,20 @@ import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; | `(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` 等方法获得实例后调用 diff --git a/components/input-number/input-number-group-slot.component.ts b/components/input-number/input-number-group-slot.component.ts new file mode 100644 index 0000000000..7b6f4f5fc1 --- /dev/null +++ b/components/input-number/input-number-group-slot.component.ts @@ -0,0 +1,28 @@ +/** + * 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 { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector: '[nz-input-number-group-slot]', + preserveWhitespaces: false, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + {{ template }} + + `, + host: { + '[class.ant-input-number-group-addon]': `type === 'addon'`, + '[class.ant-input-number-prefix]': `type === 'prefix'`, + '[class.ant-input-number-suffix]': `type === 'suffix'` + } +}) +export class NzInputNumberGroupSlotComponent { + @Input() icon?: string | null = null; + @Input() type: 'addon' | 'prefix' | 'suffix' | null = null; + @Input() template?: string | TemplateRef | null = null; +} diff --git a/components/input-number/input-number-group.component.ts b/components/input-number/input-number-group.component.ts new file mode 100644 index 0000000000..211787342f --- /dev/null +++ b/components/input-number/input-number-group.component.ts @@ -0,0 +1,256 @@ +/** + * 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 { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + Directive, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + Optional, + QueryList, + Renderer2, + SimpleChanges, + TemplateRef, + ViewEncapsulation +} from '@angular/core'; +import { merge, Subject } from 'rxjs'; +import { map, mergeMap, startWith, switchMap, takeUntil } from 'rxjs/operators'; + +import { BooleanInput, NgClassInterface, NzSizeLDSType, NzStatus, NzValidateStatus } from 'ng-zorro-antd/core/types'; +import { getStatusClassNames, InputBoolean } from 'ng-zorro-antd/core/util'; + +import { NzInputNumberComponent } from './input-number.component'; + +@Directive({ + selector: `nz-input-number-group[nzSuffix], nz-input-number-group[nzPrefix]` +}) +export class NzInputNumberGroupWhitSuffixOrPrefixDirective { + constructor(public elementRef: ElementRef) {} +} + +@Component({ + selector: 'nz-input-number-group', + exportAs: 'nzInputNumberGroup', + preserveWhitespaces: false, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +
+
+ +
+ +
+ + + + + + + + + + + + + + + `, + host: { + '[class.ant-input-number-group]': 'nzCompact', + '[class.ant-input-number-group-compact]': 'nzCompact', + '[class.ant-input-number-group-wrapper]': `isAddOn`, + '[class.ant-input-number-group-wrapper-rtl]': `isAddOn && dir === 'rtl'`, + '[class.ant-input-number-group-wrapper-lg]': `isAddOn && isLarge`, + '[class.ant-input-number-group-wrapper-sm]': `isAddOn && isSmall`, + '[class.ant-input-number-affix-wrapper]': `!isAddOn && isAffix`, + '[class.ant-input-number-affix-wrapper-rtl]': `!isAddOn && dir === 'rtl'`, + '[class.ant-input-number-affix-wrapper-focused]': `!isAddOn && isAffix && focused`, + '[class.ant-input-number-affix-wrapper-disabled]': `!isAddOn && isAffix && disabled`, + '[class.ant-input-number-affix-wrapper-lg]': `!isAddOn && isAffix && isLarge`, + '[class.ant-input-number-affix-wrapper-sm]': `!isAddOn && isAffix && isSmall` + } +}) +export class NzInputNumberGroupComponent implements AfterContentInit, OnChanges, OnInit, OnDestroy { + static ngAcceptInputType_nzCompact: BooleanInput; + + @ContentChildren(NzInputNumberComponent, { descendants: true }) + listOfNzInputNumberComponent!: QueryList; + @Input() nzAddOnBeforeIcon?: string | null = null; + @Input() nzAddOnAfterIcon?: string | null = null; + @Input() nzPrefixIcon?: string | null = null; + @Input() nzSuffixIcon?: string | null = null; + @Input() nzAddOnBefore?: string | TemplateRef; + @Input() nzAddOnAfter?: string | TemplateRef; + @Input() nzPrefix?: string | TemplateRef; + @Input() nzStatus: NzStatus = ''; + @Input() nzSuffix?: string | TemplateRef; + @Input() nzSize: NzSizeLDSType = 'default'; + @Input() @InputBoolean() nzCompact = false; + isLarge = false; + isSmall = false; + isAffix = false; + isAddOn = false; + focused = false; + disabled = false; + dir: Direction = 'ltr'; + // status + prefixCls: string = 'ant-input-number'; + status: NzValidateStatus = ''; + affixStatusCls: NgClassInterface = {}; + groupStatusCls: NgClassInterface = {}; + hasFeedback: boolean = false; + private destroy$ = new Subject(); + + constructor( + private focusMonitor: FocusMonitor, + private elementRef: ElementRef, + private renderer: Renderer2, + private cdr: ChangeDetectorRef, + @Optional() private directionality: Directionality + ) {} + + updateChildrenInputSize(): void { + if (this.listOfNzInputNumberComponent) { + this.listOfNzInputNumberComponent.forEach(item => (item.nzSize = this.nzSize)); + } + } + + ngOnInit(): void { + this.focusMonitor + .monitor(this.elementRef, true) + .pipe(takeUntil(this.destroy$)) + .subscribe(focusOrigin => { + this.focused = !!focusOrigin; + this.cdr.markForCheck(); + }); + + this.dir = this.directionality.value; + this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { + this.dir = direction; + }); + } + + ngAfterContentInit(): void { + this.updateChildrenInputSize(); + const listOfInputChange$ = this.listOfNzInputNumberComponent.changes.pipe( + startWith(this.listOfNzInputNumberComponent) + ); + listOfInputChange$ + .pipe( + switchMap(list => + merge(...[listOfInputChange$, ...list.map((input: NzInputNumberComponent) => input.disabled$)]) + ), + mergeMap(() => listOfInputChange$), + map(list => list.some((input: NzInputNumberComponent) => input.nzDisabled)), + takeUntil(this.destroy$) + ) + .subscribe(disabled => { + this.disabled = disabled; + this.cdr.markForCheck(); + }); + } + ngOnChanges(changes: SimpleChanges): void { + const { + nzSize, + nzSuffix, + nzPrefix, + nzPrefixIcon, + nzSuffixIcon, + nzAddOnAfter, + nzAddOnBefore, + nzAddOnAfterIcon, + nzAddOnBeforeIcon, + nzStatus + } = changes; + if (nzSize) { + this.updateChildrenInputSize(); + this.isLarge = this.nzSize === 'large'; + this.isSmall = this.nzSize === 'small'; + } + if (nzSuffix || nzPrefix || nzPrefixIcon || nzSuffixIcon) { + this.isAffix = !!(this.nzSuffix || this.nzPrefix || this.nzPrefixIcon || this.nzSuffixIcon); + } + if (nzAddOnAfter || nzAddOnBefore || nzAddOnAfterIcon || nzAddOnBeforeIcon) { + this.isAddOn = !!(this.nzAddOnAfter || this.nzAddOnBefore || this.nzAddOnAfterIcon || this.nzAddOnBeforeIcon); + } + if (nzStatus) { + this.setStatusStyles(this.nzStatus, this.hasFeedback); + } + } + ngOnDestroy(): void { + this.focusMonitor.stopMonitoring(this.elementRef); + this.destroy$.next(); + this.destroy$.complete(); + } + + private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void { + this.status = status; + this.hasFeedback = hasFeedback; + this.cdr.markForCheck(); + // render status if nzStatus is set + this.affixStatusCls = getStatusClassNames( + `${this.prefixCls}-affix-wrapper`, + this.isAddOn ? '' : status, + hasFeedback + ); + this.groupStatusCls = getStatusClassNames( + `${this.prefixCls}-group-wrapper`, + this.isAddOn ? status : '', + hasFeedback + ); + const statusCls = { + ...this.affixStatusCls, + ...this.groupStatusCls + }; + Object.keys(statusCls).forEach(status => { + if (statusCls[status]) { + this.renderer.addClass(this.elementRef.nativeElement, status); + } else { + this.renderer.removeClass(this.elementRef.nativeElement, status); + } + }); + } +} diff --git a/components/input-number/input-number-group.spec.ts b/components/input-number/input-number-group.spec.ts new file mode 100644 index 0000000000..1524c07664 --- /dev/null +++ b/components/input-number/input-number-group.spec.ts @@ -0,0 +1,400 @@ +import { BidiModule, Direction } from '@angular/cdk/bidi'; +import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; + +import { dispatchFakeEvent } from 'ng-zorro-antd/core/testing'; +import { NzSizeLDSType, NzStatus } from 'ng-zorro-antd/core/types'; +import { NzIconTestModule } 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'; + +describe('input-number-group', () => { + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [BidiModule, NzInputNumberModule, FormsModule, ReactiveFormsModule, NzIconTestModule], + declarations: [ + NzTestInputNumberGroupAddonComponent, + NzTestInputNumberGroupAffixComponent, + NzTestInputNumberGroupMultipleComponent, + NzTestInputNumberGroupColComponent, + NzTestInputNumberGroupMixComponent, + NzTestInputNumberGroupWithStatusComponent, + NzTestInputNumberGroupWithDirComponent + ], + providers: [] + }).compileComponents(); + }) + ); + describe('input number group', () => { + describe('addon', () => { + let testComponent: NzTestInputNumberGroupAddonComponent; + let fixture: ComponentFixture; + let inputNumberGroupElement: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputNumberGroupAddonComponent); + testComponent = fixture.debugElement.componentInstance; + fixture.detectChanges(); + inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)).nativeElement; + }); + it('should not show addon without before and after content', () => { + expect(inputNumberGroupElement.firstElementChild!.classList).not.toContain('ant-input-number-group'); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number'); + }); + it('should before content string work', () => { + testComponent.beforeContent = 'before'; + fixture.detectChanges(); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number-group'); + expect(inputNumberGroupElement.firstElementChild!.children.length).toBe(2); + expect(inputNumberGroupElement.firstElementChild!.lastElementChild!.classList).toContain('ant-input-number'); + expect((inputNumberGroupElement.firstElementChild!.firstElementChild as HTMLElement).innerText).toBe('before'); + }); + it('should before content template work', () => { + testComponent.beforeContent = testComponent.beforeTemplate; + fixture.detectChanges(); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number-group'); + expect(inputNumberGroupElement.firstElementChild!.children.length).toBe(2); + expect(inputNumberGroupElement.firstElementChild!.lastElementChild!.classList).toContain('ant-input-number'); + expect((inputNumberGroupElement.firstElementChild!.firstElementChild as HTMLElement).innerText).toBe( + 'beforeTemplate' + ); + }); + it('should after content string work', () => { + testComponent.afterContent = 'after'; + fixture.detectChanges(); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number-group'); + expect(inputNumberGroupElement.firstElementChild!.children.length).toBe(2); + expect(inputNumberGroupElement.firstElementChild!.firstElementChild!.classList).toContain('ant-input-number'); + expect((inputNumberGroupElement.firstElementChild!.lastElementChild as HTMLElement).innerText).toBe('after'); + }); + it('should after content template work', () => { + testComponent.afterContent = testComponent.afterTemplate; + fixture.detectChanges(); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number-group'); + expect(inputNumberGroupElement.firstElementChild!.children.length).toBe(2); + expect(inputNumberGroupElement.firstElementChild!.firstElementChild!.classList).toContain('ant-input-number'); + expect((inputNumberGroupElement.firstElementChild!.lastElementChild as HTMLElement).innerText).toBe( + 'afterTemplate' + ); + }); + it('should size work', () => { + testComponent.beforeContent = 'before'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-group-wrapper'); + testComponent.size = 'large'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-group-wrapper-lg'); + testComponent.size = 'small'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-group-wrapper-sm'); + }); + }); + describe('affix', () => { + let fixture: ComponentFixture; + let testComponent: NzTestInputNumberGroupAffixComponent; + let inputNumberGroupElement: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputNumberGroupAffixComponent); + testComponent = fixture.debugElement.componentInstance; + fixture.detectChanges(); + inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)).nativeElement; + }); + + it('should not show addon without before and after content', () => { + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number'); + }); + + it('should before content string work', () => { + testComponent.beforeContent = 'before'; + fixture.detectChanges(); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number-prefix'); + expect(inputNumberGroupElement.children.length).toBe(2); + expect(inputNumberGroupElement.lastElementChild!.classList).toContain('ant-input-number'); + expect((inputNumberGroupElement.firstElementChild as HTMLElement).innerText).toBe('before'); + }); + + it('should before content template work', () => { + testComponent.beforeContent = testComponent.beforeTemplate; + fixture.detectChanges(); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number-prefix'); + expect(inputNumberGroupElement.children.length).toBe(2); + expect(inputNumberGroupElement.lastElementChild!.classList).toContain('ant-input-number'); + expect((inputNumberGroupElement.firstElementChild as HTMLElement).innerText).toBe('beforeTemplate'); + }); + + it('should after content string work', () => { + testComponent.afterContent = 'after'; + fixture.detectChanges(); + expect(inputNumberGroupElement.lastElementChild!.classList).toContain('ant-input-number-suffix'); + expect(inputNumberGroupElement.children.length).toBe(2); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number'); + expect((inputNumberGroupElement.lastElementChild as HTMLElement).innerText).toBe('after'); + }); + + it('should after content template work', () => { + testComponent.afterContent = testComponent.afterTemplate; + fixture.detectChanges(); + expect(inputNumberGroupElement.lastElementChild!.classList).toContain('ant-input-number-suffix'); + expect(inputNumberGroupElement.children.length).toBe(2); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number'); + expect((inputNumberGroupElement.lastElementChild as HTMLElement).innerText).toBe('afterTemplate'); + }); + + it('should size work', () => { + testComponent.beforeContent = 'before'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-affix-wrapper'); + testComponent.size = 'large'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-affix-wrapper-lg'); + testComponent.size = 'small'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-affix-wrapper-sm'); + }); + it('should disabled work', () => { + testComponent.beforeContent = 'before'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).not.toContain('ant-input-number-affix-wrapper-disabled'); + testComponent.disabled = true; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-affix-wrapper-disabled'); + }); + it('should focus work', () => { + testComponent.beforeContent = 'before'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).not.toContain('ant-input-number-affix-wrapper-focused'); + dispatchFakeEvent(inputNumberGroupElement, 'focus'); + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-affix-wrapper-focused'); + }); + }); + describe('multiple', () => { + let fixture: ComponentFixture; + let testComponent: NzTestInputNumberGroupMultipleComponent; + let inputNumberGroupElement: HTMLElement; + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputNumberGroupMultipleComponent); + testComponent = fixture.debugElement.componentInstance; + fixture.detectChanges(); + inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)).nativeElement; + }); + it('should compact work', () => { + expect(inputNumberGroupElement.classList).not.toContain('ant-input-number-group-compact'); + testComponent.compact = true; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-group-compact'); + }); + it('should size work', () => { + expect(inputNumberGroupElement.firstElementChild!.classList).not.toContain('ant-input-number-lg'); + testComponent.size = 'large'; + fixture.detectChanges(); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number-lg'); + testComponent.size = 'small'; + fixture.detectChanges(); + expect(inputNumberGroupElement.firstElementChild!.classList).toContain('ant-input-number-sm'); + }); + }); + describe('col', () => { + let fixture: ComponentFixture; + let inputNumberGroupElement: HTMLElement; + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputNumberGroupColComponent); + fixture.detectChanges(); + inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)).nativeElement; + }); + it('should size work', () => { + expect(inputNumberGroupElement.querySelector('nz-input-number')!.classList).toContain('ant-input-number-lg'); + }); + }); + describe('mix', () => { + let fixture: ComponentFixture; + let inputGroupElement: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputNumberGroupMixComponent); + fixture.detectChanges(); + inputGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)).nativeElement; + }); + it('should mix work', () => { + expect( + inputGroupElement.querySelector('.ant-input-number-affix-wrapper')!.nextElementSibling!.classList + ).toContain('ant-input-number-group-addon'); + }); + }); + describe('status', () => { + let fixture: ComponentFixture; + let inputNumberGroupElement: DebugElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputNumberGroupWithStatusComponent); + fixture.detectChanges(); + inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)); + }); + + it('should className correct with prefix', () => { + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.classList).toContain( + 'ant-input-number-affix-wrapper-status-error' + ); + + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.className).toContain( + 'ant-input-number-affix-wrapper-status-warning' + ); + + fixture.componentInstance.status = ''; + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.className).not.toContain( + 'ant-input-number-affix-wrapper-status-warning' + ); + }); + + it('should className correct with addon', () => { + fixture.componentInstance.isAddon = true; + fixture.detectChanges(); + // re-query input element + inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)); + expect(inputNumberGroupElement.nativeElement.classList).toContain( + 'ant-input-number-group-wrapper-status-error' + ); + + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.className).toContain( + 'ant-input-number-group-wrapper-status-warning' + ); + + fixture.componentInstance.status = ''; + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.className).not.toContain( + 'ant-input-number-group-wrapper-status-warning' + ); + }); + }); + + describe('dir', () => { + let fixture: ComponentFixture; + let inputNumberGroupElement: HTMLElement; + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputNumberGroupWithDirComponent); + fixture.detectChanges(); + inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)).nativeElement; + }); + it('should dir work', () => { + expect(inputNumberGroupElement.classList).not.toContain('ant-input-number-group-wrapper-rtl'); + fixture.componentInstance.dir = 'rtl'; + fixture.detectChanges(); + expect(inputNumberGroupElement.classList).toContain('ant-input-number-group-wrapper-rtl'); + }); + }); + }); +}); + +@Component({ + template: ` + + + + beforeTemplate + afterTemplate + ` +}) +export class NzTestInputNumberGroupAddonComponent { + @ViewChild('beforeTemplate', { static: false }) beforeTemplate!: TemplateRef; + @ViewChild('afterTemplate', { static: false }) afterTemplate!: TemplateRef; + beforeContent?: string | TemplateRef; + afterContent?: string | TemplateRef; + size: NzSizeLDSType = 'default'; +} + +@Component({ + template: ` + + + + beforeTemplate + afterTemplate + ` +}) +export class NzTestInputNumberGroupAffixComponent { + @ViewChild('beforeTemplate', { static: false }) beforeTemplate!: TemplateRef; + @ViewChild('afterTemplate', { static: false }) afterTemplate!: TemplateRef; + beforeContent?: string | TemplateRef; + afterContent?: string | TemplateRef; + size: NzSizeLDSType = 'default'; + disabled = false; +} + +@Component({ + template: ` + + + + + ` +}) +export class NzTestInputNumberGroupMultipleComponent { + compact = false; + size: NzSizeLDSType = 'default'; +} + +@Component({ + template: ` + +
+ +
+
+ +
+
+ ` +}) +export class NzTestInputNumberGroupColComponent {} + +@Component({ + template: ` + + + + ` +}) +export class NzTestInputNumberGroupMixComponent {} + +@Component({ + template: ` + + + + + + + + + + + + ` +}) +export class NzTestInputNumberGroupWithStatusComponent { + isAddon = false; + status: NzStatus = 'error'; +} + +@Component({ + template: ` +
+ + + +
+ ` +}) +export class NzTestInputNumberGroupWithDirComponent { + dir: Direction = 'ltr'; +} diff --git a/components/input-number/input-number.component.ts b/components/input-number/input-number.component.ts index a761004af7..b27be56412 100644 --- a/components/input-number/input-number.component.ts +++ b/components/input-number/input-number.component.ts @@ -27,7 +27,7 @@ import { ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { fromEvent, merge } from 'rxjs'; +import { fromEvent, merge, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { NzDestroyService } from 'ng-zorro-antd/core/services'; @@ -36,6 +36,7 @@ import { NgClassInterface, NzSizeLDSType, NzStatus, + NzValidateStatus, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; @@ -114,13 +115,15 @@ export class NzInputNumberComponent implements ControlValueAccessor, AfterViewIn private value?: number; 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 = {}; - nzHasFeedback: boolean = false; + hasFeedback: boolean = false; onChange: OnChangeType = () => {}; onTouched: OnTouchedType = () => {}; @Output() readonly nzBlur = new EventEmitter(); @@ -142,7 +145,7 @@ export class NzInputNumberComponent implements ControlValueAccessor, AfterViewIn @Input() nzPrecision?: number; @Input() nzPrecisionMode: 'cut' | 'toFixed' | ((value: number | string, precision?: number) => number) = 'toFixed'; @Input() nzPlaceHolder = ''; - @Input() nzStatus?: NzStatus; + @Input() nzStatus: NzStatus = ''; @Input() nzStep = 1; @Input() nzInputMode: string = 'decimal'; @Input() nzId: string | null = null; @@ -373,6 +376,7 @@ export class NzInputNumberComponent implements ControlValueAccessor, AfterViewIn setDisabledState(disabled: boolean): void { this.nzDisabled = disabled; + this.disabled$.next(disabled); this.cdr.markForCheck(); } @@ -448,14 +452,17 @@ export class NzInputNumberComponent implements ControlValueAccessor, AfterViewIn } ngOnChanges(changes: SimpleChanges): void { - const { nzStatus } = changes; + const { nzStatus, nzDisabled } = changes; if (changes.nzFormatter && !changes.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.setStatusStyles(this.nzStatus, this.hasFeedback); } } @@ -482,9 +489,14 @@ export class NzInputNumberComponent implements ControlValueAccessor, AfterViewIn }); } - private setStatusStyles(): void { + 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, this.nzStatus, this.nzHasFeedback); + this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback); + console.dir(this.statusCls, status); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.elementRef.nativeElement, status); diff --git a/components/input-number/input-number.module.ts b/components/input-number/input-number.module.ts index 9c84f48775..17c054a4a5 100644 --- a/components/input-number/input-number.module.ts +++ b/components/input-number/input-number.module.ts @@ -8,13 +8,24 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzInputNumberGroupSlotComponent } from './input-number-group-slot.component'; +import { + NzInputNumberGroupComponent, + NzInputNumberGroupWhitSuffixOrPrefixDirective +} from './input-number-group.component'; import { NzInputNumberComponent } from './input-number.component'; @NgModule({ - imports: [BidiModule, CommonModule, FormsModule, NzIconModule], - declarations: [NzInputNumberComponent], - exports: [NzInputNumberComponent] + imports: [BidiModule, CommonModule, FormsModule, NzOutletModule, NzIconModule], + declarations: [ + NzInputNumberComponent, + NzInputNumberGroupWhitSuffixOrPrefixDirective, + NzInputNumberGroupComponent, + NzInputNumberGroupSlotComponent + ], + exports: [NzInputNumberComponent, NzInputNumberGroupWhitSuffixOrPrefixDirective, NzInputNumberGroupComponent] }) export class NzInputNumberModule {} diff --git a/components/input-number/public-api.ts b/components/input-number/public-api.ts index ef0ae535ce..9434308324 100644 --- a/components/input-number/public-api.ts +++ b/components/input-number/public-api.ts @@ -4,4 +4,6 @@ */ 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/entry.less b/components/input-number/style/entry.less index 06547c43ac..96cebe33bf 100644 --- a/components/input-number/style/entry.less +++ b/components/input-number/style/entry.less @@ -1 +1,2 @@ @import './index.less'; +@import './patch.less'; diff --git a/components/input-number/style/patch.less b/components/input-number/style/patch.less new file mode 100644 index 0000000000..cbe972a814 --- /dev/null +++ b/components/input-number/style/patch.less @@ -0,0 +1,13 @@ +.@{ant-prefix}-input-number { + &-affix-wrapper { + > nz-input-number.@{ant-prefix}-input-number { + width: 100%; + border: none; + outline: none; + + &.@{ant-prefix}-input-number-focused { + box-shadow: none !important; + } + } + } +}