Skip to content

Commit

Permalink
feat(module:form): make form work with status (#7489)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
simplejason authored Jun 15, 2022
1 parent 23a2fd5 commit 98ac620
Show file tree
Hide file tree
Showing 60 changed files with 1,400 additions and 182 deletions.
42 changes: 33 additions & 9 deletions components/cascader/cascader.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -115,6 +117,7 @@ const defaultDisplayRender = (labels: string[]): string => labels.join(' / ');
[class.ant-cascader-picker-arrow-expand]="menuVisible"
></i>
<i *ngIf="isLoading" nz-icon nzType="loading"></i>
<nz-form-item-feedback-icon *ngIf="hasFeedback && !!status" [status]="status"></nz-form-item-feedback-icon>
</span>
<span class="ant-select-clear" *ngIf="clearIconVisible">
<i nz-icon nzType="close-circle" nzTheme="fill" (click)="clearSelection($event)"></i>
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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(() => {
Expand Down Expand Up @@ -450,7 +470,7 @@ export class NzCascaderComponent
ngOnChanges(changes: SimpleChanges): void {
const { nzStatus } = changes;
if (nzStatus) {
this.setStatusStyles();
this.setStatusStyles(this.nzStatus, this.hasFeedback);
}
}

Expand Down Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion components/cascader/cascader.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,7 +33,8 @@ import { NzCascaderComponent } from './cascader.component';
NzIconModule,
NzInputModule,
NzNoAnimationModule,
NzOverlayModule
NzOverlayModule,
NzFormPatchModule
],
declarations: [NzCascaderComponent, NzCascaderOptionComponent],
exports: [NzCascaderComponent]
Expand Down
67 changes: 64 additions & 3 deletions components/cascader/cascader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand Down Expand Up @@ -67,13 +68,15 @@ describe('cascader', () => {
ReactiveFormsModule,
NoopAnimationsModule,
NzCascaderModule,
NzIconTestModule
NzIconTestModule,
NzFormModule
],
declarations: [
NzDemoCascaderDefaultComponent,
NzDemoCascaderLoadDataComponent,
NzDemoCascaderRtlComponent,
NzDemoCascaderStatusComponent
NzDemoCascaderStatusComponent,
NzDemoCascaderInFormComponent
]
}).compileComponents();

Expand Down Expand Up @@ -1815,6 +1818,45 @@ describe('cascader', () => {
expect(cascader.nativeElement.className).not.toContain('ant-select-status-warning');
});
});
describe('In form', () => {
let fixture: ComponentFixture<NzDemoCascaderInFormComponent>;
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 = [
Expand Down Expand Up @@ -2208,3 +2250,22 @@ export class NzDemoCascaderStatusComponent {
public nzOptions: any[] | null = options1;
public status: NzStatus = 'error';
}

@Component({
template: `
<form nz-form [formGroup]="validateForm">
<nz-form-item>
<nz-form-control nzHasFeedback>
<nz-cascader formControlName="demo" [nzOptions]="nzOptions"></nz-cascader>
</nz-form-control>
</nz-form-item>
</form>
`
})
export class NzDemoCascaderInFormComponent {
validateForm: FormGroup = this.fb.group({
demo: [null, [Validators.required]]
});
public nzOptions: any[] | null = options1;
constructor(private fb: FormBuilder) {}
}
5 changes: 4 additions & 1 deletion components/checkbox/checkbox.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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'`
}
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions components/core/form/index.ts
Original file line number Diff line number Diff line change
@@ -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';
5 changes: 5 additions & 0 deletions components/core/form/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "public-api.ts"
}
}
54 changes: 54 additions & 0 deletions components/core/form/nz-form-item-feedback-icon.component.ts
Original file line number Diff line number Diff line change
@@ -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: ` <i *ngIf="iconType" nz-icon [nzType]="iconType"></i> `,
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();
}
}
54 changes: 54 additions & 0 deletions components/core/form/nz-form-item-feedback-icon.spec.ts
Original file line number Diff line number Diff line change
@@ -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<NzTestFormItemFeedbackIconComponent>;
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: ` <nz-form-item-feedback-icon [status]="status"></nz-form-item-feedback-icon> `
})
export class NzTestFormItemFeedbackIconComponent {
status: NzValidateStatus = '';
}
Loading

0 comments on commit 98ac620

Please sign in to comment.