Skip to content

Commit

Permalink
feat(module:form): support auto error tips (#4888)
Browse files Browse the repository at this point in the history
close #4523
  • Loading branch information
danranVm committed Apr 15, 2020
1 parent b6f3bda commit 0b85483
Show file tree
Hide file tree
Showing 8 changed files with 529 additions and 40 deletions.
1 change: 1 addition & 0 deletions components/core/config/config.ts
Expand Up @@ -140,6 +140,7 @@ export interface EmptyConfig {

export interface FormConfig {
nzNoColon?: boolean;
nzTipOptions?: Record<string, Record<string, string>>;
}

export interface IconConfig {
Expand Down
28 changes: 28 additions & 0 deletions components/form/demo/auto-tips.md
@@ -0,0 +1,28 @@
---
order: 11
title:
zh-CN: 自动提示
en-US: Auto tips
---

## zh-CN

让提示变得更简单。
需要预先自定义 `Validators` 和提供 `nzTipOptions`,它们优先级如下:

- `Validators` > `nzTipOptions`
- 通过 `@Input` 设置 `nzTipOptions`
- 通过全局配置设置 `nzTipOptions`

另外,你可以使用 `nzDisableAutoTips` 去禁用它。

## en-US

Make tips to be easy.
Need to customize `Validators` and provide `nzTipOptions` in advance, the priority is as follows:

- `Validators` > `nzTipOptions`
- Via `@Input` set `nzTipOptions`
- Via global config set `nzTipOptions`

In addition, you can use `nzDisableAutoTips` to disable it.
166 changes: 166 additions & 0 deletions components/form/demo/auto-tips.ts
@@ -0,0 +1,166 @@
import { Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { Observable, Observer } from 'rxjs';

@Component({
selector: 'nz-demo-form-auto-tips',
template: `
<form nz-form [nzTipOptions]="tipOptions" [formGroup]="validateForm" (ngSubmit)="submitForm(validateForm.value)">
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>Username</nz-form-label>
<nz-form-control [nzSpan]="12" nzValidatingTip="Validating...">
<input nz-input formControlName="userName" placeholder="async validate try to write JasonWood" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>Mobile</nz-form-label>
<nz-form-control [nzSpan]="12">
<input nz-input formControlName="mobile" placeholder="mobile" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>E-mail</nz-form-label>
<nz-form-control [nzSpan]="12">
<input nz-input formControlName="email" placeholder="email" type="email" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>Password</nz-form-label>
<nz-form-control [nzSpan]="12" [nzDisableAutoTips]="true" nzErrorTip="Please input your password!">
<input nz-input type="password" formControlName="password" (ngModelChange)="validateConfirmPassword()" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>Confirm Password</nz-form-label>
<nz-form-control [nzSpan]="12" [nzDisableAutoTips]="true" [nzErrorTip]="passwordErrorTpl">
<input nz-input type="password" formControlName="confirm" placeholder="confirm your password" />
<ng-template #passwordErrorTpl let-control>
<ng-container *ngIf="control.hasError('required')">
Please confirm your password!
</ng-container>
<ng-container *ngIf="control.hasError('confirm')">
Password is inconsistent!
</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzOffset]="7" [nzSpan]="12">
<button nz-button nzType="primary">Submit</button>
</nz-form-control>
</nz-form-item>
</form>
`,
styles: [
`
[nz-form] {
max-width: 600px;
}
`
]
})
export class NzDemoFormAutoTipsComponent {
validateForm: FormGroup;

// current locale is key of the nzTipOptions
tipOptions: Record<string, Record<string, string>> = {
'zh-cn': {
required: '必填项',
email: '邮箱格式不正确'
},
en: {
required: 'Input is required',
email: 'The input is not valid email'
}
};

submitForm(value: { userName: string; email: string; password: string; confirm: string; comment: string }): void {
for (const key in this.validateForm.controls) {
this.validateForm.controls[key].markAsDirty();
this.validateForm.controls[key].updateValueAndValidity();
}
console.log(value);
}

validateConfirmPassword(): void {
setTimeout(() => this.validateForm.controls.confirm.updateValueAndValidity());
}

userNameAsyncValidator = (control: FormControl) =>
new Observable((observer: Observer<MyValidationErrors | null>) => {
setTimeout(() => {
if (control.value === 'JasonWood') {
observer.next({
duplicated: { 'zh-cn': `用户名已存在`, en: `The username is redundant!` }
});
} else {
observer.next(null);
}
observer.complete();
}, 1000);
});

confirmValidator = (control: FormControl): { [s: string]: boolean } => {
if (!control.value) {
return { error: true, required: true };
} else if (control.value !== this.validateForm.controls.password.value) {
return { confirm: true, error: true };
}
return {};
};

constructor(private fb: FormBuilder) {
// use `MyValidators`
const { required, maxLength, minLength, email, mobile } = MyValidators;
this.validateForm = this.fb.group({
userName: ['', [required, maxLength(12), minLength(6)], [this.userNameAsyncValidator]],
mobile: ['', [required, mobile]],
email: ['', [required, email]],
password: ['', [required]],
confirm: ['', [this.confirmValidator]]
});
}
}

// current locale is key of the MyErrorsOptions
export type MyErrorsOptions = { 'zh-cn': string; en: string } & Record<string, NzSafeAny>;
export type MyValidationErrors = Record<string, MyErrorsOptions>;

export class MyValidators extends Validators {
static minLength(minLength: number): ValidatorFn {
return (control: AbstractControl): MyValidationErrors | null => {
if (Validators.minLength(minLength)(control) === null) {
return null;
}
return { minlength: { 'zh-cn': `最小长度为 ${minLength}`, en: `MinLength is ${minLength}` } };
};
}

static maxLength(maxLength: number): ValidatorFn {
return (control: AbstractControl): MyValidationErrors | null => {
if (Validators.maxLength(maxLength)(control) === null) {
return null;
}
return { maxlength: { 'zh-cn': `最大长度为 ${maxLength}`, en: `MaxLength is ${maxLength}` } };
};
}

static mobile(control: AbstractControl): MyValidationErrors | null {
const value = control.value;

if (isEmptyInputValue(value)) {
return null;
}

return isMobile(value) ? null : { mobile: { 'zh-cn': `手机号码格式不正确`, en: `Mobile phone number is not valid` } };
}
}

function isEmptyInputValue(value: NzSafeAny): boolean {
return value == null || value.length === 0;
}

function isMobile(value: string): boolean {
return typeof value === 'string' && /(^1\d{10}$)/.test(value);
}
5 changes: 4 additions & 1 deletion components/form/doc/index.en-US.md
Expand Up @@ -54,7 +54,8 @@ import { NzFormModule } from 'ng-zorro-antd/form';
| -------- | ----------- | ---- | ------------- | ------------- |
| `[nzLayout]`| Form layout | `'horizontal' \| 'vertical' \| 'inline'` | `'horizontal'` |
| `[nzNoColon]`| change default props `[nzNoColon]` value of `nz-form-label` | `boolean` | `false` ||

| `[nzTipOptions]`| Set default props `[nzTipOptions]` value of `nz-form-control`, please refer to the example: **Auto tips** | `Record<string, Record<string, string>>` | `{}` ||
| `[nzDisableAutoTips]`| Set default props `[nzDisableAutoTip]` value of `nz-form-control` | `boolean` | `false` ||

### nz-form-item

Expand Down Expand Up @@ -90,6 +91,8 @@ A form consists of one or more form fields whose type includes input, textarea,
| `[nzWarningTip]`| Tip display when validate warning | `string \| TemplateRef<{ $implicit: FormControl \| NgModel }>` | - |
| `[nzErrorTip]`| Tip display when validate error | `string \| TemplateRef<{ $implicit: FormControl \| NgModel }>` | - |
| `[nzValidatingTip]`| Tip display when validating | `string \| TemplateRef<{ $implicit: FormControl \| NgModel }>` | - |
| `[nzTipOptions]`| The object of the tips, please refer to the example: **Auto tips** | `Record<string, string \| Record<string, string>>` | - | - |
| `[nzDisableAutoTips]`| Disable Auto Tips | `boolean` | - | - |

### nz-form-split

Expand Down
6 changes: 4 additions & 2 deletions components/form/doc/index.zh-CN.md
Expand Up @@ -54,6 +54,8 @@ import { NzFormModule } from 'ng-zorro-antd/form';
| --- | --- | --- | --- | --- |
| `[nzLayout]`| 表单布局 | `'horizontal' \| 'vertical' \| 'inline'` | `'horizontal'` |
| `[nzNoColon]`| 配置 `nz-form-label``[nzNoColon]` 的默认值 | `boolean` | `false` ||
| `[nzTipOptions]`| 配置 `nz-form-control``[nzTipOptions]` 的默认值, 具体用法请参考示例:**自动提示** | `Record<string, Record<string, string>>` | `{}` ||
| `[nzDisableAutoTips]`| 配置 `nz-form-control``[nzDisableAutoTips]` 的默认值 | `boolean` | `false` ||

### nz-form-item

Expand Down Expand Up @@ -91,7 +93,8 @@ import { NzFormModule } from 'ng-zorro-antd/form';
| `[nzWarningTip]`| 校验状态 warning 时提示信息 | `string \| TemplateRef<{ $implicit: FormControl \| NgModel }>` | - |
| `[nzErrorTip]`| 校验状态 error 时提示信息 | `string \| TemplateRef<{ $implicit: FormControl \| NgModel }>` | - |
| `[nzValidatingTip]`| 正在校验时提示信息 | `string \| TemplateRef<{ $implicit: FormControl \| NgModel }>` | - |

| `[nzTipOptions]`| 配置提示的对象, 具体用法请参考示例:**自动提示** | `Record<string, Record<string, string>>` | - | - |
| `[nzDisableAutoTips]`| 禁用自动提示 | `boolean` | - | - |

### nz-form-split

Expand All @@ -100,4 +103,3 @@ import { NzFormModule } from 'ng-zorro-antd/form';
### nz-form-text

`nz-form-control` 中直接显示文本

73 changes: 66 additions & 7 deletions components/form/form-control.component.ts
Expand Up @@ -27,8 +27,10 @@ import { helpMotion } from 'ng-zorro-antd/core/animation';
import { BooleanInput } from 'ng-zorro-antd/core/types';

import { toBoolean } from 'ng-zorro-antd/core/util';
import { Subscription } from 'rxjs';
import { startWith } from 'rxjs/operators';
import { NzI18nService } from 'ng-zorro-antd/i18n';
import { Subject, Subscription } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';

import { NzFormControlStatusType, NzFormItemComponent } from './form-item.component';

const iconTypeMap = {
Expand Down Expand Up @@ -68,11 +70,21 @@ export class NzFormControlComponent implements OnDestroy, OnInit, AfterContentIn
static ngAcceptInputType_nzHasFeedback: BooleanInput;
static ngAcceptInputType_nzRequired: BooleanInput;
static ngAcceptInputType_nzNoColon: BooleanInput;
static ngAcceptInputType_nzDisableAutoTips: BooleanInput;

private _hasFeedback = false;
private validateChanges: Subscription = Subscription.EMPTY;
private validateString: string | null = null;
private status: NzFormControlStatusType = null;
private destroyed$ = new Subject<void>();
private localeId: string;
private defaultTipOptions: Record<string, string | Record<string, string>>;
private defaultDisableAutoTips: boolean;
private autoErrorTip: string;

private get disableAutoTips(): boolean {
return this.nzDisableAutoTips !== 'default' ? toBoolean(this.nzDisableAutoTips) : this.defaultDisableAutoTips;
}

validateControl: FormControl | NgModel | null = null;
iconType: typeof iconTypeMap[keyof typeof iconTypeMap] | null = null;
Expand All @@ -84,6 +96,8 @@ export class NzFormControlComponent implements OnDestroy, OnInit, AfterContentIn
@Input() nzErrorTip: string | TemplateRef<{ $implicit: FormControl | NgModel }>;
@Input() nzValidatingTip: string | TemplateRef<{ $implicit: FormControl | NgModel }>;
@Input() nzExtra: string | TemplateRef<void>;
@Input() nzTipOptions: Record<string, Record<string, string>>;
@Input() nzDisableAutoTips: boolean | 'default' = 'default';

@Input()
set nzHasFeedback(value: boolean) {
Expand Down Expand Up @@ -122,9 +136,13 @@ export class NzFormControlComponent implements OnDestroy, OnInit, AfterContentIn
this.removeSubscribe();
/** miss detect https://github.com/angular/angular/issues/10887 **/
if (this.validateControl && this.validateControl.statusChanges) {
this.validateChanges = this.validateControl.statusChanges.pipe(startWith(null)).subscribe(() => {
this.setStatus();
this.cdr.markForCheck();
this.validateChanges = this.validateControl.statusChanges.pipe(startWith(null)).subscribe(_ => {
if (this.disableAutoTips) {
this.setStatus();
this.cdr.markForCheck();
} else {
this.updateAutoTip();
}
});
}
}
Expand Down Expand Up @@ -169,7 +187,7 @@ export class NzFormControlComponent implements OnDestroy, OnInit, AfterContentIn
private getInnerTip(status: NzFormControlStatusType): string | TemplateRef<{ $implicit: FormControl | NgModel }> | null {
switch (status) {
case 'error':
return this.nzErrorTip;
return (!this.disableAutoTips && this.autoErrorTip) || this.nzErrorTip;
case 'validating':
return this.nzValidatingTip;
case 'success':
Expand All @@ -181,13 +199,54 @@ export class NzFormControlComponent implements OnDestroy, OnInit, AfterContentIn
}
}

private updateAutoTip(): void {
this.updateAutoErrorTip();
this.setStatus();
this.cdr.markForCheck();
}

private updateAutoErrorTip(): void {
if (this.validateControl) {
const errors = this.validateControl.errors || {};
let autoErrorTip = '';
for (const key in errors) {
if (errors.hasOwnProperty(key)) {
autoErrorTip = errors[key][this.localeId];
if (!autoErrorTip) {
const tipOptions = this.nzTipOptions || this.defaultTipOptions || {};
autoErrorTip = (tipOptions[this.localeId] || {})[key];
}
}
if (!!autoErrorTip) {
break;
}
}
this.autoErrorTip = autoErrorTip;
}
}

setDefaultAutoTipConf(tipOptions: Record<string, string | Record<string, string>>, disableAutoTip: boolean): void {
this.defaultTipOptions = tipOptions;
this.defaultDisableAutoTips = disableAutoTip;
if (!this.disableAutoTips) {
this.updateAutoTip();
}
}

constructor(
elementRef: ElementRef,
@Optional() @Host() private nzFormItemComponent: NzFormItemComponent,
private cdr: ChangeDetectorRef,
renderer: Renderer2
renderer: Renderer2,
i18n: NzI18nService
) {
renderer.addClass(elementRef.nativeElement, 'ant-form-item-control');
i18n.localeChange.pipe(takeUntil(this.destroyed$)).subscribe(locale => {
this.localeId = locale.locale;
if (!this.disableAutoTips) {
this.updateAutoTip();
}
});
}

ngOnInit(): void {
Expand Down

0 comments on commit 0b85483

Please sign in to comment.