Skip to content

Commit 98ac620

Browse files
authored
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
1 parent 23a2fd5 commit 98ac620

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1400
-182
lines changed

components/cascader/cascader.component.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ import {
3131
ViewEncapsulation
3232
} from '@angular/core';
3333
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
34-
import { BehaviorSubject, EMPTY, fromEvent, Observable } from 'rxjs';
35-
import { startWith, switchMap, takeUntil } from 'rxjs/operators';
34+
import { BehaviorSubject, EMPTY, fromEvent, Observable, of as observableOf } from 'rxjs';
35+
import { distinctUntilChanged, map, startWith, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
3636

3737
import { slideMotion } from 'ng-zorro-antd/core/animation';
3838
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
39+
import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form';
3940
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
4041
import { DEFAULT_CASCADER_POSITIONS } from 'ng-zorro-antd/core/overlay';
4142
import { NzDestroyService } from 'ng-zorro-antd/core/services';
@@ -45,7 +46,8 @@ import {
4546
NgClassType,
4647
NgStyleInterface,
4748
NzSafeAny,
48-
NzStatus
49+
NzStatus,
50+
NzValidateStatus
4951
} from 'ng-zorro-antd/core/types';
5052
import { getStatusClassNames, InputBoolean, toArray } from 'ng-zorro-antd/core/util';
5153
import { NzCascaderI18nInterface, NzI18nService } from 'ng-zorro-antd/i18n';
@@ -115,6 +117,7 @@ const defaultDisplayRender = (labels: string[]): string => labels.join(' / ');
115117
[class.ant-cascader-picker-arrow-expand]="menuVisible"
116118
></i>
117119
<i *ngIf="isLoading" nz-icon nzType="loading"></i>
120+
<nz-form-item-feedback-icon *ngIf="hasFeedback && !!status" [status]="status"></nz-form-item-feedback-icon>
118121
</span>
119122
<span class="ant-select-clear" *ngIf="clearIconVisible">
120123
<i nz-icon nzType="close-circle" nzTheme="fill" (click)="clearSelection($event)"></i>
@@ -207,6 +210,7 @@ const defaultDisplayRender = (labels: string[]): string => labels.join(' / ');
207210
],
208211
host: {
209212
'[attr.tabIndex]': '"0"',
213+
'[class.ant-select-in-form-item]': '!!nzFormStatusService',
210214
'[class.ant-select-lg]': 'nzSize === "large"',
211215
'[class.ant-select-sm]': 'nzSize === "small"',
212216
'[class.ant-select-allow-clear]': 'nzAllowClear',
@@ -267,7 +271,7 @@ export class NzCascaderComponent
267271
@Input() nzMenuStyle: NgStyleInterface | null = null;
268272
@Input() nzMouseEnterDelay: number = 150; // ms
269273
@Input() nzMouseLeaveDelay: number = 150; // ms
270-
@Input() nzStatus?: NzStatus;
274+
@Input() nzStatus: NzStatus = '';
271275

272276
@Input() nzTriggerAction: NzCascaderTriggerType | NzCascaderTriggerType[] = ['click'] as NzCascaderTriggerType[];
273277
@Input() nzChangeOn?: (option: NzCascaderOption, level: number) => boolean;
@@ -292,7 +296,8 @@ export class NzCascaderComponent
292296

293297
prefixCls: string = 'ant-select';
294298
statusCls: NgClassInterface = {};
295-
nzHasFeedback: boolean = false;
299+
status: NzValidateStatus = '';
300+
hasFeedback: boolean = false;
296301

297302
/**
298303
* If the dropdown should show the empty content.
@@ -379,7 +384,9 @@ export class NzCascaderComponent
379384
private elementRef: ElementRef,
380385
private renderer: Renderer2,
381386
@Optional() private directionality: Directionality,
382-
@Host() @Optional() public noAnimation?: NzNoAnimationDirective
387+
@Host() @Optional() public noAnimation?: NzNoAnimationDirective,
388+
@Optional() public nzFormStatusService?: NzFormStatusService,
389+
@Optional() private nzFormNoStatusService?: NzFormNoStatusService
383390
) {
384391
this.el = elementRef.nativeElement;
385392
this.cascaderService.withComponent(this);
@@ -388,6 +395,19 @@ export class NzCascaderComponent
388395
}
389396

390397
ngOnInit(): void {
398+
this.nzFormStatusService?.formStatusChanges
399+
.pipe(
400+
distinctUntilChanged((pre, cur) => {
401+
return pre.status === cur.status && pre.hasFeedback === cur.hasFeedback;
402+
}),
403+
withLatestFrom(this.nzFormNoStatusService ? this.nzFormNoStatusService.noFormStatus : observableOf(false)),
404+
map(([{ status, hasFeedback }, noStatus]) => ({ status: noStatus ? '' : status, hasFeedback })),
405+
takeUntil(this.destroy$)
406+
)
407+
.subscribe(({ status, hasFeedback }) => {
408+
this.setStatusStyles(status, hasFeedback);
409+
});
410+
391411
const srv = this.cascaderService;
392412

393413
srv.$redraw.pipe(takeUntil(this.destroy$)).subscribe(() => {
@@ -450,7 +470,7 @@ export class NzCascaderComponent
450470
ngOnChanges(changes: SimpleChanges): void {
451471
const { nzStatus } = changes;
452472
if (nzStatus) {
453-
this.setStatusStyles();
473+
this.setStatusStyles(this.nzStatus, this.hasFeedback);
454474
}
455475
}
456476

@@ -785,9 +805,13 @@ export class NzCascaderComponent
785805
}
786806
}
787807

788-
private setStatusStyles(): void {
808+
private setStatusStyles(status: NzValidateStatus, hasFeedback: boolean): void {
809+
// set inner status
810+
this.status = status;
811+
this.hasFeedback = hasFeedback;
812+
this.cdr.markForCheck();
789813
// render status if nzStatus is set
790-
this.statusCls = getStatusClassNames(this.prefixCls, this.nzStatus, this.nzHasFeedback);
814+
this.statusCls = getStatusClassNames(this.prefixCls, status, hasFeedback);
791815
Object.keys(this.statusCls).forEach(status => {
792816
if (this.statusCls[status]) {
793817
this.renderer.addClass(this.elementRef.nativeElement, status);

components/cascader/cascader.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common';
99
import { NgModule } from '@angular/core';
1010
import { FormsModule } from '@angular/forms';
1111

12+
import { NzFormPatchModule } from 'ng-zorro-antd/core/form';
1213
import { NzHighlightModule } from 'ng-zorro-antd/core/highlight';
1314
import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation';
1415
import { NzOutletModule } from 'ng-zorro-antd/core/outlet';
@@ -32,7 +33,8 @@ import { NzCascaderComponent } from './cascader.component';
3233
NzIconModule,
3334
NzInputModule,
3435
NzNoAnimationModule,
35-
NzOverlayModule
36+
NzOverlayModule,
37+
NzFormPatchModule
3638
],
3739
declarations: [NzCascaderComponent, NzCascaderOptionComponent],
3840
exports: [NzCascaderComponent]

components/cascader/cascader.spec.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { OverlayContainer } from '@angular/cdk/overlay';
2222
import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core';
2323
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
24-
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
24+
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
2525
import { By } from '@angular/platform-browser';
2626
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
2727

@@ -34,6 +34,7 @@ import {
3434
import { NzStatus } from 'ng-zorro-antd/core/types';
3535
import { NzIconTestModule } from 'ng-zorro-antd/icon/testing';
3636

37+
import { NzFormModule } from '../form';
3738
import { NzCascaderComponent } from './cascader.component';
3839
import { NzCascaderModule } from './cascader.module';
3940
import { NzCascaderOption, NzShowSearchOptions } from './typings';
@@ -67,13 +68,15 @@ describe('cascader', () => {
6768
ReactiveFormsModule,
6869
NoopAnimationsModule,
6970
NzCascaderModule,
70-
NzIconTestModule
71+
NzIconTestModule,
72+
NzFormModule
7173
],
7274
declarations: [
7375
NzDemoCascaderDefaultComponent,
7476
NzDemoCascaderLoadDataComponent,
7577
NzDemoCascaderRtlComponent,
76-
NzDemoCascaderStatusComponent
78+
NzDemoCascaderStatusComponent,
79+
NzDemoCascaderInFormComponent
7780
]
7881
}).compileComponents();
7982

@@ -1815,6 +1818,45 @@ describe('cascader', () => {
18151818
expect(cascader.nativeElement.className).not.toContain('ant-select-status-warning');
18161819
});
18171820
});
1821+
describe('In form', () => {
1822+
let fixture: ComponentFixture<NzDemoCascaderInFormComponent>;
1823+
let formGroup: FormGroup;
1824+
let cascader: DebugElement;
1825+
1826+
beforeEach(() => {
1827+
fixture = TestBed.createComponent(NzDemoCascaderInFormComponent);
1828+
cascader = fixture.debugElement.query(By.directive(NzCascaderComponent));
1829+
formGroup = fixture.componentInstance.validateForm;
1830+
fixture.detectChanges();
1831+
});
1832+
1833+
it('should className correct', () => {
1834+
expect(cascader.nativeElement.className).not.toContain('ant-select-status-error');
1835+
expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeNull();
1836+
formGroup.get('demo')!.markAsDirty();
1837+
formGroup.get('demo')!.setValue(null);
1838+
formGroup.get('demo')!.updateValueAndValidity();
1839+
fixture.detectChanges();
1840+
1841+
// show error
1842+
expect(cascader.nativeElement.className).toContain('ant-select-status-error');
1843+
expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy();
1844+
expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon').className).toContain(
1845+
'ant-form-item-feedback-icon-error'
1846+
);
1847+
1848+
formGroup.get('demo')!.markAsDirty();
1849+
formGroup.get('demo')!.setValue(['a', 'b']);
1850+
formGroup.get('demo')!.updateValueAndValidity();
1851+
fixture.detectChanges();
1852+
// show success
1853+
expect(cascader.nativeElement.className).toContain('ant-select-status-success');
1854+
expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon')).toBeTruthy();
1855+
expect(cascader.nativeElement.querySelector('nz-form-item-feedback-icon').className).toContain(
1856+
'ant-form-item-feedback-icon-success'
1857+
);
1858+
});
1859+
});
18181860
});
18191861

18201862
const ID_NAME_LIST = [
@@ -2208,3 +2250,22 @@ export class NzDemoCascaderStatusComponent {
22082250
public nzOptions: any[] | null = options1;
22092251
public status: NzStatus = 'error';
22102252
}
2253+
2254+
@Component({
2255+
template: `
2256+
<form nz-form [formGroup]="validateForm">
2257+
<nz-form-item>
2258+
<nz-form-control nzHasFeedback>
2259+
<nz-cascader formControlName="demo" [nzOptions]="nzOptions"></nz-cascader>
2260+
</nz-form-control>
2261+
</nz-form-item>
2262+
</form>
2263+
`
2264+
})
2265+
export class NzDemoCascaderInFormComponent {
2266+
validateForm: FormGroup = this.fb.group({
2267+
demo: [null, [Validators.required]]
2268+
});
2269+
public nzOptions: any[] | null = options1;
2270+
constructor(private fb: FormBuilder) {}
2271+
}

components/checkbox/checkbox.component.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
2626
import { fromEvent, Subject } from 'rxjs';
2727
import { takeUntil } from 'rxjs/operators';
2828

29+
import { NzFormStatusService } from 'ng-zorro-antd/core/form';
2930
import { BooleanInput, NzSafeAny, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types';
3031
import { InputBoolean } from 'ng-zorro-antd/core/util';
3132

@@ -68,6 +69,7 @@ import { NzCheckboxWrapperComponent } from './checkbox-wrapper.component';
6869
],
6970
host: {
7071
class: 'ant-checkbox-wrapper',
72+
'[class.ant-checkbox-wrapper-in-form-item]': '!!nzFormStatusService',
7173
'[class.ant-checkbox-wrapper-checked]': 'nzChecked',
7274
'[class.ant-checkbox-rtl]': `dir === 'rtl'`
7375
}
@@ -135,7 +137,8 @@ export class NzCheckboxComponent implements OnInit, ControlValueAccessor, OnDest
135137
@Optional() private nzCheckboxWrapperComponent: NzCheckboxWrapperComponent,
136138
private cdr: ChangeDetectorRef,
137139
private focusMonitor: FocusMonitor,
138-
@Optional() private directionality: Directionality
140+
@Optional() private directionality: Directionality,
141+
@Optional() public nzFormStatusService?: NzFormStatusService
139142
) {}
140143

141144
ngOnInit(): void {

components/core/form/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Use of this source code is governed by an MIT-style license that can be
3+
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
4+
*/
5+
6+
export * from './public-api';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "public-api.ts"
4+
}
5+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Use of this source code is governed by an MIT-style license that can be
3+
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
4+
*/
5+
6+
import {
7+
ChangeDetectionStrategy,
8+
ChangeDetectorRef,
9+
Component,
10+
Input,
11+
OnChanges,
12+
SimpleChanges,
13+
ViewEncapsulation
14+
} from '@angular/core';
15+
16+
import { NzValidateStatus } from 'ng-zorro-antd/core/types';
17+
18+
const iconTypeMap = {
19+
error: 'close-circle-fill',
20+
validating: 'loading',
21+
success: 'check-circle-fill',
22+
warning: 'exclamation-circle-fill'
23+
} as const;
24+
25+
@Component({
26+
selector: 'nz-form-item-feedback-icon',
27+
exportAs: 'nzFormFeedbackIcon',
28+
preserveWhitespaces: false,
29+
encapsulation: ViewEncapsulation.None,
30+
changeDetection: ChangeDetectionStrategy.OnPush,
31+
template: ` <i *ngIf="iconType" nz-icon [nzType]="iconType"></i> `,
32+
host: {
33+
class: 'ant-form-item-feedback-icon',
34+
'[class.ant-form-item-feedback-icon-error]': 'status==="error"',
35+
'[class.ant-form-item-feedback-icon-warning]': 'status==="warning"',
36+
'[class.ant-form-item-feedback-icon-success]': 'status==="success"',
37+
'[class.ant-form-item-feedback-icon-validating]': 'status==="validating"'
38+
}
39+
})
40+
export class NzFormItemFeedbackIconComponent implements OnChanges {
41+
@Input() status: NzValidateStatus = '';
42+
constructor(public cdr: ChangeDetectorRef) {}
43+
44+
iconType: typeof iconTypeMap[keyof typeof iconTypeMap] | null = null;
45+
46+
ngOnChanges(_changes: SimpleChanges): void {
47+
this.updateIcon();
48+
}
49+
50+
updateIcon(): void {
51+
this.iconType = this.status ? iconTypeMap[this.status] : null;
52+
this.cdr.markForCheck();
53+
}
54+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Component, DebugElement } from '@angular/core';
2+
import { By } from '@angular/platform-browser';
3+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4+
5+
import { NzFormPatchModule } from 'ng-zorro-antd/core/form/nz-form-patch.module';
6+
import { ɵComponentBed as ComponentBed, ɵcreateComponentBed as createComponentBed } from 'ng-zorro-antd/core/testing';
7+
import { NzValidateStatus } from 'ng-zorro-antd/core/types';
8+
9+
import { NzFormItemFeedbackIconComponent } from './nz-form-item-feedback-icon.component';
10+
11+
const testBedOptions = { imports: [NzFormPatchModule, NoopAnimationsModule] };
12+
13+
describe('nz-form-item-feedback-icon', () => {
14+
describe('default', () => {
15+
let testBed: ComponentBed<NzTestFormItemFeedbackIconComponent>;
16+
let fixtureInstance: NzTestFormItemFeedbackIconComponent;
17+
let feedback: DebugElement;
18+
beforeEach(() => {
19+
testBed = createComponentBed(NzTestFormItemFeedbackIconComponent, testBedOptions);
20+
fixtureInstance = testBed.fixture.componentInstance;
21+
feedback = testBed.fixture.debugElement.query(By.directive(NzFormItemFeedbackIconComponent));
22+
testBed.fixture.detectChanges();
23+
});
24+
it('should className correct', () => {
25+
expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon');
26+
fixtureInstance.status = 'success';
27+
testBed.fixture.detectChanges();
28+
expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon-success');
29+
expect(feedback.nativeElement.querySelector('.anticon-check-circle-fill')).toBeTruthy();
30+
31+
fixtureInstance.status = 'error';
32+
testBed.fixture.detectChanges();
33+
expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon-error');
34+
expect(feedback.nativeElement.querySelector('.anticon-close-circle-fill')).toBeTruthy();
35+
36+
fixtureInstance.status = 'warning';
37+
testBed.fixture.detectChanges();
38+
expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon-warning');
39+
expect(feedback.nativeElement.querySelector('.anticon-exclamation-circle-fill')).toBeTruthy();
40+
41+
fixtureInstance.status = 'validating';
42+
testBed.fixture.detectChanges();
43+
expect(feedback.nativeElement.classList).toContain('ant-form-item-feedback-icon-validating');
44+
expect(feedback.nativeElement.querySelector('.anticon-loading')).toBeTruthy();
45+
});
46+
});
47+
});
48+
49+
@Component({
50+
template: ` <nz-form-item-feedback-icon [status]="status"></nz-form-item-feedback-icon> `
51+
})
52+
export class NzTestFormItemFeedbackIconComponent {
53+
status: NzValidateStatus = '';
54+
}

0 commit comments

Comments
 (0)