From 98ac620a1eac4e307505450fbf7890f5b3da20ff Mon Sep 17 00:00:00 2001 From: simplejason Date: Wed, 15 Jun 2022 14:56:37 +0800 Subject: [PATCH] feat(module:form): make form work with status (#7489) * feat(module:form): emit status changes to notify components to change * feat(module:form): make date-picker work in form * feat(module:form): make input work in form * chore(module:input-number): make input-number-group work in form * fix(module:checkbox): make checkbox work in form * fix(module:radio): make radio work in form * fix(module:select): make select work in form * fix(module:time-picker): make time picker work in form * fix(module:transfer): make transfer work in form * fix(module:tree-select): make tree select work in form * fix(module:mention): make mention work in form * fix(module:input): make input work in form * fix(module:input): not render status under addonbefore or addonafter * fix(module:form): add tests * fix(module:input): move feedback component to entrypoint * chore: fix some demos * fix(module:form): move feedback to form-patch module --- components/cascader/cascader.component.ts | 42 ++++++++--- components/cascader/cascader.module.ts | 4 +- components/cascader/cascader.spec.ts | 67 ++++++++++++++++- components/checkbox/checkbox.component.ts | 5 +- components/core/form/index.ts | 6 ++ components/core/form/ng-package.json | 5 ++ .../nz-form-item-feedback-icon.component.ts | 54 ++++++++++++++ .../form/nz-form-item-feedback-icon.spec.ts | 54 ++++++++++++++ .../core/form/nz-form-no-status.service.ts | 13 ++++ components/core/form/nz-form-patch.module.ts | 18 +++++ .../core/form/nz-form-status.service.ts | 14 ++++ components/core/form/public-api.ts | 9 +++ .../date-picker/date-picker.component.spec.ts | 66 ++++++++++++++++- .../date-picker/date-picker.component.ts | 37 ++++++++-- components/date-picker/date-picker.module.ts | 2 + .../month-picker.component.spec.ts | 15 +++- .../range-picker.component.spec.ts | 21 +++++- .../date-picker/week-picker.component.spec.ts | 23 +++++- .../date-picker/year-picker.component.spec.ts | 15 +++- components/form/form-control.component.ts | 25 +++---- components/form/form-control.spec.ts | 8 -- components/form/form-item.component.ts | 4 +- components/grid/col.directive.ts | 2 +- components/grid/demo/flex.md | 4 +- components/grid/demo/flex.ts | 7 ++ components/grid/row.directive.ts | 3 +- .../input-number-group-slot.component.ts | 1 - .../input-number-group.component.ts | 40 ++++++++-- .../input-number/input-number-group.spec.ts | 74 ++++++++++++++++++- .../input-number/input-number.component.ts | 49 ++++++++---- .../input-number/input-number.module.ts | 3 +- components/input-number/input-number.spec.ts | 63 +++++++++++++++- components/input-number/style/patch.less | 6 ++ components/input/input-group.component.ts | 61 ++++++++++++--- components/input/input-group.spec.ts | 62 +++++++++++++++- components/input/input.directive.ts | 53 +++++++++++-- components/input/input.module.ts | 4 +- components/input/input.spec.ts | 59 ++++++++++++++- components/input/style/patch.less | 23 ++++++ components/mention/mention.component.ts | 44 ++++++++--- components/mention/mention.module.ts | 3 +- components/mention/nz-mention.spec.ts | 54 +++++++++++++- components/page-header/demo/actions.ts | 16 ++-- components/page-header/demo/content.ts | 22 ++++-- components/page-header/demo/ghost.ts | 8 +- components/page-header/demo/module | 4 +- components/page-header/demo/responsive.ts | 8 +- components/radio/radio.component.ts | 5 +- components/select/select-arrow.component.ts | 7 +- components/select/select.component.ts | 47 +++++++++--- components/select/select.module.ts | 2 + components/select/select.spec.ts | 51 +++++++++++++ .../time-picker/time-picker.component.spec.ts | 55 +++++++++++++- .../time-picker/time-picker.component.ts | 36 +++++++-- components/time-picker/time-picker.module.ts | 4 +- components/transfer/transfer.component.ts | 42 +++++++++-- components/transfer/transfer.spec.ts | 53 ++++++++++++- .../tree-select/tree-select.component.ts | 47 ++++++++++-- components/tree-select/tree-select.module.ts | 4 +- components/tree-select/tree-select.spec.ts | 49 +++++++++++- 60 files changed, 1400 insertions(+), 182 deletions(-) create mode 100644 components/core/form/index.ts create mode 100644 components/core/form/ng-package.json create mode 100644 components/core/form/nz-form-item-feedback-icon.component.ts create mode 100644 components/core/form/nz-form-item-feedback-icon.spec.ts create mode 100644 components/core/form/nz-form-no-status.service.ts create mode 100644 components/core/form/nz-form-patch.module.ts create mode 100644 components/core/form/nz-form-status.service.ts create mode 100644 components/core/form/public-api.ts diff --git a/components/cascader/cascader.component.ts b/components/cascader/cascader.component.ts index 9fa16d4bc9c..2f29e3be720 100644 --- a/components/cascader/cascader.component.ts +++ b/components/cascader/cascader.component.ts @@ -31,11 +31,12 @@ import { ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { BehaviorSubject, EMPTY, fromEvent, Observable } from 'rxjs'; -import { startWith, switchMap, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject, EMPTY, fromEvent, Observable, of as observableOf } from 'rxjs'; +import { distinctUntilChanged, map, startWith, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators'; import { slideMotion } from 'ng-zorro-antd/core/animation'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation'; import { DEFAULT_CASCADER_POSITIONS } from 'ng-zorro-antd/core/overlay'; import { NzDestroyService } from 'ng-zorro-antd/core/services'; @@ -45,7 +46,8 @@ import { NgClassType, NgStyleInterface, NzSafeAny, - NzStatus + NzStatus, + NzValidateStatus } from 'ng-zorro-antd/core/types'; import { getStatusClassNames, InputBoolean, toArray } from 'ng-zorro-antd/core/util'; import { NzCascaderI18nInterface, NzI18nService } from 'ng-zorro-antd/i18n'; @@ -115,6 +117,7 @@ const defaultDisplayRender = (labels: string[]): string => labels.join(' / '); [class.ant-cascader-picker-arrow-expand]="menuVisible" > + @@ -207,6 +210,7 @@ const defaultDisplayRender = (labels: string[]): string => labels.join(' / '); ], host: { '[attr.tabIndex]': '"0"', + '[class.ant-select-in-form-item]': '!!nzFormStatusService', '[class.ant-select-lg]': 'nzSize === "large"', '[class.ant-select-sm]': 'nzSize === "small"', '[class.ant-select-allow-clear]': 'nzAllowClear', @@ -267,7 +271,7 @@ export class NzCascaderComponent @Input() nzMenuStyle: NgStyleInterface | null = null; @Input() nzMouseEnterDelay: number = 150; // ms @Input() nzMouseLeaveDelay: number = 150; // ms - @Input() nzStatus?: NzStatus; + @Input() nzStatus: NzStatus = ''; @Input() nzTriggerAction: NzCascaderTriggerType | NzCascaderTriggerType[] = ['click'] as NzCascaderTriggerType[]; @Input() nzChangeOn?: (option: NzCascaderOption, level: number) => boolean; @@ -292,7 +296,8 @@ export class NzCascaderComponent prefixCls: string = 'ant-select'; statusCls: NgClassInterface = {}; - nzHasFeedback: boolean = false; + status: NzValidateStatus = ''; + hasFeedback: boolean = false; /** * If the dropdown should show the empty content. @@ -379,7 +384,9 @@ export class NzCascaderComponent private elementRef: ElementRef, private renderer: Renderer2, @Optional() private directionality: Directionality, - @Host() @Optional() public noAnimation?: NzNoAnimationDirective + @Host() @Optional() public noAnimation?: NzNoAnimationDirective, + @Optional() public nzFormStatusService?: NzFormStatusService, + @Optional() private nzFormNoStatusService?: NzFormNoStatusService ) { this.el = elementRef.nativeElement; this.cascaderService.withComponent(this); @@ -388,6 +395,19 @@ export class NzCascaderComponent } ngOnInit(): void { + this.nzFormStatusService?.formStatusChanges + .pipe( + distinctUntilChanged((pre, cur) => { + return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; + }), + withLatestFrom(this.nzFormNoStatusService ? this.nzFormNoStatusService.noFormStatus : observableOf(false)), + map(([{ status, hasFeedback }, noStatus]) => ({ status: noStatus ? '' : status, hasFeedback })), + takeUntil(this.destroy$) + ) + .subscribe(({ status, hasFeedback }) => { + this.setStatusStyles(status, hasFeedback); + }); + const srv = this.cascaderService; srv.$redraw.pipe(takeUntil(this.destroy$)).subscribe(() => { @@ -450,7 +470,7 @@ export class NzCascaderComponent ngOnChanges(changes: SimpleChanges): void { const { nzStatus } = changes; if (nzStatus) { - this.setStatusStyles(); + this.setStatusStyles(this.nzStatus, this.hasFeedback); } } @@ -785,9 +805,13 @@ export class NzCascaderComponent } } - 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); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.elementRef.nativeElement, status); diff --git a/components/cascader/cascader.module.ts b/components/cascader/cascader.module.ts index f56d17ff8ac..be2dd4bd287 100644 --- a/components/cascader/cascader.module.ts +++ b/components/cascader/cascader.module.ts @@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NzFormPatchModule } from 'ng-zorro-antd/core/form'; import { NzHighlightModule } from 'ng-zorro-antd/core/highlight'; import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation'; import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; @@ -32,7 +33,8 @@ import { NzCascaderComponent } from './cascader.component'; NzIconModule, NzInputModule, NzNoAnimationModule, - NzOverlayModule + NzOverlayModule, + NzFormPatchModule ], declarations: [NzCascaderComponent, NzCascaderOptionComponent], exports: [NzCascaderComponent] diff --git a/components/cascader/cascader.spec.ts b/components/cascader/cascader.spec.ts index 3cd8b735fbf..c5829681be6 100644 --- a/components/cascader/cascader.spec.ts +++ b/components/cascader/cascader.spec.ts @@ -21,7 +21,7 @@ import { import { OverlayContainer } from '@angular/cdk/overlay'; import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core'; import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -34,6 +34,7 @@ import { import { NzStatus } from 'ng-zorro-antd/core/types'; import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; +import { NzFormModule } from '../form'; import { NzCascaderComponent } from './cascader.component'; import { NzCascaderModule } from './cascader.module'; import { NzCascaderOption, NzShowSearchOptions } from './typings'; @@ -67,13 +68,15 @@ describe('cascader', () => { ReactiveFormsModule, NoopAnimationsModule, NzCascaderModule, - NzIconTestModule + NzIconTestModule, + NzFormModule ], declarations: [ NzDemoCascaderDefaultComponent, NzDemoCascaderLoadDataComponent, NzDemoCascaderRtlComponent, - NzDemoCascaderStatusComponent + NzDemoCascaderStatusComponent, + NzDemoCascaderInFormComponent ] }).compileComponents(); @@ -1815,6 +1818,45 @@ describe('cascader', () => { expect(cascader.nativeElement.className).not.toContain('ant-select-status-warning'); }); }); + describe('In form', () => { + let fixture: ComponentFixture; + let formGroup: FormGroup; + let cascader: DebugElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzDemoCascaderInFormComponent); + cascader = fixture.debugElement.query(By.directive(NzCascaderComponent)); + formGroup = fixture.componentInstance.validateForm; + fixture.detectChanges(); + }); + + it('should className correct', () => { + expect(cascader.nativeElement.className).not.toContain('ant-select-status-error'); + expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeNull(); + formGroup.get('demo')!.markAsDirty(); + formGroup.get('demo')!.setValue(null); + formGroup.get('demo')!.updateValueAndValidity(); + fixture.detectChanges(); + + // show error + expect(cascader.nativeElement.className).toContain('ant-select-status-error'); + expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon').className).toContain( + 'ant-form-item-feedback-icon-error' + ); + + formGroup.get('demo')!.markAsDirty(); + formGroup.get('demo')!.setValue(['a', 'b']); + formGroup.get('demo')!.updateValueAndValidity(); + fixture.detectChanges(); + // show success + expect(cascader.nativeElement.className).toContain('ant-select-status-success'); + expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon').className).toContain( + 'ant-form-item-feedback-icon-success' + ); + }); + }); }); const ID_NAME_LIST = [ @@ -2208,3 +2250,22 @@ export class NzDemoCascaderStatusComponent { public nzOptions: any[] | null = options1; public status: NzStatus = 'error'; } + +@Component({ + template: ` +
+ + + + + +
+ ` +}) +export class NzDemoCascaderInFormComponent { + validateForm: FormGroup = this.fb.group({ + demo: [null, [Validators.required]] + }); + public nzOptions: any[] | null = options1; + constructor(private fb: FormBuilder) {} +} diff --git a/components/checkbox/checkbox.component.ts b/components/checkbox/checkbox.component.ts index 4d9912000af..db56c8fb165 100644 --- a/components/checkbox/checkbox.component.ts +++ b/components/checkbox/checkbox.component.ts @@ -26,6 +26,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { fromEvent, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { NzFormStatusService } from 'ng-zorro-antd/core/form'; import { BooleanInput, NzSafeAny, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; import { InputBoolean } from 'ng-zorro-antd/core/util'; @@ -68,6 +69,7 @@ import { NzCheckboxWrapperComponent } from './checkbox-wrapper.component'; ], host: { class: 'ant-checkbox-wrapper', + '[class.ant-checkbox-wrapper-in-form-item]': '!!nzFormStatusService', '[class.ant-checkbox-wrapper-checked]': 'nzChecked', '[class.ant-checkbox-rtl]': `dir === 'rtl'` } @@ -135,7 +137,8 @@ export class NzCheckboxComponent implements OnInit, ControlValueAccessor, OnDest @Optional() private nzCheckboxWrapperComponent: NzCheckboxWrapperComponent, private cdr: ChangeDetectorRef, private focusMonitor: FocusMonitor, - @Optional() private directionality: Directionality + @Optional() private directionality: Directionality, + @Optional() public nzFormStatusService?: NzFormStatusService ) {} ngOnInit(): void { diff --git a/components/core/form/index.ts b/components/core/form/index.ts new file mode 100644 index 00000000000..97717c1c837 --- /dev/null +++ b/components/core/form/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/core/form/ng-package.json b/components/core/form/ng-package.json new file mode 100644 index 00000000000..789c95e4962 --- /dev/null +++ b/components/core/form/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/components/core/form/nz-form-item-feedback-icon.component.ts b/components/core/form/nz-form-item-feedback-icon.component.ts new file mode 100644 index 00000000000..2c31325c719 --- /dev/null +++ b/components/core/form/nz-form-item-feedback-icon.component.ts @@ -0,0 +1,54 @@ +/** + * 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, + ChangeDetectorRef, + Component, + Input, + OnChanges, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; + +import { NzValidateStatus } from 'ng-zorro-antd/core/types'; + +const iconTypeMap = { + error: 'close-circle-fill', + validating: 'loading', + success: 'check-circle-fill', + warning: 'exclamation-circle-fill' +} as const; + +@Component({ + selector: 'nz-form-item-feedback-icon', + exportAs: 'nzFormFeedbackIcon', + preserveWhitespaces: false, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` `, + host: { + class: 'ant-form-item-feedback-icon', + '[class.ant-form-item-feedback-icon-error]': 'status==="error"', + '[class.ant-form-item-feedback-icon-warning]': 'status==="warning"', + '[class.ant-form-item-feedback-icon-success]': 'status==="success"', + '[class.ant-form-item-feedback-icon-validating]': 'status==="validating"' + } +}) +export class NzFormItemFeedbackIconComponent implements OnChanges { + @Input() status: NzValidateStatus = ''; + constructor(public cdr: ChangeDetectorRef) {} + + iconType: typeof iconTypeMap[keyof typeof iconTypeMap] | null = null; + + ngOnChanges(_changes: SimpleChanges): void { + this.updateIcon(); + } + + updateIcon(): void { + this.iconType = this.status ? iconTypeMap[this.status] : null; + this.cdr.markForCheck(); + } +} diff --git a/components/core/form/nz-form-item-feedback-icon.spec.ts b/components/core/form/nz-form-item-feedback-icon.spec.ts new file mode 100644 index 00000000000..0d1d1a62575 --- /dev/null +++ b/components/core/form/nz-form-item-feedback-icon.spec.ts @@ -0,0 +1,54 @@ +import { Component, DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { NzFormPatchModule } from 'ng-zorro-antd/core/form/nz-form-patch.module'; +import { ɵComponentBed as ComponentBed, ɵcreateComponentBed as createComponentBed } from 'ng-zorro-antd/core/testing'; +import { NzValidateStatus } from 'ng-zorro-antd/core/types'; + +import { NzFormItemFeedbackIconComponent } from './nz-form-item-feedback-icon.component'; + +const testBedOptions = { imports: [NzFormPatchModule, NoopAnimationsModule] }; + +describe('nz-form-item-feedback-icon', () => { + describe('default', () => { + let testBed: ComponentBed; + let fixtureInstance: NzTestFormItemFeedbackIconComponent; + let feedback: DebugElement; + beforeEach(() => { + testBed = createComponentBed(NzTestFormItemFeedbackIconComponent, testBedOptions); + fixtureInstance = testBed.fixture.componentInstance; + feedback = testBed.fixture.debugElement.query(By.directive(NzFormItemFeedbackIconComponent)); + testBed.fixture.detectChanges(); + }); + it('should className correct', () => { + expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon'); + fixtureInstance.status = 'success'; + testBed.fixture.detectChanges(); + expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon-success'); + expect(feedback.nativeElement.querySelector('.anticon-check-circle-fill')).toBeTruthy(); + + fixtureInstance.status = 'error'; + testBed.fixture.detectChanges(); + expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon-error'); + expect(feedback.nativeElement.querySelector('.anticon-close-circle-fill')).toBeTruthy(); + + fixtureInstance.status = 'warning'; + testBed.fixture.detectChanges(); + expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon-warning'); + expect(feedback.nativeElement.querySelector('.anticon-exclamation-circle-fill')).toBeTruthy(); + + fixtureInstance.status = 'validating'; + testBed.fixture.detectChanges(); + expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon-validating'); + expect(feedback.nativeElement.querySelector('.anticon-loading')).toBeTruthy(); + }); + }); +}); + +@Component({ + template: ` ` +}) +export class NzTestFormItemFeedbackIconComponent { + status: NzValidateStatus = ''; +} diff --git a/components/core/form/nz-form-no-status.service.ts b/components/core/form/nz-form-no-status.service.ts new file mode 100644 index 00000000000..299fea83cf0 --- /dev/null +++ b/components/core/form/nz-form-no-status.service.ts @@ -0,0 +1,13 @@ +/** + * 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 { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +// Used in input-group/input-number-group to make sure components in addon work well +@Injectable() +export class NzFormNoStatusService { + noFormStatus = new BehaviorSubject(false); +} diff --git a/components/core/form/nz-form-patch.module.ts b/components/core/form/nz-form-patch.module.ts new file mode 100644 index 00000000000..2c4388cd0ab --- /dev/null +++ b/components/core/form/nz-form-patch.module.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 { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { NzIconModule } from 'ng-zorro-antd/icon'; + +import { NzFormItemFeedbackIconComponent } from './nz-form-item-feedback-icon.component'; + +@NgModule({ + imports: [CommonModule, NzIconModule], + exports: [NzFormItemFeedbackIconComponent], + declarations: [NzFormItemFeedbackIconComponent] +}) +export class NzFormPatchModule {} diff --git a/components/core/form/nz-form-status.service.ts b/components/core/form/nz-form-status.service.ts new file mode 100644 index 00000000000..7580bc2dafa --- /dev/null +++ b/components/core/form/nz-form-status.service.ts @@ -0,0 +1,14 @@ +/** + * 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 { Injectable } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; + +import { NzValidateStatus } from 'ng-zorro-antd/core/types'; + +@Injectable() +export class NzFormStatusService { + formStatusChanges = new ReplaySubject<{ status: NzValidateStatus; hasFeedback: boolean }>(1); +} diff --git a/components/core/form/public-api.ts b/components/core/form/public-api.ts new file mode 100644 index 00000000000..c5e46065c7d --- /dev/null +++ b/components/core/form/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 './nz-form-status.service'; +export * from './nz-form-no-status.service'; +export * from './nz-form-item-feedback-icon.component'; +export * from './nz-form-patch.module'; diff --git a/components/date-picker/date-picker.component.spec.ts b/components/date-picker/date-picker.component.spec.ts index 516d56b75b3..9c21d79160b 100644 --- a/components/date-picker/date-picker.component.spec.ts +++ b/components/date-picker/date-picker.component.spec.ts @@ -5,7 +5,7 @@ import { registerLocaleData } from '@angular/common'; import zh from '@angular/common/locales/zh'; import { ApplicationRef, Component, DebugElement, TemplateRef, ViewChild } from '@angular/core'; import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -22,8 +22,9 @@ import { import { ComponentBed, createComponentBed } from 'ng-zorro-antd/core/testing/component-bed'; import { NgStyleInterface, NzStatus } from 'ng-zorro-antd/core/types'; import { NzI18nModule, NzI18nService, NZ_DATE_LOCALE } from 'ng-zorro-antd/i18n'; -import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; +import { NzFormModule } from '../form'; import en_US from '../i18n/languages/en_US'; import { NzDatePickerComponent } from './date-picker.component'; import { NzDatePickerModule } from './date-picker.module'; @@ -1193,7 +1194,7 @@ describe('status', () => { let fixtureInstance: NzTestDatePickerStatusComponent; let datePickerElement!: HTMLElement; beforeEach(() => { - testBed = createComponentBed(NzTestDatePickerStatusComponent, { imports: [NzDatePickerModule, NzIconModule] }); + testBed = createComponentBed(NzTestDatePickerStatusComponent, { imports: [NzDatePickerModule, NzIconTestModule] }); fixture = testBed.fixture; fixtureInstance = fixture.componentInstance; datePickerElement = fixture.debugElement.query(By.directive(NzDatePickerComponent)).nativeElement; @@ -1212,6 +1213,47 @@ describe('status', () => { }); }); +describe('in form', () => { + let testBed: ComponentBed; + let fixture: ComponentFixture; + let datePickerElement!: HTMLElement; + let formGroup: FormGroup; + beforeEach(() => { + testBed = createComponentBed(NzTestDatePickerInFormComponent, { + imports: [NzDatePickerModule, NzIconTestModule, NzFormModule, ReactiveFormsModule, FormsModule] + }); + fixture = testBed.fixture; + datePickerElement = fixture.debugElement.query(By.directive(NzDatePickerComponent)).nativeElement; + formGroup = fixture.componentInstance.validateForm; + fixture.detectChanges(); + }); + it('should classname correct', () => { + expect(datePickerElement.classList).not.toContain('ant-picker-status-error'); + expect(datePickerElement.querySelector('nz-form-item-feedback-icon')).toBeNull(); + + formGroup.get('demo')!.markAsDirty(); + formGroup.get('demo')!.setValue(null); + formGroup.get('demo')!.updateValueAndValidity(); + fixture.detectChanges(); + expect(datePickerElement.classList).toContain('ant-picker-status-error'); + expect(datePickerElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + expect(datePickerElement!.querySelector('nz-form-item-feedback-icon')!.className).toContain( + 'ant-form-item-feedback-icon-error' + ); + + formGroup.get('demo')!.markAsDirty(); + formGroup.get('demo')!.setValue(new Date()); + formGroup.get('demo')!.updateValueAndValidity(); + fixture.detectChanges(); + // show success + expect(datePickerElement.classList).toContain('ant-picker-status-success'); + expect(datePickerElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + expect(datePickerElement.querySelector('nz-form-item-feedback-icon')!.className).toContain( + 'ant-form-item-feedback-icon-success' + ); + }); +}); + @Component({ template: ` @@ -1332,3 +1374,21 @@ class NzTestDatePickerComponent { class NzTestDatePickerStatusComponent { status: NzStatus = 'error'; } + +@Component({ + template: ` +
+ + + + + +
+ ` +}) +class NzTestDatePickerInFormComponent { + validateForm: FormGroup = this.fb.group({ + demo: [null, [Validators.required]] + }); + constructor(private fb: FormBuilder) {} +} diff --git a/components/date-picker/date-picker.component.ts b/components/date-picker/date-picker.component.ts index 0ce822c4519..3e4ce081b12 100644 --- a/components/date-picker/date-picker.component.ts +++ b/components/date-picker/date-picker.component.ts @@ -40,12 +40,13 @@ import { ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { of as observableOf, Subject } from 'rxjs'; +import { distinctUntilChanged, map, takeUntil, withLatestFrom } from 'rxjs/operators'; import { NzResizeObserver } from 'ng-zorro-antd/cdk/resize-observer'; import { slideMotion } from 'ng-zorro-antd/core/animation'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation'; import { CandyDate, cloneDate, CompatibleValue, wrongSortOrder } from 'ng-zorro-antd/core/time'; import { @@ -54,6 +55,7 @@ import { NgClassInterface, NzSafeAny, NzStatus, + NzValidateStatus, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; @@ -160,6 +162,7 @@ export type NzDatePickerSizeType = 'large' | 'default' | 'small'; +
@@ -263,6 +266,7 @@ export class NzDatePickerComponent implements OnInit, OnChanges, OnDestroy, Afte // status statusCls: NgClassInterface = {}; + status: NzValidateStatus = ''; hasFeedback: boolean = false; public panelMode: NzDateMode | NzDateMode[] = 'date'; @@ -285,7 +289,7 @@ export class NzDatePickerComponent implements OnInit, OnChanges, OnDestroy, Afte @Input() nzPopupStyle: object = POPUP_STYLE_PATCH; @Input() nzDropdownClassName?: string; @Input() nzSize: NzDatePickerSizeType = 'default'; - @Input() nzStatus?: NzStatus; + @Input() nzStatus: NzStatus = ''; @Input() nzFormat!: string; @Input() nzDateRender?: TemplateRef | string | FunctionProp | string>; @Input() nzDisabledTime?: DisabledTimeFn; @@ -610,13 +614,28 @@ export class NzDatePickerComponent implements OnInit, OnChanges, OnDestroy, Afte private platform: Platform, @Inject(DOCUMENT) doc: NzSafeAny, @Optional() private directionality: Directionality, - @Host() @Optional() public noAnimation?: NzNoAnimationDirective + @Host() @Optional() public noAnimation?: NzNoAnimationDirective, + @Optional() private nzFormStatusService?: NzFormStatusService, + @Optional() private nzFormNoStatusService?: NzFormNoStatusService ) { this.document = doc; this.origin = new CdkOverlayOrigin(this.elementRef); } ngOnInit(): void { + this.nzFormStatusService?.formStatusChanges + .pipe( + distinctUntilChanged((pre, cur) => { + return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; + }), + withLatestFrom(this.nzFormNoStatusService ? this.nzFormNoStatusService.noFormStatus : observableOf(false)), + map(([{ status, hasFeedback }, noStatus]) => ({ status: noStatus ? '' : status, hasFeedback })), + takeUntil(this.destroyed$) + ) + .subscribe(({ status, hasFeedback }) => { + this.setStatusStyles(status, hasFeedback); + }); + // Subscribe the every locale change if the nzLocale is not handled by user if (!this.nzLocale) { this.i18n.localeChange.pipe(takeUntil(this.destroyed$)).subscribe(() => this.setLocale()); @@ -691,7 +710,7 @@ export class NzDatePickerComponent implements OnInit, OnChanges, OnDestroy, Afte } if (nzStatus) { - this.setStatusStyles(); + this.setStatusStyles(this.nzStatus, this.hasFeedback); } } @@ -843,9 +862,13 @@ export class NzDatePickerComponent implements OnInit, OnChanges, OnDestroy, Afte } // status - 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.hasFeedback); + this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.elementRef.nativeElement, status); diff --git a/components/date-picker/date-picker.module.ts b/components/date-picker/date-picker.module.ts index d4d33b9ebb4..0f55a51f3fd 100644 --- a/components/date-picker/date-picker.module.ts +++ b/components/date-picker/date-picker.module.ts @@ -10,6 +10,7 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzFormPatchModule } from 'ng-zorro-antd/core/form'; import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation'; import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; import { NzOverlayModule } from 'ng-zorro-antd/core/overlay'; @@ -36,6 +37,7 @@ import { NzYearPickerComponent } from './year-picker.component'; NzIconModule, NzOverlayModule, NzNoAnimationModule, + NzFormPatchModule, NzOutletModule, NzTimePickerModule, NzButtonModule, diff --git a/components/date-picker/month-picker.component.spec.ts b/components/date-picker/month-picker.component.spec.ts index dd46e8bb512..f61c503c2d2 100644 --- a/components/date-picker/month-picker.component.spec.ts +++ b/components/date-picker/month-picker.component.spec.ts @@ -148,6 +148,17 @@ describe('NzMonthPickerComponent', () => { expect(compStyles.getPropertyValue('border-bottom-right-radius') === '0px').toBeTruthy(); }); + it('should nz-month-picker work', fakeAsync(() => { + fixtureInstance.useSuite = 5; + fixture.whenRenderingDone().then(() => { + tick(500); + fixture.detectChanges(); + expect(getPickerContainer()).not.toBeNull(); + const pickerInput = getPickerInput(fixture.debugElement); + expect(pickerInput).not.toBeNull(); + }); + })); + it('should support nzDisabledDate', fakeAsync(() => { fixture.detectChanges(); const compareDate = new Date('2018-11-15 00:00:00'); @@ -435,11 +446,13 @@ describe('NzMonthPickerComponent', () => { + + ` }) class NzTestMonthPickerComponent { - useSuite!: 1 | 2 | 3 | 4; + useSuite!: 1 | 2 | 3 | 4 | 5; @ViewChild('tplExtraFooter', { static: true }) tplExtraFooter!: TemplateRef; // --- Suite 1 nzAllowClear: boolean = false; diff --git a/components/date-picker/range-picker.component.spec.ts b/components/date-picker/range-picker.component.spec.ts index 7d5f42d410c..dc3c573b030 100644 --- a/components/date-picker/range-picker.component.spec.ts +++ b/components/date-picker/range-picker.component.spec.ts @@ -80,6 +80,17 @@ describe('NzRangePickerComponent', () => { expect(getPickerContainer()).toBeNull(); })); + it('should nz-range-picker work', fakeAsync(() => { + fixtureInstance.useSuite = 5; + fixture.whenRenderingDone().then(() => { + tick(500); + fixture.detectChanges(); + expect(getPickerContainer()).not.toBeNull(); + const pickerInput = getPickerInput(fixture.debugElement); + expect(pickerInput).not.toBeNull(); + }); + })); + it('should open by click and close by tab', fakeAsync(() => { fixtureInstance.useSuite = 4; @@ -832,6 +843,9 @@ describe('NzRangePickerComponent', () => { const newDateString = ['2019-09-15 11:08:22', '2020-10-10 11:08:22']; typeInElement(newDateString[0], leftInput); fixture.detectChanges(); + // should focus the other input + leftInput.dispatchEvent(ENTER_EVENT); + fixture.detectChanges(); typeInElement(newDateString[1], rightInput); fixture.detectChanges(); rightInput.dispatchEvent(ENTER_EVENT); @@ -871,6 +885,9 @@ describe('NzRangePickerComponent', () => { const rightInput = getRangePickerRightInput(fixture.debugElement); typeInElement('2019-08-10', leftInput); fixture.detectChanges(); + // should focus the other input + leftInput.dispatchEvent(ENTER_EVENT); + fixture.detectChanges(); typeInElement('2018-02-06', rightInput); fixture.detectChanges(); getRangePickerRightInput(fixture.debugElement).dispatchEvent(ENTER_EVENT); @@ -1149,11 +1166,13 @@ describe('NzRangePickerComponent', () => { + + ` }) class NzTestRangePickerComponent { - useSuite!: 1 | 2 | 3 | 4; + useSuite!: 1 | 2 | 3 | 4 | 5; @ViewChild('tplDateRender', { static: true }) tplDateRender!: TemplateRef; @ViewChild('tplExtraFooter', { static: true }) tplExtraFooter!: TemplateRef; diff --git a/components/date-picker/week-picker.component.spec.ts b/components/date-picker/week-picker.component.spec.ts index dc2c3648d03..7ace20b69be 100644 --- a/components/date-picker/week-picker.component.spec.ts +++ b/components/date-picker/week-picker.component.spec.ts @@ -32,6 +32,9 @@ describe('NzWeekPickerComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(NzTestWeekPickerComponent); fixtureInstance = fixture.componentInstance; + // set initial mode + fixtureInstance.useSuite = 1; + fixture.detectChanges(); }); afterEach(() => { @@ -69,6 +72,18 @@ describe('NzWeekPickerComponent', () => { expect(queryFromOverlay('.ant-picker-week-panel')).toBeTruthy(); })); + it('should show week num', fakeAsync(() => { + fixtureInstance.useSuite = 2; + fixture.whenRenderingDone().then(() => { + tick(500); + fixture.detectChanges(); + fixtureInstance.nzFormat = undefined; // cover branch + fixture.detectChanges(); + openPickerByClickTrigger(); + expect(queryFromOverlay('.ant-picker-week-panel-row .ant-picker-cell-week')).toBeDefined(); + }); + })); + //////////// function queryFromOverlay(selector: string): HTMLElement { @@ -84,9 +99,15 @@ describe('NzWeekPickerComponent', () => { }); @Component({ - template: ` ` + template: ` + + + + + ` }) export class NzTestWeekPickerComponent { + useSuite!: 1 | 2; nzFormat?: string; nzValue: Date | null = null; } diff --git a/components/date-picker/year-picker.component.spec.ts b/components/date-picker/year-picker.component.spec.ts index f48f64d657b..00b3e6b5204 100644 --- a/components/date-picker/year-picker.component.spec.ts +++ b/components/date-picker/year-picker.component.spec.ts @@ -131,6 +131,17 @@ describe('NzYearPickerComponent', () => { }); })); + it('should nz-year-picker work', fakeAsync(() => { + fixtureInstance.useSuite = 5; + fixture.whenRenderingDone().then(() => { + tick(500); + fixture.detectChanges(); + expect(getPickerContainer()).not.toBeNull(); + const pickerInput = getPickerInput(fixture.debugElement); + expect(pickerInput).not.toBeNull(); + }); + })); + it('should support nzCompact', () => { fixtureInstance.useSuite = 4; fixture.detectChanges(); @@ -373,11 +384,13 @@ describe('NzYearPickerComponent', () => { + + ` }) class NzTestYearPickerComponent { - useSuite?: 1 | 2 | 3 | 4; + useSuite?: 1 | 2 | 3 | 4 | 5; @ViewChild('tplExtraFooter', { static: true }) tplExtraFooter!: TemplateRef; // --- Suite 1 diff --git a/components/form/form-control.component.ts b/components/form/form-control.component.ts index fee457c2e25..5f80d89fa60 100644 --- a/components/form/form-control.component.ts +++ b/components/form/form-control.component.ts @@ -26,6 +26,7 @@ import { Observable, Subject, Subscription } from 'rxjs'; import { filter, startWith, takeUntil, tap } from 'rxjs/operators'; import { helpMotion } from 'ng-zorro-antd/core/animation'; +import { NzFormStatusService } from 'ng-zorro-antd/core/form'; import { BooleanInput, NzSafeAny } from 'ng-zorro-antd/core/types'; import { toBoolean } from 'ng-zorro-antd/core/util'; import { NzI18nService } from 'ng-zorro-antd/i18n'; @@ -33,13 +34,6 @@ import { NzI18nService } from 'ng-zorro-antd/i18n'; import { NzFormControlStatusType, NzFormItemComponent } from './form-item.component'; import { NzFormDirective } from './form.directive'; -const iconTypeMap = { - error: 'close-circle-fill', - validating: 'loading', - success: 'check-circle-fill', - warning: 'exclamation-circle-fill' -} as const; - @Component({ selector: 'nz-form-control', exportAs: 'nzFormControl', @@ -52,9 +46,6 @@ const iconTypeMap = {
- - -
@@ -66,7 +57,8 @@ const iconTypeMap = {
{{ nzExtra }}
- ` + `, + providers: [NzFormStatusService] }) export class NzFormControlComponent implements OnChanges, OnDestroy, OnInit, AfterContentInit, OnDestroy { static ngAcceptInputType_nzHasFeedback: BooleanInput; @@ -87,9 +79,8 @@ export class NzFormControlComponent implements OnChanges, OnDestroy, OnInit, Aft : this.nzFormDirective?.nzDisableAutoTips; } - status: NzFormControlStatusType = null; + status: NzFormControlStatusType = ''; validateControl: AbstractControl | NgModel | null = null; - iconType: typeof iconTypeMap[keyof typeof iconTypeMap] | null = null; innerTip: string | TemplateRef<{ $implicit: AbstractControl | NgModel }> | null = null; @ContentChild(NgControl, { static: false }) defaultValidateControl?: FormControlName | FormControlDirective; @@ -104,6 +95,7 @@ export class NzFormControlComponent implements OnChanges, OnDestroy, OnInit, Aft @Input() set nzHasFeedback(value: boolean) { this._hasFeedback = toBoolean(value); + this.nzFormStatusService.formStatusChanges.next({ status: this.status, hasFeedback: this._hasFeedback }); if (this.nzFormItemComponent) { this.nzFormItemComponent.setHasFeedback(this._hasFeedback); } @@ -148,8 +140,8 @@ export class NzFormControlComponent implements OnChanges, OnDestroy, OnInit, Aft private setStatus(): void { this.status = this.getControlStatus(this.validateString); - this.iconType = this.status ? iconTypeMap[this.status] : null; this.innerTip = this.getInnerTip(this.status); + this.nzFormStatusService.formStatusChanges.next({ status: this.status, hasFeedback: this.nzHasFeedback }); if (this.nzFormItemComponent) { this.nzFormItemComponent.setWithHelpViaTips(!!this.innerTip); this.nzFormItemComponent.setStatus(this.status); @@ -172,7 +164,7 @@ export class NzFormControlComponent implements OnChanges, OnDestroy, OnInit, Aft } else if (validateString === 'success' || this.validateControlStatus('VALID')) { status = 'success'; } else { - status = null; + status = ''; } return status; @@ -243,7 +235,8 @@ export class NzFormControlComponent implements OnChanges, OnDestroy, OnInit, Aft private cdr: ChangeDetectorRef, renderer: Renderer2, i18n: NzI18nService, - @Optional() private nzFormDirective: NzFormDirective + @Optional() private nzFormDirective: NzFormDirective, + private nzFormStatusService: NzFormStatusService ) { renderer.addClass(elementRef.nativeElement, 'ant-form-item-control'); diff --git a/components/form/form-control.spec.ts b/components/form/form-control.spec.ts index 94d05035f71..5c636cb7823 100644 --- a/components/form/form-control.spec.ts +++ b/components/form/form-control.spec.ts @@ -46,14 +46,6 @@ describe('nz-form-control', () => { it('should className correct', () => { expect(formControl.nativeElement.classList).toContain('ant-form-item-control'); }); - it('should hasFeedback work', () => { - expect(formItem.nativeElement.classList).not.toContain('ant-form-item-has-feedback'); - expect(formControl.nativeElement.querySelector('.ant-form-item-children-icon .anticon')).toBeNull(); - testComponent.hasFeedback = true; - testBed.fixture.detectChanges(); - expect(formItem.nativeElement.classList).toContain('ant-form-item-has-feedback'); - expect(formControl.nativeElement.querySelector('.ant-form-item-children-icon .anticon')).not.toBeNull(); - }); it('should status work', () => { const statusList: Array = ['warning', 'validating', 'pending', 'error', 'success']; statusList.forEach(status => { diff --git a/components/form/form-item.component.ts b/components/form/form-item.component.ts index 9a199d3bc20..b0e2fed4d5c 100644 --- a/components/form/form-item.component.ts +++ b/components/form/form-item.component.ts @@ -14,7 +14,7 @@ import { } from '@angular/core'; import { Subject } from 'rxjs'; -export type NzFormControlStatusType = 'success' | 'error' | 'warning' | 'validating' | null; +export type NzFormControlStatusType = 'success' | 'error' | 'warning' | 'validating' | ''; /** should add nz-row directive to host, track https://github.com/angular/angular/issues/8785 **/ @Component({ @@ -34,7 +34,7 @@ export type NzFormControlStatusType = 'success' | 'error' | 'warning' | 'validat template: ` ` }) export class NzFormItemComponent implements OnDestroy, OnDestroy { - status: NzFormControlStatusType = null; + status: NzFormControlStatusType = ''; hasFeedback = false; withHelpClass = false; diff --git a/components/grid/col.directive.ts b/components/grid/col.directive.ts index d2401ed096e..fe2e3a2afd7 100644 --- a/components/grid/col.directive.ts +++ b/components/grid/col.directive.ts @@ -128,7 +128,7 @@ export class NzColDirective implements OnInit, OnChanges, AfterViewInit, OnDestr ngOnInit(): void { this.dir = this.directionality.value; - this.directionality.change.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { + this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe((direction: Direction) => { this.dir = direction; this.setHostClassMap(); }); diff --git a/components/grid/demo/flex.md b/components/grid/demo/flex.md index 55831492f95..0a6a8a36df2 100755 --- a/components/grid/demo/flex.md +++ b/components/grid/demo/flex.md @@ -9,9 +9,9 @@ title: 布局基础。 -子元素根据不同的值 `start`,`center`,`end`,`space-between`,`space-around`,分别定义其在父节点里面的排版方式。 +子元素根据不同的值 `start`、`center`、`end`、`space-between`、`space-around` 和 `space-evenly`,分别定义其在父节点里面的排版方式。 ## en-US -Child elements depending on the value of the `start`,` center`, `end`,` space-between`, `space-around`, which are defined in its parent node layout mode. +Child elements depending on the value of the `start`,` center`, `end`,` space-between`, `space-around`, `space-evenly`, which are defined in its parent node layout mode. diff --git a/components/grid/demo/flex.ts b/components/grid/demo/flex.ts index 7ebe910a1ab..58aa1a620aa 100755 --- a/components/grid/demo/flex.ts +++ b/components/grid/demo/flex.ts @@ -39,6 +39,13 @@ import { Component } from '@angular/core';
col-4
col-4
+

sub-element align evenly

+
+
col-4
+
col-4
+
col-4
+
col-4
+
`, styles: [ diff --git a/components/grid/row.directive.ts b/components/grid/row.directive.ts index 4bd1b429c03..0c8c231b37b 100644 --- a/components/grid/row.directive.ts +++ b/components/grid/row.directive.ts @@ -25,7 +25,7 @@ import { takeUntil } from 'rxjs/operators'; import { gridResponsiveMap, NzBreakpointKey, NzBreakpointService } from 'ng-zorro-antd/core/services'; import { IndexableObject } from 'ng-zorro-antd/core/types'; -export type NzJustify = 'start' | 'end' | 'center' | 'space-around' | 'space-between'; +export type NzJustify = 'start' | 'end' | 'center' | 'space-around' | 'space-between' | 'space-evenly'; export type NzAlign = 'top' | 'middle' | 'bottom'; @Directive({ @@ -41,6 +41,7 @@ export type NzAlign = 'top' | 'middle' | 'bottom'; '[class.ant-row-center]': `nzJustify === 'center'`, '[class.ant-row-space-around]': `nzJustify === 'space-around'`, '[class.ant-row-space-between]': `nzJustify === 'space-between'`, + '[class.ant-row-space-evenly]': `nzJustify === 'space-evenly'`, '[class.ant-row-rtl]': `dir === "rtl"` } }) diff --git a/components/input-number/input-number-group-slot.component.ts b/components/input-number/input-number-group-slot.component.ts index 7b6f4f5fc1c..f887dfc508f 100644 --- a/components/input-number/input-number-group-slot.component.ts +++ b/components/input-number/input-number-group-slot.component.ts @@ -13,7 +13,6 @@ import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewEncapsulati template: ` {{ template }} - `, host: { '[class.ant-input-number-group-addon]': `type === 'addon'`, diff --git a/components/input-number/input-number-group.component.ts b/components/input-number/input-number-group.component.ts index 211787342f4..973d09841d4 100644 --- a/components/input-number/input-number-group.component.ts +++ b/components/input-number/input-number-group.component.ts @@ -25,8 +25,9 @@ import { ViewEncapsulation } from '@angular/core'; import { merge, Subject } from 'rxjs'; -import { map, mergeMap, startWith, switchMap, takeUntil } from 'rxjs/operators'; +import { distinctUntilChanged, map, mergeMap, startWith, switchMap, takeUntil } from 'rxjs/operators'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; import { BooleanInput, NgClassInterface, NzSizeLDSType, NzStatus, NzValidateStatus } from 'ng-zorro-antd/core/types'; import { getStatusClassNames, InputBoolean } from 'ng-zorro-antd/core/util'; @@ -45,6 +46,7 @@ export class NzInputNumberGroupWhitSuffixOrPrefixDirective { preserveWhitespaces: false, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, + providers: [NzFormNoStatusService], template: `
@@ -86,7 +90,7 @@ export class NzInputNumberGroupWhitSuffixOrPrefixDirective { >
(); @@ -149,7 +155,9 @@ export class NzInputNumberGroupComponent implements AfterContentInit, OnChanges, private elementRef: ElementRef, private renderer: Renderer2, private cdr: ChangeDetectorRef, - @Optional() private directionality: Directionality + @Optional() private directionality: Directionality, + @Optional() private nzFormStatusService?: NzFormStatusService, + @Optional() private nzFormNoStatusService?: NzFormNoStatusService ) {} updateChildrenInputSize(): void { @@ -159,6 +167,17 @@ export class NzInputNumberGroupComponent implements AfterContentInit, OnChanges, } 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$)) @@ -215,6 +234,7 @@ export class NzInputNumberGroupComponent implements AfterContentInit, OnChanges, } if (nzAddOnAfter || nzAddOnBefore || nzAddOnAfterIcon || nzAddOnBeforeIcon) { this.isAddOn = !!(this.nzAddOnAfter || this.nzAddOnBefore || this.nzAddOnAfterIcon || this.nzAddOnBeforeIcon); + this.nzFormNoStatusService?.noFormStatus?.next(this.isAddOn); } if (nzStatus) { this.setStatusStyles(this.nzStatus, this.hasFeedback); @@ -227,19 +247,25 @@ export class NzInputNumberGroupComponent implements AfterContentInit, OnChanges, } private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void { - this.status = status; this.hasFeedback = hasFeedback; + this.isFeedback = !!status && hasFeedback; + const baseAffix = !!(this.nzSuffix || this.nzPrefix || this.nzPrefixIcon || this.nzSuffixIcon); + this.isAffix = baseAffix || (!this.isAddOn && hasFeedback); + this.affixInGroupStatusCls = + this.isAffix || this.isFeedback + ? (this.affixStatusCls = getStatusClassNames(`${this.prefixCls}-affix-wrapper`, status, hasFeedback)) + : {}; this.cdr.markForCheck(); // render status if nzStatus is set this.affixStatusCls = getStatusClassNames( `${this.prefixCls}-affix-wrapper`, this.isAddOn ? '' : status, - hasFeedback + this.isAddOn ? false : hasFeedback ); this.groupStatusCls = getStatusClassNames( `${this.prefixCls}-group-wrapper`, this.isAddOn ? status : '', - hasFeedback + this.isAddOn ? hasFeedback : false ); const statusCls = { ...this.affixStatusCls, diff --git a/components/input-number/input-number-group.spec.ts b/components/input-number/input-number-group.spec.ts index 1524c07664a..0cc8621e0d7 100644 --- a/components/input-number/input-number-group.spec.ts +++ b/components/input-number/input-number-group.spec.ts @@ -10,11 +10,13 @@ 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'; +import { NzFormControlStatusType, NzFormModule } from '../form'; + describe('input-number-group', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [BidiModule, NzInputNumberModule, FormsModule, ReactiveFormsModule, NzIconTestModule], + imports: [BidiModule, NzInputNumberModule, FormsModule, ReactiveFormsModule, NzIconTestModule, NzFormModule], declarations: [ NzTestInputNumberGroupAddonComponent, NzTestInputNumberGroupAffixComponent, @@ -22,7 +24,8 @@ describe('input-number-group', () => { NzTestInputNumberGroupColComponent, NzTestInputNumberGroupMixComponent, NzTestInputNumberGroupWithStatusComponent, - NzTestInputNumberGroupWithDirComponent + NzTestInputNumberGroupWithDirComponent, + NzTestInputNumberGroupInFormComponent ], providers: [] }).compileComponents(); @@ -292,6 +295,54 @@ describe('input-number-group', () => { expect(inputNumberGroupElement.classList).toContain('ant-input-number-group-wrapper-rtl'); }); }); + + describe('in form', () => { + let fixture: ComponentFixture; + let inputNumberGroupElement: DebugElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputNumberGroupInFormComponent); + fixture.detectChanges(); + inputNumberGroupElement = fixture.debugElement.query(By.directive(NzInputNumberGroupComponent)); + }); + + it('should className correct', () => { + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.classList).toContain( + 'ant-input-number-affix-wrapper-status-error' + ); + expect(inputNumberGroupElement.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.classList).toContain( + 'ant-input-number-affix-wrapper-status-warning' + ); + + fixture.componentInstance.status = 'success'; + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.classList).toContain( + 'ant-input-number-affix-wrapper-status-success' + ); + + fixture.componentInstance.feedback = false; + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeNull(); + }); + + it('should className correct with addon', () => { + fixture.componentInstance.addon = 'before'; + fixture.detectChanges(); + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(inputNumberGroupElement.nativeElement.classList).toContain( + 'ant-input-number-group-wrapper-status-warning' + ); + expect(inputNumberGroupElement.nativeElement.classList).not.toContain( + 'ant-input-number-affix-wrapper-status-warning' + ); + }); + }); }); }); @@ -398,3 +449,22 @@ export class NzTestInputNumberGroupWithStatusComponent { export class NzTestInputNumberGroupWithDirComponent { dir: Direction = 'ltr'; } + +@Component({ + template: ` +
+ + + + + + + +
+ ` +}) +export class NzTestInputNumberGroupInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; + addon: string = ''; +} diff --git a/components/input-number/input-number.component.ts b/components/input-number/input-number.component.ts index b27be56412d..4e977462902 100644 --- a/components/input-number/input-number.component.ts +++ b/components/input-number/input-number.component.ts @@ -28,8 +28,9 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { fromEvent, merge, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; +import { NzFormStatusService } from 'ng-zorro-antd/core/form'; import { NzDestroyService } from 'ng-zorro-antd/core/services'; import { BooleanInput, @@ -84,6 +85,11 @@ import { getStatusClassNames, InputBoolean, isNotNil } from 'ng-zorro-antd/core/ (ngModelChange)="onModelChange($event)" /> + `, providers: [ { @@ -97,6 +103,7 @@ import { getStatusClassNames, InputBoolean, isNotNil } from 'ng-zorro-antd/core/ 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]': `nzSize === 'large'`, '[class.ant-input-number-sm]': `nzSize === 'small'`, @@ -395,21 +402,36 @@ export class NzInputNumberComponent implements ControlValueAccessor, AfterViewIn private focusMonitor: FocusMonitor, private renderer: Renderer2, @Optional() private directionality: Directionality, - private destroy$: NzDestroyService + private destroy$: NzDestroyService, + @Optional() public nzFormStatusService?: NzFormStatusService ) {} ngOnInit(): void { - this.focusMonitor.monitor(this.elementRef, true).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(); - } - }); + 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 => { + 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) => { @@ -496,7 +518,6 @@ export class NzInputNumberComponent implements ControlValueAccessor, AfterViewIn this.cdr.markForCheck(); // render status if nzStatus is set 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 17c054a4a5d..23466040dcd 100644 --- a/components/input-number/input-number.module.ts +++ b/components/input-number/input-number.module.ts @@ -8,6 +8,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NzFormPatchModule } from 'ng-zorro-antd/core/form'; import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; import { NzIconModule } from 'ng-zorro-antd/icon'; @@ -19,7 +20,7 @@ import { import { NzInputNumberComponent } from './input-number.component'; @NgModule({ - imports: [BidiModule, CommonModule, FormsModule, NzOutletModule, NzIconModule], + imports: [BidiModule, CommonModule, FormsModule, NzOutletModule, NzIconModule, NzFormPatchModule], declarations: [ NzInputNumberComponent, NzInputNumberGroupWhitSuffixOrPrefixDirective, diff --git a/components/input-number/input-number.spec.ts b/components/input-number/input-number.spec.ts index 62b1e7f4a8c..89045e334f3 100644 --- a/components/input-number/input-number.spec.ts +++ b/components/input-number/input-number.spec.ts @@ -8,18 +8,20 @@ import { take } from 'rxjs/operators'; import { createKeyboardEvent, createMouseEvent, dispatchEvent, dispatchFakeEvent } from 'ng-zorro-antd/core/testing'; import { NzStatus } from 'ng-zorro-antd/core/types'; +import { NzFormControlStatusType, NzFormModule } from '../form'; import { NzInputNumberComponent } from './input-number.component'; import { NzInputNumberModule } from './input-number.module'; describe('input number', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [NzInputNumberModule, FormsModule, ReactiveFormsModule], + imports: [NzInputNumberModule, FormsModule, ReactiveFormsModule, NzFormModule], declarations: [ NzTestInputNumberBasicComponent, NzTestInputNumberFormComponent, NzTestReadOnlyInputNumberBasicComponent, - NzTestInputNumberStatusComponent + NzTestInputNumberStatusComponent, + NzTestInputNumberInFormComponent ] }); TestBed.compileComponents(); @@ -547,6 +549,47 @@ describe('input number', () => { expect(inputNumber.nativeElement.className).not.toContain('ant-input-number-status-warning'); }); }); + + describe('input number in form', () => { + let fixture: ComponentFixture; + let testComponent: NzTestInputNumberInFormComponent; + let inputNumber: DebugElement; + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(NzTestInputNumberInFormComponent); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + + inputNumber = fixture.debugElement.query(By.directive(NzInputNumberComponent)); + })); + it('should className correct', () => { + const feedbackElement = fixture.nativeElement.querySelector('nz-form-item-feedback-icon'); + fixture.detectChanges(); + expect(inputNumber.nativeElement.classList).toContain('ant-input-number-status-error'); + expect(feedbackElement.classList).toContain('ant-form-item-feedback-icon-error'); + + testComponent.status = 'success'; + fixture.detectChanges(); + expect(inputNumber.nativeElement.classList).toContain('ant-input-number-status-success'); + expect(feedbackElement.classList).toContain('ant-form-item-feedback-icon-success'); + + testComponent.status = 'warning'; + fixture.detectChanges(); + expect(inputNumber.nativeElement.classList).toContain('ant-input-number-status-warning'); + expect(feedbackElement.classList).toContain('ant-form-item-feedback-icon-warning'); + + testComponent.status = 'validating'; + fixture.detectChanges(); + expect(inputNumber.nativeElement.classList).toContain('ant-input-number-status-validating'); + expect(feedbackElement.classList).toContain('ant-form-item-feedback-icon-validating'); + + testComponent.feedback = false; + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeNull(); + }); + }); }); @Component({ @@ -620,3 +663,19 @@ export class NzTestInputNumberFormComponent { export class NzTestInputNumberStatusComponent { status: NzStatus = 'error'; } + +@Component({ + template: ` +
+ + + + + +
+ ` +}) +export class NzTestInputNumberInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; +} diff --git a/components/input-number/style/patch.less b/components/input-number/style/patch.less index cbe972a8148..78d69682962 100644 --- a/components/input-number/style/patch.less +++ b/components/input-number/style/patch.less @@ -10,4 +10,10 @@ } } } + + &&-has-feedback { + .@{ant-prefix}-input-number-handler-wrap { + z-index: 2; + } + } } diff --git a/components/input/input-group.component.ts b/components/input/input-group.component.ts index 47f1da4e74d..19216ac3dbe 100644 --- a/components/input/input-group.component.ts +++ b/components/input/input-group.component.ts @@ -25,9 +25,10 @@ import { ViewEncapsulation } from '@angular/core'; import { merge, Subject } from 'rxjs'; -import { map, mergeMap, startWith, switchMap, takeUntil } from 'rxjs/operators'; +import { distinctUntilChanged, map, mergeMap, startWith, switchMap, takeUntil } from 'rxjs/operators'; -import { BooleanInput, NgClassInterface, NzSizeLDSType, NzStatus } from 'ng-zorro-antd/core/types'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; +import { BooleanInput, NgClassInterface, NzSizeLDSType, NzStatus, NzValidateStatus } from 'ng-zorro-antd/core/types'; import { getStatusClassNames, InputBoolean } from 'ng-zorro-antd/core/util'; import { NzInputDirective } from './input.directive'; @@ -45,6 +46,7 @@ export class NzInputGroupWhitSuffixOrPrefixDirective { preserveWhitespaces: false, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, + providers: [NzFormNoStatusService], template: ` @@ -91,7 +95,8 @@ export class NzInputGroupWhitSuffixOrPrefixDirective { type="suffix" [icon]="nzSuffixIcon" [template]="nzSuffix" - > + > +
@@ -132,7 +137,7 @@ export class NzInputGroupComponent implements AfterContentInit, OnChanges, OnIni @Input() nzAddOnBefore?: string | TemplateRef; @Input() nzAddOnAfter?: string | TemplateRef; @Input() nzPrefix?: string | TemplateRef; - @Input() nzStatus?: NzStatus; + @Input() nzStatus: NzStatus = ''; @Input() nzSuffix?: string | TemplateRef; @Input() nzSize: NzSizeLDSType = 'default'; @Input() @InputBoolean() nzSearch = false; @@ -141,6 +146,7 @@ export class NzInputGroupComponent implements AfterContentInit, OnChanges, OnIni isSmall = false; isAffix = false; isAddOn = false; + isFeedback = false; focused = false; disabled = false; dir: Direction = 'ltr'; @@ -148,6 +154,7 @@ export class NzInputGroupComponent implements AfterContentInit, OnChanges, OnIni prefixCls: string = 'ant-input'; affixStatusCls: NgClassInterface = {}; groupStatusCls: NgClassInterface = {}; + affixInGroupStatusCls: NgClassInterface = {}; hasFeedback: boolean = false; private destroy$ = new Subject(); @@ -156,7 +163,9 @@ export class NzInputGroupComponent implements AfterContentInit, OnChanges, OnIni private elementRef: ElementRef, private renderer: Renderer2, private cdr: ChangeDetectorRef, - @Optional() private directionality: Directionality + @Optional() private directionality: Directionality, + @Optional() private nzFormStatusService?: NzFormStatusService, + @Optional() private nzFormNoStatusService?: NzFormNoStatusService ) {} updateChildrenInputSize(): void { @@ -166,6 +175,17 @@ export class NzInputGroupComponent implements AfterContentInit, OnChanges, OnIni } 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$)) @@ -218,9 +238,10 @@ export class NzInputGroupComponent implements AfterContentInit, OnChanges, OnIni } if (nzAddOnAfter || nzAddOnBefore || nzAddOnAfterIcon || nzAddOnBeforeIcon) { this.isAddOn = !!(this.nzAddOnAfter || this.nzAddOnBefore || this.nzAddOnAfterIcon || this.nzAddOnBeforeIcon); + this.nzFormNoStatusService?.noFormStatus?.next(this.isAddOn); } if (nzStatus) { - this.setStatusStyles(); + this.setStatusStyles(this.nzStatus, this.hasFeedback); } } ngOnDestroy(): void { @@ -229,11 +250,31 @@ export class NzInputGroupComponent implements AfterContentInit, OnChanges, OnIni this.destroy$.complete(); } - private setStatusStyles(): void { + private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void { + this.hasFeedback = hasFeedback; + this.isFeedback = !!status && hasFeedback; + const baseAffix = !!(this.nzSuffix || this.nzPrefix || this.nzPrefixIcon || this.nzSuffixIcon); + this.isAffix = baseAffix || (!this.isAddOn && hasFeedback); + this.affixInGroupStatusCls = + this.isAffix || this.isFeedback + ? (this.affixStatusCls = getStatusClassNames(`${this.prefixCls}-affix-wrapper`, status, hasFeedback)) + : {}; + this.cdr.markForCheck(); // render status if nzStatus is set - this.affixStatusCls = getStatusClassNames(`${this.prefixCls}-affix-wrapper`, this.nzStatus, this.hasFeedback); - this.groupStatusCls = getStatusClassNames(`${this.prefixCls}-group-wrapper`, this.nzStatus, this.hasFeedback); - const statusCls = this.isAffix ? this.affixStatusCls : this.isAddOn ? this.groupStatusCls : {}; + this.affixStatusCls = getStatusClassNames( + `${this.prefixCls}-affix-wrapper`, + this.isAddOn ? '' : status, + this.isAddOn ? false : hasFeedback + ); + this.groupStatusCls = getStatusClassNames( + `${this.prefixCls}-group-wrapper`, + this.isAddOn ? status : '', + this.isAddOn ? hasFeedback : false + ); + const statusCls = { + ...this.affixStatusCls, + ...this.groupStatusCls + }; Object.keys(statusCls).forEach(status => { if (statusCls[status]) { this.renderer.addClass(this.elementRef.nativeElement, status); diff --git a/components/input/input-group.spec.ts b/components/input/input-group.spec.ts index 74204794a7e..4585c132e03 100644 --- a/components/input/input-group.spec.ts +++ b/components/input/input-group.spec.ts @@ -7,6 +7,7 @@ import { dispatchFakeEvent } from 'ng-zorro-antd/core/testing'; import { NzStatus } from 'ng-zorro-antd/core/types'; import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; +import { NzFormControlStatusType, NzFormModule } from '../form'; import { NzInputGroupComponent } from './input-group.component'; import { NzInputModule } from './input.module'; @@ -14,14 +15,15 @@ describe('input-group', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [NzInputModule, FormsModule, ReactiveFormsModule, NzIconTestModule], + imports: [NzInputModule, FormsModule, ReactiveFormsModule, NzIconTestModule, NzFormModule], declarations: [ NzTestInputGroupAddonComponent, NzTestInputGroupAffixComponent, NzTestInputGroupMultipleComponent, NzTestInputGroupColComponent, NzTestInputGroupMixComponent, - NzTestInputGroupWithStatusComponent + NzTestInputGroupWithStatusComponent, + NzTestInputGroupInFormComponent ], providers: [] }).compileComponents(); @@ -280,6 +282,43 @@ describe('input-group', () => { expect(inputElement.nativeElement.className).not.toContain('ant-input-group-wrapper-status-warning'); }); }); + describe('in form', () => { + let fixture: ComponentFixture; + let inputElement: DebugElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputGroupInFormComponent); + inputElement = fixture.debugElement.query(By.directive(NzInputGroupComponent)); + fixture.detectChanges(); + }); + + it('should className correct', () => { + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-affix-wrapper-status-error'); + expect(inputElement.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-affix-wrapper-status-warning'); + + fixture.componentInstance.status = 'success'; + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-affix-wrapper-status-success'); + + fixture.componentInstance.feedback = false; + fixture.detectChanges(); + expect(inputElement.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeNull(); + }); + + it('should className correct with addon', () => { + fixture.componentInstance.addon = 'before'; + fixture.detectChanges(); + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-group-wrapper-status-warning'); + expect(inputElement.nativeElement.classList).not.toContain('ant-input-affix-wrapper-status-warning'); + }); + }); }); }); @@ -375,3 +414,22 @@ export class NzTestInputGroupWithStatusComponent { isAddon = false; status: NzStatus = 'error'; } + +@Component({ + template: ` +
+ + + + + + + +
+ ` +}) +export class NzTestInputGroupInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; + addon: string = ''; +} diff --git a/components/input/input.directive.ts b/components/input/input.directive.ts index 9e009535ec0..ae6d165766c 100644 --- a/components/input/input.directive.ts +++ b/components/input/input.directive.ts @@ -5,6 +5,7 @@ import { Direction, Directionality } from '@angular/cdk/bidi'; import { + ComponentRef, Directive, ElementRef, Input, @@ -14,13 +15,15 @@ import { Optional, Renderer2, Self, - SimpleChanges + SimpleChanges, + ViewContainerRef } from '@angular/core'; import { NgControl } from '@angular/forms'; import { Subject } from 'rxjs'; -import { filter, takeUntil } from 'rxjs/operators'; +import { distinctUntilChanged, filter, takeUntil } from 'rxjs/operators'; -import { BooleanInput, NgClassInterface, NzSizeLDSType, NzStatus } from 'ng-zorro-antd/core/types'; +import { NzFormItemFeedbackIconComponent, NzFormStatusService } from 'ng-zorro-antd/core/form'; +import { BooleanInput, NgClassInterface, NzSizeLDSType, NzStatus, NzValidateStatus } from 'ng-zorro-antd/core/types'; import { getStatusClassNames, InputBoolean } from 'ng-zorro-antd/core/util'; @Directive({ @@ -40,7 +43,7 @@ export class NzInputDirective implements OnChanges, OnInit, OnDestroy { static ngAcceptInputType_nzBorderless: BooleanInput; @Input() @InputBoolean() nzBorderless = false; @Input() nzSize: NzSizeLDSType = 'default'; - @Input() nzStatus?: NzStatus; + @Input() nzStatus: NzStatus = ''; @Input() get disabled(): boolean { if (this.ngControl && this.ngControl.disabled !== null) { @@ -56,20 +59,36 @@ export class NzInputDirective implements OnChanges, OnInit, OnDestroy { dir: Direction = 'ltr'; // status prefixCls: string = 'ant-input'; + status: NzValidateStatus = ''; statusCls: NgClassInterface = {}; hasFeedback: boolean = false; + feedbackRef: ComponentRef | null = null; + components: Array> = []; private destroy$ = new Subject(); constructor( @Optional() @Self() public ngControl: NgControl, private renderer: Renderer2, private elementRef: ElementRef, - @Optional() private directionality: Directionality + protected hostView: ViewContainerRef, + @Optional() private directionality: Directionality, + @Optional() private nzFormStatusService?: NzFormStatusService ) { renderer.addClass(elementRef.nativeElement, 'ant-input'); } 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); + }); + if (this.ngControl) { this.ngControl.statusChanges ?.pipe( @@ -93,7 +112,7 @@ export class NzInputDirective implements OnChanges, OnInit, OnDestroy { this.disabled$.next(this.disabled); } if (nzStatus) { - this.setStatusStyles(); + this.setStatusStyles(this.nzStatus, this.hasFeedback); } } @@ -102,9 +121,13 @@ export class NzInputDirective implements OnChanges, OnInit, OnDestroy { this.destroy$.complete(); } - private setStatusStyles(): void { + private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void { + // set inner status + this.status = status; + this.hasFeedback = hasFeedback; + this.renderFeedbackIcon(); // render status if nzStatus is set - this.statusCls = getStatusClassNames(this.prefixCls, this.nzStatus, this.hasFeedback); + this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.elementRef.nativeElement, status); @@ -113,4 +136,18 @@ export class NzInputDirective implements OnChanges, OnInit, OnDestroy { } }); } + + private renderFeedbackIcon(): void { + if (!this.status || !this.hasFeedback) { + // remove feedback + this.hostView.clear(); + this.feedbackRef = null; + return; + } + + this.feedbackRef = this.feedbackRef || this.hostView.createComponent(NzFormItemFeedbackIconComponent); + this.feedbackRef.location.nativeElement.classList.add('ant-input-suffix'); + this.feedbackRef.instance.status = this.status; + this.feedbackRef.instance.updateIcon(); + } } diff --git a/components/input/input.module.ts b/components/input/input.module.ts index cc781ff37aa..e764486095f 100644 --- a/components/input/input.module.ts +++ b/components/input/input.module.ts @@ -8,6 +8,7 @@ import { PlatformModule } from '@angular/cdk/platform'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { NzFormItemFeedbackIconComponent, NzFormPatchModule } from 'ng-zorro-antd/core/form'; import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; import { NzIconModule } from 'ng-zorro-antd/icon'; @@ -33,6 +34,7 @@ import { NzTextareaCountComponent } from './textarea-count.component'; NzAutosizeDirective, NzInputGroupWhitSuffixOrPrefixDirective ], - imports: [BidiModule, CommonModule, NzIconModule, PlatformModule, NzOutletModule] + entryComponents: [NzFormItemFeedbackIconComponent], + imports: [BidiModule, CommonModule, NzIconModule, PlatformModule, NzOutletModule, NzFormPatchModule] }) export class NzInputModule {} diff --git a/components/input/input.spec.ts b/components/input/input.spec.ts index ebe63738486..cd32c5bf44b 100644 --- a/components/input/input.spec.ts +++ b/components/input/input.spec.ts @@ -8,6 +8,7 @@ import { NzStatus } from 'ng-zorro-antd/core/types'; import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; import { NzInputGroupComponent } from 'ng-zorro-antd/input/input-group.component'; +import { NzFormControlStatusType, NzFormModule } from '../form'; import { NzInputDirective } from './input.directive'; import { NzInputModule } from './input.module'; @@ -15,13 +16,14 @@ describe('input', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [BidiModule, NzInputModule, FormsModule, ReactiveFormsModule, NzIconTestModule], + imports: [BidiModule, NzInputModule, FormsModule, ReactiveFormsModule, NzIconTestModule, NzFormModule], declarations: [ NzTestInputWithInputComponent, NzTestInputWithTextAreaComponent, NzTestInputFormComponent, NzTestInputWithStatusComponent, - NzTestInputWithDirComponent + NzTestInputWithDirComponent, + NzTestInputInFormComponent ], providers: [] }).compileComponents(); @@ -148,6 +150,43 @@ describe('input', () => { expect(inputElement.nativeElement.className).not.toContain('ant-input-status-warning'); }); }); + + describe('input in form', () => { + let fixture: ComponentFixture; + let inputElement: DebugElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestInputInFormComponent); + fixture.detectChanges(); + inputElement = fixture.debugElement.query(By.directive(NzInputDirective)); + }); + + it('should className correct', () => { + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-status-error'); + expect(inputElement.nativeElement.nextSibling.classList).toContain('ant-form-item-feedback-icon-error'); + + fixture.componentInstance.status = 'success'; + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-status-success'); + expect(inputElement.nativeElement.nextSibling.classList).toContain('ant-form-item-feedback-icon-success'); + + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-status-warning'); + expect(inputElement.nativeElement.nextSibling.classList).toContain('ant-form-item-feedback-icon-warning'); + + fixture.componentInstance.status = 'validating'; + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-status-validating'); + expect(inputElement.nativeElement.nextSibling.classList).toContain('ant-form-item-feedback-icon-validating'); + + fixture.componentInstance.feedback = false; + fixture.detectChanges(); + expect(inputElement.nativeElement.classList).toContain('ant-input-status-validating'); + expect(inputElement.nativeElement.nextSibling?.classList).not.toContain('ant-form-item-feedback-icon'); + }); + }); }); @Component({ @@ -205,3 +244,19 @@ export class NzTestInputFormComponent { export class NzTestInputWithStatusComponent { status: NzStatus = 'error'; } + +@Component({ + template: ` +
+ + + + + +
+ ` +}) +export class NzTestInputInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; +} diff --git a/components/input/style/patch.less b/components/input/style/patch.less index 9f71d9c726f..90ca373365d 100644 --- a/components/input/style/patch.less +++ b/components/input/style/patch.less @@ -25,3 +25,26 @@ textarea.nz-textarea-autosize-measuring { } } } + +.@{ant-prefix}-input { + &-suffix { + display: flex; + flex: none; + align-items: center; + pointer-events: none; + } + + &-suffix { + position: absolute; + top: 0; + right: 0; + z-index: 1; + height: 100%; + margin-right: @input-padding-horizontal-base; + margin-left: @input-affix-margin; + } + + &&-has-feedback { + padding-right: @padding-lg; + } +} \ No newline at end of file diff --git a/components/mention/mention.component.ts b/components/mention/mention.component.ts index d38d13ce78d..acfb37e5db8 100644 --- a/components/mention/mention.component.ts +++ b/components/mention/mention.component.ts @@ -39,12 +39,13 @@ import { ViewChildren, ViewContainerRef } from '@angular/core'; -import { fromEvent, merge, Observable, Subscription } from 'rxjs'; -import { startWith, switchMap, takeUntil } from 'rxjs/operators'; +import { fromEvent, merge, Observable, of as observableOf, Subscription } from 'rxjs'; +import { distinctUntilChanged, map, startWith, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; import { DEFAULT_MENTION_BOTTOM_POSITIONS, DEFAULT_MENTION_TOP_POSITIONS } from 'ng-zorro-antd/core/overlay'; import { NzDestroyService } from 'ng-zorro-antd/core/services'; -import { BooleanInput, NgClassInterface, NzSafeAny, NzStatus } from 'ng-zorro-antd/core/types'; +import { BooleanInput, NgClassInterface, NzSafeAny, NzStatus, NzValidateStatus } from 'ng-zorro-antd/core/types'; import { getCaretCoordinates, getMentions, getStatusClassNames, InputBoolean } from 'ng-zorro-antd/core/util'; import { NZ_MENTION_CONFIG } from './config'; @@ -100,6 +101,11 @@ export type MentionPlacement = 'top' | 'bottom';
+ `, preserveWhitespaces: false, changeDetection: ChangeDetectionStrategy.OnPush, @@ -118,7 +124,7 @@ export class NzMentionComponent implements OnDestroy, OnInit, AfterViewInit, OnC @Input() nzNotFoundContent: string = '无匹配结果,轻敲空格完成输入'; @Input() nzPlacement: MentionPlacement = 'bottom'; @Input() nzSuggestions: NzSafeAny[] = []; - @Input() nzStatus?: NzStatus; + @Input() nzStatus: NzStatus = ''; @Output() readonly nzOnSelect: EventEmitter = new EventEmitter(); @Output() readonly nzOnSearchChange: EventEmitter = new EventEmitter(); @@ -142,7 +148,8 @@ export class NzMentionComponent implements OnDestroy, OnInit, AfterViewInit, OnC // status prefixCls: string = 'ant-mentions'; statusCls: NgClassInterface = {}; - nzHasFeedback: boolean = false; + status: NzValidateStatus = ''; + hasFeedback: boolean = false; private previousValue: string | null = null; private cursorMention: string | null = null; @@ -175,10 +182,25 @@ export class NzMentionComponent implements OnDestroy, OnInit, AfterViewInit, OnC private elementRef: ElementRef, private renderer: Renderer2, private nzMentionService: NzMentionService, - private destroy$: NzDestroyService + private destroy$: NzDestroyService, + @Optional() private nzFormStatusService?: NzFormStatusService, + @Optional() private nzFormNoStatusService?: NzFormNoStatusService ) {} ngOnInit(): void { + this.nzFormStatusService?.formStatusChanges + .pipe( + distinctUntilChanged((pre, cur) => { + return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; + }), + withLatestFrom(this.nzFormNoStatusService ? this.nzFormNoStatusService.noFormStatus : observableOf(false)), + map(([{ status, hasFeedback }, noStatus]) => ({ status: noStatus ? '' : status, hasFeedback })), + takeUntil(this.destroy$) + ) + .subscribe(({ status, hasFeedback }) => { + this.setStatusStyles(status, hasFeedback); + }); + this.nzMentionService.triggerChanged().subscribe(trigger => { this.trigger = trigger; this.bindTriggerEvents(); @@ -202,7 +224,7 @@ export class NzMentionComponent implements OnDestroy, OnInit, AfterViewInit, OnC } } if (nzStatus) { - this.setStatusStyles(); + this.setStatusStyles(this.nzStatus, this.hasFeedback); } } @@ -477,9 +499,13 @@ export class NzMentionComponent implements OnDestroy, OnInit, AfterViewInit, OnC return this.positionStrategy; } - 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); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.elementRef.nativeElement, status); diff --git a/components/mention/mention.module.ts b/components/mention/mention.module.ts index a86bf607a85..9095d6b244e 100644 --- a/components/mention/mention.module.ts +++ b/components/mention/mention.module.ts @@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NzFormPatchModule } from 'ng-zorro-antd/core/form'; import { NzEmptyModule } from 'ng-zorro-antd/empty'; import { NzIconModule } from 'ng-zorro-antd/icon'; @@ -19,7 +20,7 @@ import { NzMentionComponent } from './mention.component'; const COMPONENTS = [NzMentionComponent, NzMentionTriggerDirective, NzMentionSuggestionDirective]; @NgModule({ - imports: [BidiModule, CommonModule, FormsModule, OverlayModule, NzIconModule, NzEmptyModule], + imports: [BidiModule, CommonModule, FormsModule, OverlayModule, NzIconModule, NzEmptyModule, NzFormPatchModule], declarations: [...COMPONENTS], exports: [...COMPONENTS] }) diff --git a/components/mention/nz-mention.spec.ts b/components/mention/nz-mention.spec.ts index 64f17f3bc7d..0aa3226a0d5 100644 --- a/components/mention/nz-mention.spec.ts +++ b/components/mention/nz-mention.spec.ts @@ -19,6 +19,7 @@ import { import { NzStatus } from 'ng-zorro-antd/core/types'; import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; +import { NzFormControlStatusType, NzFormModule } from '../form'; import { NzInputModule } from '../input'; import { NzMentionTriggerDirective } from './mention-trigger'; import { NzMentionComponent } from './mention.component'; @@ -41,13 +42,15 @@ describe('mention', () => { NoopAnimationsModule, FormsModule, ReactiveFormsModule, - NzIconTestModule + NzIconTestModule, + NzFormModule ], declarations: [ NzTestSimpleMentionComponent, NzTestPropertyMentionComponent, NzTestDirMentionComponent, - NzTestStatusMentionComponent + NzTestStatusMentionComponent, + NzTestMentionInFormComponent ], providers: [ { provide: Directionality, useFactory: () => ({ value: dir }) }, @@ -549,6 +552,35 @@ describe('mention', () => { expect(mention.nativeElement.classList).not.toContain('ant-mentions-status-warning'); }); }); + + describe('in form', () => { + let fixture: ComponentFixture; + let mention: DebugElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestMentionInFormComponent); + mention = fixture.debugElement.query(By.directive(NzMentionComponent)); + fixture.detectChanges(); + }); + + it('should className correct', () => { + fixture.detectChanges(); + expect(mention.nativeElement.classList).toContain('ant-mentions-status-error'); + expect(mention.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(mention.nativeElement.classList).toContain('ant-mentions-status-warning'); + + fixture.componentInstance.status = 'success'; + fixture.detectChanges(); + expect(mention.nativeElement.classList).toContain('ant-mentions-status-success'); + + fixture.componentInstance.feedback = false; + fixture.detectChanges(); + expect(mention.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeNull(); + }); + }); }); @Component({ @@ -652,3 +684,21 @@ class NzTestDirMentionComponent { class NzTestStatusMentionComponent { status: NzStatus = 'error'; } + +@Component({ + template: ` +
+ + + + + + + +
+ ` +}) +class NzTestMentionInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; +} diff --git a/components/page-header/demo/actions.ts b/components/page-header/demo/actions.ts index e6035ad5d85..1cdf5ed0856 100644 --- a/components/page-header/demo/actions.ts +++ b/components/page-header/demo/actions.ts @@ -7,9 +7,11 @@ import { Component } from '@angular/core'; Title This is a subtitle - - - + + + + + @@ -31,9 +33,11 @@ import { Component } from '@angular/core'; Runing - - - + + + + + diff --git a/components/page-header/demo/content.ts b/components/page-header/demo/content.ts index f10c7b6cf14..920e5ea801e 100644 --- a/components/page-header/demo/content.ts +++ b/components/page-header/demo/content.ts @@ -29,12 +29,22 @@ import { Component } from '@angular/core'; - - - - + + + + + +
  • 1st menu item length
  • diff --git a/components/page-header/demo/ghost.ts b/components/page-header/demo/ghost.ts index e679079bf37..31d49e4c924 100644 --- a/components/page-header/demo/ghost.ts +++ b/components/page-header/demo/ghost.ts @@ -8,9 +8,11 @@ import { Component } from '@angular/core'; Title This is a subtitle - - - + + + + + diff --git a/components/page-header/demo/module b/components/page-header/demo/module index e1bb3f1ac3d..5c27a397f95 100644 --- a/components/page-header/demo/module +++ b/components/page-header/demo/module @@ -10,6 +10,7 @@ import { NzDropDownModule } from 'ng-zorro-antd/dropdown'; import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzStatisticModule } from 'ng-zorro-antd/statistic'; import { NzGridModule } from 'ng-zorro-antd/grid'; +import { NzSpaceModule } from 'ng-zorro-antd/space'; export const moduleList = [ NzPageHeaderModule, @@ -23,5 +24,6 @@ export const moduleList = [ NzDropDownModule, NzIconModule, NzStatisticModule, - NzGridModule + NzGridModule, + NzSpaceModule ]; diff --git a/components/page-header/demo/responsive.ts b/components/page-header/demo/responsive.ts index fd0d551bd3c..0f258db9beb 100644 --- a/components/page-header/demo/responsive.ts +++ b/components/page-header/demo/responsive.ts @@ -7,9 +7,11 @@ import { Component } from '@angular/core'; Title This is a subtitle - - - + + + + +
    diff --git a/components/radio/radio.component.ts b/components/radio/radio.component.ts index e5b5fd924ee..f384c54d3ef 100644 --- a/components/radio/radio.component.ts +++ b/components/radio/radio.component.ts @@ -25,6 +25,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { fromEvent, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { NzFormStatusService } from 'ng-zorro-antd/core/form'; import { BooleanInput, NzSafeAny, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; import { InputBoolean } from 'ng-zorro-antd/core/util'; @@ -68,6 +69,7 @@ import { NzRadioService } from './radio.service'; } ], host: { + '[class.ant-radio-wrapper-in-form-item]': '!!nzFormStatusService', '[class.ant-radio-wrapper]': '!isRadioButton', '[class.ant-radio-button-wrapper]': 'isRadioButton', '[class.ant-radio-wrapper-checked]': 'isChecked && !isRadioButton', @@ -111,7 +113,8 @@ export class NzRadioComponent implements ControlValueAccessor, AfterViewInit, On private focusMonitor: FocusMonitor, @Optional() private directionality: Directionality, @Optional() @Inject(NzRadioService) private nzRadioService: NzRadioService | null, - @Optional() @Inject(NzRadioButtonDirective) private nzRadioButtonDirective: NzRadioButtonDirective | null + @Optional() @Inject(NzRadioButtonDirective) private nzRadioButtonDirective: NzRadioButtonDirective | null, + @Optional() public nzFormStatusService?: NzFormStatusService ) {} setDisabledState(disabled: boolean): void { diff --git a/components/select/select-arrow.component.ts b/components/select/select-arrow.component.ts index 9e2d6f0fc3f..f489bdc60b9 100644 --- a/components/select/select-arrow.component.ts +++ b/components/select/select-arrow.component.ts @@ -14,16 +14,17 @@ import { NzSafeAny } from 'ng-zorro-antd/core/types'; template: ` - + - + + {{ feedbackIcon }} `, host: { class: 'ant-select-arrow', @@ -33,7 +34,9 @@ import { NzSafeAny } from 'ng-zorro-antd/core/types'; export class NzSelectArrowComponent { @Input() loading = false; @Input() search = false; + @Input() showArrow = false; @Input() suffixIcon: TemplateRef | string | null = null; + @Input() feedbackIcon: TemplateRef | string | null = null; constructor() {} } diff --git a/components/select/select.component.ts b/components/select/select.component.ts index fba46f434cc..4130fbfd93e 100644 --- a/components/select/select.component.ts +++ b/components/select/select.component.ts @@ -33,11 +33,12 @@ import { ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { BehaviorSubject, combineLatest, fromEvent, merge } from 'rxjs'; -import { startWith, switchMap, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, fromEvent, merge, of as observableOf } from 'rxjs'; +import { distinctUntilChanged, map, startWith, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators'; import { slideMotion } from 'ng-zorro-antd/core/animation'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation'; import { cancelRequestAnimationFrame, reqAnimFrame } from 'ng-zorro-antd/core/polyfill'; import { NzDestroyService } from 'ng-zorro-antd/core/services'; @@ -46,6 +47,7 @@ import { NgClassInterface, NzSafeAny, NzStatus, + NzValidateStatus, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; @@ -108,11 +110,18 @@ export type NzSelectSizeType = 'large' | 'default' | 'small'; (keydown)="onKeyDown($event)" > + [feedbackIcon]="feedbackIconTpl" + > + + + + + { + return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; + }), + withLatestFrom(this.nzFormNoStatusService ? this.nzFormNoStatusService.noFormStatus : observableOf(false)), + map(([{ status, hasFeedback }, noStatus]) => ({ status: noStatus ? '' : status, hasFeedback })), + takeUntil(this.destroy$) + ) + .subscribe(({ status, hasFeedback }) => { + this.setStatusStyles(status, hasFeedback); + }); + this.focusMonitor .monitor(this.host, true) .pipe(takeUntil(this.destroy$)) @@ -714,9 +740,12 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon this.focusMonitor.stopMonitoring(this.host); } - private setStatusStyles(): void { + private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void { + this.status = status; + this.hasFeedback = hasFeedback; + this.cdr.markForCheck(); // render status if nzStatus is set - this.statusCls = getStatusClassNames(this.prefixCls, this.nzStatus, this.hasFeedback); + this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.host.nativeElement, status); diff --git a/components/select/select.module.ts b/components/select/select.module.ts index d6b8222b7e0..93a30a37f31 100644 --- a/components/select/select.module.ts +++ b/components/select/select.module.ts @@ -12,6 +12,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NzFormPatchModule } from 'ng-zorro-antd/core/form'; import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation'; import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; import { NzOverlayModule } from 'ng-zorro-antd/core/overlay'; @@ -47,6 +48,7 @@ import { NzSelectComponent } from './select.component'; NzOverlayModule, NzNoAnimationModule, NzTransitionPatchModule, + NzFormPatchModule, ScrollingModule, A11yModule ], diff --git a/components/select/select.spec.ts b/components/select/select.spec.ts index cee48a71fa8..b3d93ad45bd 100644 --- a/components/select/select.spec.ts +++ b/components/select/select.spec.ts @@ -13,6 +13,7 @@ import { ɵcreateComponentBed as createComponentBed } from 'ng-zorro-antd/core/testing'; import { NzSafeAny, NzStatus } from 'ng-zorro-antd/core/types'; +import { NzFormControlStatusType, NzFormModule } from 'ng-zorro-antd/form'; import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; import { NzSelectSearchComponent } from './select-search.component'; @@ -1296,6 +1297,40 @@ describe('select', () => { expect(selectElement.classList).not.toContain('ant-select-status-warning'); }); }); + describe('in form', () => { + let testBed: ComponentBed; + let component: TestSelectInFormComponent; + let fixture: ComponentFixture; + let selectElement!: HTMLElement; + + beforeEach(() => { + testBed = createComponentBed(TestSelectInFormComponent, { + imports: [NzSelectModule, NzIconTestModule, NzFormModule, FormsModule] + }); + component = testBed.component; + fixture = testBed.fixture; + selectElement = testBed.debugElement.query(By.directive(NzSelectComponent)).nativeElement; + }); + + it('should classname correct', () => { + fixture.detectChanges(); + expect(selectElement.classList).toContain('ant-select-status-error'); + expect(selectElement.classList).toContain('ant-select-in-form-item'); + expect(selectElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + + component.status = 'warning'; + fixture.detectChanges(); + expect(selectElement.classList).toContain('ant-select-status-warning'); + + component.status = 'success'; + fixture.detectChanges(); + expect(selectElement.classList).toContain('ant-select-status-success'); + + component.feedback = false; + fixture.detectChanges(); + expect(selectElement.querySelector('nz-form-item-feedback-icon')).toBeNull(); + }); + }); }); @Component({ @@ -1593,3 +1628,19 @@ export class TestSelectReactiveTagsComponent { export class TestSelectStatusComponent { status: NzStatus = 'error'; } + +@Component({ + template: ` +
    + + + + + +
    + ` +}) +export class TestSelectInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; +} diff --git a/components/time-picker/time-picker.component.spec.ts b/components/time-picker/time-picker.component.spec.ts index ffd4716f2c1..35b153db558 100644 --- a/components/time-picker/time-picker.component.spec.ts +++ b/components/time-picker/time-picker.component.spec.ts @@ -12,6 +12,7 @@ import { dispatchFakeEvent, dispatchMouseEvent, typeInElement } from 'ng-zorro-a import { NzStatus } from 'ng-zorro-antd/core/types'; import { PREFIX_CLASS } from 'ng-zorro-antd/date-picker'; import { getPickerInput, getPickerOkButton } from 'ng-zorro-antd/date-picker/testing/util'; +import { NzFormControlStatusType, NzFormModule } from 'ng-zorro-antd/form'; import { en_GB, NzI18nModule, NzI18nService } from '../i18n'; import { NzTimePickerComponent } from './time-picker.component'; @@ -26,9 +27,14 @@ describe('time-picker', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [BidiModule, NoopAnimationsModule, FormsModule, NzI18nModule, NzTimePickerModule], + imports: [BidiModule, NoopAnimationsModule, FormsModule, NzI18nModule, NzTimePickerModule, NzFormModule], schemas: [NO_ERRORS_SCHEMA], - declarations: [NzTestTimePickerComponent, NzTestTimePickerStatusComponent, NzTestTimePickerDirComponent] + declarations: [ + NzTestTimePickerComponent, + NzTestTimePickerStatusComponent, + NzTestTimePickerDirComponent, + NzTestTimePickerInFormComponent + ] }); TestBed.compileComponents(); inject([OverlayContainer], (oc: OverlayContainer) => { @@ -336,6 +342,35 @@ describe('time-picker', () => { }); }); + describe('time-picker in form', () => { + let testComponent: NzTestTimePickerInFormComponent; + let fixture: ComponentFixture; + let timeElement!: HTMLElement; + beforeEach(() => { + fixture = TestBed.createComponent(NzTestTimePickerInFormComponent); + testComponent = fixture.debugElement.componentInstance; + fixture.detectChanges(); + timeElement = fixture.debugElement.query(By.directive(NzTimePickerComponent)).nativeElement; + }); + it('should className correct', () => { + fixture.detectChanges(); + expect(timeElement.classList).toContain('ant-picker-status-error'); + expect(timeElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy(); + + testComponent.status = 'warning'; + fixture.detectChanges(); + expect(timeElement.classList).toContain('ant-picker-status-warning'); + + testComponent.status = 'success'; + fixture.detectChanges(); + expect(timeElement.classList).toContain('ant-picker-status-success'); + + testComponent.feedback = false; + fixture.detectChanges(); + expect(timeElement.querySelector('nz-form-item-feedback-icon')).toBeNull(); + }); + }); + function queryFromOverlay(selector: string): HTMLElement { return overlayContainerElement.querySelector(selector) as HTMLElement; } @@ -392,3 +427,19 @@ export class NzTestTimePickerStatusComponent { export class NzTestTimePickerDirComponent { dir: Direction = 'ltr'; } + +@Component({ + template: ` +
    + + + + + +
    + ` +}) +export class NzTestTimePickerInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; +} diff --git a/components/time-picker/time-picker.component.ts b/components/time-picker/time-picker.component.ts index 301776d9261..b6416e93f19 100644 --- a/components/time-picker/time-picker.component.ts +++ b/components/time-picker/time-picker.component.ts @@ -27,14 +27,15 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable, of, Subject } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { distinctUntilChanged, map, takeUntil, withLatestFrom } from 'rxjs/operators'; import { isValid } from 'date-fns'; import { slideMotion } from 'ng-zorro-antd/core/animation'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; import { warn } from 'ng-zorro-antd/core/logger'; -import { BooleanInput, NgClassInterface, NzSafeAny, NzStatus } from 'ng-zorro-antd/core/types'; +import { BooleanInput, NgClassInterface, NzSafeAny, NzStatus, NzValidateStatus } from 'ng-zorro-antd/core/types'; import { getStatusClassNames, InputBoolean, isNil } from 'ng-zorro-antd/core/util'; import { DateHelperService, NzI18nInterface, NzI18nService } from 'ng-zorro-antd/i18n'; @@ -66,6 +67,7 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'timePicker'; + @@ -179,12 +181,13 @@ export class NzTimePickerComponent implements ControlValueAccessor, OnInit, Afte // status prefixCls: string = 'ant-picker'; statusCls: NgClassInterface = {}; + status: NzValidateStatus = ''; hasFeedback: boolean = false; @ViewChild('inputElement', { static: true }) inputRef!: ElementRef; @Input() nzId: string | null = null; @Input() nzSize: string | null = null; - @Input() nzStatus?: NzStatus; + @Input() nzStatus: NzStatus = ''; @Input() @WithConfig() nzHourStep: number = 1; @Input() @WithConfig() nzMinuteStep: number = 1; @Input() @WithConfig() nzSecondStep: number = 1; @@ -329,10 +332,25 @@ export class NzTimePickerComponent implements ControlValueAccessor, OnInit, Afte private cdr: ChangeDetectorRef, private dateHelper: DateHelperService, private platform: Platform, - @Optional() private directionality: Directionality + @Optional() private directionality: Directionality, + @Optional() private nzFormStatusService?: NzFormStatusService, + @Optional() private nzFormNoStatusService?: NzFormNoStatusService ) {} ngOnInit(): void { + this.nzFormStatusService?.formStatusChanges + .pipe( + distinctUntilChanged((pre, cur) => { + return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; + }), + withLatestFrom(this.nzFormNoStatusService ? this.nzFormNoStatusService.noFormStatus : of(false)), + map(([{ status, hasFeedback }, noStatus]) => ({ status: noStatus ? '' : status, hasFeedback })), + takeUntil(this.destroy$) + ) + .subscribe(({ status, hasFeedback }) => { + this.setStatusStyles(status, hasFeedback); + }); + this.inputSize = Math.max(8, this.nzFormat.length) + 2; this.origin = new CdkOverlayOrigin(this.element); @@ -369,7 +387,7 @@ export class NzTimePickerComponent implements ControlValueAccessor, OnInit, Afte this.updateAutoFocus(); } if (nzStatus) { - this.setStatusStyles(); + this.setStatusStyles(this.nzStatus, this.hasFeedback); } } @@ -430,9 +448,13 @@ export class NzTimePickerComponent implements ControlValueAccessor, OnInit, Afte ); } - 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.hasFeedback); + this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.element.nativeElement, status); diff --git a/components/time-picker/time-picker.module.ts b/components/time-picker/time-picker.module.ts index 5f75550e7b7..1a51e2313e9 100644 --- a/components/time-picker/time-picker.module.ts +++ b/components/time-picker/time-picker.module.ts @@ -10,6 +10,7 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzFormPatchModule } from 'ng-zorro-antd/core/form'; import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; import { NzOverlayModule } from 'ng-zorro-antd/core/overlay'; import { NzI18nModule } from 'ng-zorro-antd/i18n'; @@ -30,7 +31,8 @@ import { NzTimePickerComponent } from './time-picker.component'; NzIconModule, NzOverlayModule, NzOutletModule, - NzButtonModule + NzButtonModule, + NzFormPatchModule ] }) export class NzTimePickerModule {} diff --git a/components/transfer/transfer.component.ts b/components/transfer/transfer.component.ts index 673c71f115c..b35c0bfa7c9 100644 --- a/components/transfer/transfer.component.ts +++ b/components/transfer/transfer.component.ts @@ -23,10 +23,18 @@ import { ViewChildren, ViewEncapsulation } from '@angular/core'; -import { Observable, of, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { Observable, of as observableOf, of, Subject } from 'rxjs'; +import { distinctUntilChanged, map, takeUntil, withLatestFrom } from 'rxjs/operators'; -import { BooleanInput, NgClassInterface, NgStyleInterface, NzSafeAny, NzStatus } from 'ng-zorro-antd/core/types'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; +import { + BooleanInput, + NgClassInterface, + NgStyleInterface, + NzSafeAny, + NzStatus, + NzValidateStatus +} from 'ng-zorro-antd/core/types'; import { getStatusClassNames, InputBoolean, toArray } from 'ng-zorro-antd/core/util'; import { NzI18nService, NzTransferI18nInterface } from 'ng-zorro-antd/i18n'; @@ -188,7 +196,7 @@ export class NzTransferComponent implements OnInit, OnChanges, OnDestroy { @Input() nzNotFoundContent?: string; @Input() nzTargetKeys: string[] = []; @Input() nzSelectedKeys: string[] = []; - @Input() nzStatus?: NzStatus; + @Input() nzStatus: NzStatus = ''; // events @Output() readonly nzChange = new EventEmitter(); @@ -296,7 +304,9 @@ export class NzTransferComponent implements OnInit, OnChanges, OnDestroy { private i18n: NzI18nService, private elementRef: ElementRef, private renderer: Renderer2, - @Optional() private directionality: Directionality + @Optional() private directionality: Directionality, + @Optional() private nzFormStatusService?: NzFormStatusService, + @Optional() private nzFormNoStatusService?: NzFormNoStatusService ) {} private markForCheckAllList(): void { @@ -330,6 +340,19 @@ export class NzTransferComponent implements OnInit, OnChanges, OnDestroy { } ngOnInit(): void { + this.nzFormStatusService?.formStatusChanges + .pipe( + distinctUntilChanged((pre, cur) => { + return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; + }), + withLatestFrom(this.nzFormNoStatusService ? this.nzFormNoStatusService.noFormStatus : observableOf(false)), + map(([{ status, hasFeedback }, noStatus]) => ({ status: noStatus ? '' : status, hasFeedback })), + takeUntil(this.unsubscribe$) + ) + .subscribe(({ status, hasFeedback }) => { + this.setStatusStyles(status, hasFeedback); + }); + this.i18n.localeChange.pipe(takeUntil(this.unsubscribe$)).subscribe(() => { this.locale = this.i18n.getLocaleData('Transfer'); this.markForCheckAllList(); @@ -358,7 +381,7 @@ export class NzTransferComponent implements OnInit, OnChanges, OnDestroy { this.handleNzSelectedKeys(); } if (nzStatus) { - this.setStatusStyles(); + this.setStatusStyles(this.nzStatus, this.hasFeedback); } } @@ -367,9 +390,12 @@ export class NzTransferComponent implements OnInit, OnChanges, OnDestroy { this.unsubscribe$.complete(); } - private setStatusStyles(): void { + private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void { + // set inner status + this.hasFeedback = hasFeedback; + this.cdr.markForCheck(); // render status if nzStatus is set - this.statusCls = getStatusClassNames(this.prefixCls, this.nzStatus, this.hasFeedback); + this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.elementRef.nativeElement, status); diff --git a/components/transfer/transfer.spec.ts b/components/transfer/transfer.spec.ts index 4eb7f014f00..c078711bfa4 100644 --- a/components/transfer/transfer.spec.ts +++ b/components/transfer/transfer.spec.ts @@ -11,12 +11,14 @@ import { ViewEncapsulation } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { NzStatus } from 'ng-zorro-antd/core/types'; +import { NzFormControlStatusType, NzFormModule } from 'ng-zorro-antd/form'; import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; import en_US from '../i18n/languages/en_US'; @@ -36,19 +38,21 @@ describe('transfer', () => { | Test996Component | NzTestTransferRtlComponent | NzTestTransferStatusComponent + | NzTestTransferInFormComponent >; let dl: DebugElement; let instance: TestTransferComponent; let pageObject: TransferPageObject; beforeEach(() => { injector = TestBed.configureTestingModule({ - imports: [BidiModule, NoopAnimationsModule, NzTransferModule, NzIconTestModule], + imports: [BidiModule, NoopAnimationsModule, NzTransferModule, NzIconTestModule, FormsModule, NzFormModule], declarations: [ TestTransferComponent, TestTransferCustomRenderComponent, Test996Component, NzTestTransferRtlComponent, - NzTestTransferStatusComponent + NzTestTransferStatusComponent, + NzTestTransferInFormComponent ] }); fixture = TestBed.createComponent(TestTransferComponent); @@ -405,6 +409,35 @@ describe('transfer', () => { }); }); + describe('transfer in form', () => { + let componentElement: HTMLElement; + let testComponent: NzTestTransferInFormComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestTransferInFormComponent); + componentElement = fixture.debugElement.query(By.directive(NzTransferComponent)).nativeElement; + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + }); + + it('should className correct', () => { + fixture.detectChanges(); + expect(componentElement.classList).toContain('ant-transfer-status-error'); + + testComponent.status = 'warning'; + fixture.detectChanges(); + expect(componentElement.classList).toContain('ant-transfer-status-warning'); + + testComponent.status = 'success'; + fixture.detectChanges(); + expect(componentElement.classList).toContain('ant-transfer-status-success'); + + testComponent.feedback = false; + fixture.detectChanges(); + expect(componentElement.classList).not.toContain('ant-transfer-has-feedback'); + }); + }); + class TransferPageObject { [key: string]: any; @@ -648,3 +681,19 @@ export class NzTestTransferRtlComponent { export class NzTestTransferStatusComponent { status: NzStatus = 'error'; } + +@Component({ + template: ` +
    + + + + + +
    + ` +}) +export class NzTestTransferInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; +} diff --git a/components/tree-select/tree-select.component.ts b/components/tree-select/tree-select.component.ts index 13bcd9a2a76..ef36b333d9c 100644 --- a/components/tree-select/tree-select.component.ts +++ b/components/tree-select/tree-select.component.ts @@ -30,10 +30,11 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { merge, of as observableOf, Subject } from 'rxjs'; -import { filter, takeUntil, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, takeUntil, tap, withLatestFrom } from 'rxjs/operators'; import { slideMotion } from 'ng-zorro-antd/core/animation'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; +import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form'; import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation'; import { reqAnimFrame } from 'ng-zorro-antd/core/polyfill'; import { @@ -50,6 +51,7 @@ import { NgStyleInterface, NzSizeLDSType, NzStatus, + NzValidateStatus, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; @@ -180,7 +182,15 @@ const TREE_SELECT_DEFAULT_CLASS = 'ant-select-dropdown ant-select-tree-dropdown' > - + + + + + ; @Input() nzNotFoundContent?: string; - @Input() nzNodes: Array = []; + @Input() nzNodes: NzTreeNodeOptions[] | NzTreeNode[] = []; @Input() nzOpen = false; @Input() @WithConfig() nzSize: NzSizeLDSType = 'default'; @Input() nzPlaceHolder = ''; @@ -296,7 +307,8 @@ export class NzTreeSelectComponent extends NzTreeBase implements ControlValueAcc prefixCls: string = 'ant-select'; statusCls: NgClassInterface = {}; - nzHasFeedback: boolean = false; + status: NzValidateStatus = ''; + hasFeedback: boolean = false; dropdownClassName = TREE_SELECT_DEFAULT_CLASS; triggerWidth?: number; @@ -332,7 +344,9 @@ export class NzTreeSelectComponent extends NzTreeBase implements ControlValueAcc private elementRef: ElementRef, @Optional() private directionality: Directionality, private focusMonitor: FocusMonitor, - @Host() @Optional() public noAnimation?: NzNoAnimationDirective + @Host() @Optional() public noAnimation?: NzNoAnimationDirective, + @Optional() public nzFormStatusService?: NzFormStatusService, + @Optional() private nzFormNoStatusService?: NzFormNoStatusService ) { super(nzTreeService); @@ -341,6 +355,19 @@ export class NzTreeSelectComponent extends NzTreeBase implements ControlValueAcc } ngOnInit(): void { + this.nzFormStatusService?.formStatusChanges + .pipe( + distinctUntilChanged((pre, cur) => { + return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback; + }), + withLatestFrom(this.nzFormNoStatusService ? this.nzFormNoStatusService.noFormStatus : observableOf(false)), + map(([{ status, hasFeedback }, noStatus]) => ({ status: noStatus ? '' : status, hasFeedback })), + takeUntil(this.destroy$) + ) + .subscribe(({ status, hasFeedback }) => { + this.setStatusStyles(status, hasFeedback); + }); + this.isDestroy = false; this.subscribeSelectionChange(); @@ -383,9 +410,13 @@ export class NzTreeSelectComponent extends NzTreeBase implements ControlValueAcc this.closeDropDown(); } - 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); Object.keys(this.statusCls).forEach(status => { if (this.statusCls[status]) { this.renderer.addClass(this.elementRef.nativeElement, status); @@ -405,7 +436,7 @@ export class NzTreeSelectComponent extends NzTreeBase implements ControlValueAcc this.dropdownClassName = className ? `${TREE_SELECT_DEFAULT_CLASS} ${className}` : TREE_SELECT_DEFAULT_CLASS; } if (nzStatus) { - this.setStatusStyles(); + this.setStatusStyles(this.nzStatus, this.hasFeedback); } } diff --git a/components/tree-select/tree-select.module.ts b/components/tree-select/tree-select.module.ts index f609c7574a8..583652df2ae 100644 --- a/components/tree-select/tree-select.module.ts +++ b/components/tree-select/tree-select.module.ts @@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { NzFormPatchModule } from 'ng-zorro-antd/core/form'; import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation'; import { NzOverlayModule } from 'ng-zorro-antd/core/overlay'; import { NzEmptyModule } from 'ng-zorro-antd/empty'; @@ -29,7 +30,8 @@ import { NzTreeSelectComponent } from './tree-select.component'; NzIconModule, NzEmptyModule, NzOverlayModule, - NzNoAnimationModule + NzNoAnimationModule, + NzFormPatchModule ], declarations: [NzTreeSelectComponent], exports: [NzTreeSelectComponent] diff --git a/components/tree-select/tree-select.spec.ts b/components/tree-select/tree-select.spec.ts index 96c7cd91ea2..bc59822d868 100644 --- a/components/tree-select/tree-select.spec.ts +++ b/components/tree-select/tree-select.spec.ts @@ -17,6 +17,7 @@ import { } from 'ng-zorro-antd/core/testing'; import { NzTreeNode, NzTreeNodeOptions } from 'ng-zorro-antd/core/tree'; import { NzStatus } from 'ng-zorro-antd/core/types'; +import { NzFormControlStatusType, NzFormModule } from 'ng-zorro-antd/form'; import { NzTreeSelectComponent } from './tree-select.component'; import { NzTreeSelectModule } from './tree-select.module'; @@ -29,13 +30,14 @@ describe('tree-select component', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - imports: [NzTreeSelectModule, NoopAnimationsModule, FormsModule, ReactiveFormsModule], + imports: [NzTreeSelectModule, NoopAnimationsModule, FormsModule, ReactiveFormsModule, NzFormModule], declarations: [ NzTestTreeSelectBasicComponent, NzTestTreeSelectCheckableComponent, NzTestTreeSelectFormComponent, NzTestTreeSelectCustomizedIconComponent, - NzTestTreeSelectStatusComponent + NzTestTreeSelectStatusComponent, + NzTestTreeSelectInFormComponent ], providers: [ { @@ -631,6 +633,33 @@ describe('tree-select component', () => { expect(treeSelect.nativeElement.className).not.toContain('ant-select-status-warning'); }); }); + + describe('in form', () => { + let fixture: ComponentFixture; + let treeSelect!: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestTreeSelectInFormComponent); + treeSelect = fixture.debugElement.query(By.directive(NzTreeSelectComponent)).nativeElement; + }); + + it('should className correct', () => { + fixture.detectChanges(); + expect(treeSelect.classList).toContain('ant-select-status-error'); + + fixture.componentInstance.status = 'warning'; + fixture.detectChanges(); + expect(treeSelect.classList).toContain('ant-select-status-warning'); + + fixture.componentInstance.status = 'success'; + fixture.detectChanges(); + expect(treeSelect.classList).toContain('ant-select-status-success'); + + fixture.componentInstance.feedback = false; + fixture.detectChanges(); + expect(treeSelect.querySelector('nz-form-item-feedback-icon')).toBeNull(); + }); + }); }); @Component({ @@ -926,3 +955,19 @@ export class NzTestTreeSelectStatusComponent { } ]; } + +@Component({ + template: ` +
    + + + + + +
    + ` +}) +export class NzTestTreeSelectInFormComponent { + status: NzFormControlStatusType = 'error'; + feedback = true; +}