0%

Angular响应式表单在reset之后表单状态还是invalid的问题探索

做表单提交的时候,很多的场景是提交完一个表单后跳转页面,或者页面刷新,或者表单重绘,这种场景下,等于表单是一次性的,提交后不用管当前表单的状态。

但是假设另一种场景,我们填写表单后提交,表单不重新绘制,我们需要手动将表单回复到初始状态。这个情况下我们使用响应式表单的reset()方法即可。但是遇到个问题,当我调用formGroup.reset()的时候,发现页面上的表单的验证状态没有被重置,也就是说还是调用reset之后,表单的状态会变为invalid。这很蛋疼。

场景

我们的应用场景是一个表单,html里面有一个表单:

1
2
3
4
5
6
7
8
9
10
11
<form [formGroup]="form" class="create-form">
<mat-form-field floatLabel="never" class="m-r-10">
<input matInput autocomplete="off" placeholder="计划内容" formControlName="content">
</mat-form-field>
<mat-form-field floatLabel="never">
<input matInput [matDatepicker]="picker" [min]="minDate" placeholder="截止时间" formControlName="endTime">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker disabled="false"></mat-datepicker>
</mat-form-field>
<button mat-button color="primary" tpe="submit" class="m-r-10">添加</button>
</form>

ts文件中对应的提交方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export class TodoComponent implements OnInit {
@Output() submitEvent: EventEmitter<TodoInterface> = new EventEmitter<TodoInterface>();
form: FormGroup;
minDate: Date;

constructor(
private fb: FormBuilder,
) {
}

ngOnInit(): void {
this.minDate = new Date();
this.form = this.fb.group({
content: [null, Validators.required],
endTime: [moment().add(1, 'days'), Validators.required],
isCompleted: false,
});
}

submit() {
this.form.updateValueAndValidity();
if (this.form.invalid) {
return;
}
const endTime = this.form.getRawValue().endTime;
const params = this.form.getRawValue() as TodoInterface;
params.endTime = (endTime as Moment).valueOf();
this.submitEvent.emit(params);
this.form.reset({
content: null,
endTime: moment().add(1, 'days'),
isCompleted: false,
});
}
}

然后现在出现的问题是,当点击提交后,“计划内容”的输入框是红色的invalid状态。

探索路程

首先google搜索,找到一个质量比较高的angular components issuess,发现问题是仅仅重置FormGroup还不够,需要重置实际表单的提交状态。那么如何重置表单?

投机的方法

看到一个比较简单(投机)的方法,直接在submit方法中传递$event,然后用currentTargetreset表单。尝试了下,可以直接解决问题。看看下。

先看html:

1
2
<form [formGroup]="form" class="create-form" (ngSubmit)="submit($event)">
<form>

ts文件中的改动:

1
2
3
4
5
6
7
8
9
10
11
12
submit(event) {
this.form.updateValueAndValidity();
if (this.form.invalid) {
return;
}
const endTime = this.form.getRawValue().endTime;
const params = this.form.getRawValue() as TodoInterface;
params.endTime = (endTime as Moment).valueOf();
this.submitEvent.emit(params);
console.log(event.currentTarget);
(event.currentTarget as HTMLFormElement).reset();
}

这里event.currentTarget是当前处理事件的目标dom节点,对应到ts中的类型的话就是HTMLFormElement,它是原生的dom,那么里面的reset方法只是重置掉这个Form。

这个看起来是能满足我们的需要,但是假设我们要在重置的时候传递值,那么是行不通的,这个方法不接受传值。

验证器的触发错误状态的条件是isInvalid && (isTouched || isSumbitted)。所以当点击提交的时候,当前表单的isSumbittedture,我们需要使用ViewChild的方式将表单的提交状态重置掉。

使用FormGroupDirective的resetForm来重置表单

在ts中进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export class TodoComponent implements OnInit {
@ViewChild(FormGroupDirective) myForm;
// other code ...

submit() {
this.form.updateValueAndValidity();
if (this.form.invalid) {
return;
}
const endTime = this.form.getRawValue().endTime;
const params = this.form.getRawValue() as TodoInterface;
params.endTime = (endTime as Moment).valueOf();
this.submitEvent.emit(params);
if (this.myForm) {
this.myForm.resetForm();
}
}
}

发现还是老样子,很蛋疼。

但又一想,我是把提交事件放在了提交按钮上,但是对于这个表单,它并不认识这个按钮。。。

那我们的html是不是有什么问题?

相应的改一下html:

1
2
3
4
5
6
7
8
9
10
11
<form [formGroup]="form" class="create-form" (ngSubmit)="submit()">
<mat-form-field floatLabel="never" class="m-r-10">
<input matInput autocomplete="off" placeholder="计划内容" formControlName="content">
</mat-form-field>
<mat-form-field floatLabel="never">
<input matInput [matDatepicker]="picker" [min]="minDate" placeholder="截止时间" formControlName="endTime">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker disabled="false"></mat-datepicker>
</mat-form-field>
<button mat-button color="primary" type="submit" class="m-r-10">添加</button>
</form>

然后再测试一下,竟然OK了,😓

还是得规范点。。。

那我们来看下@ViewChild(FormGroupDirective) myForm这个声明起啥用。

我们知道ViewChild是一个视图查询的属性装饰器,它会去查找视图中的第一个FormGroup指令,然后我们可以在ts中消费它。

那对应的,FormGroupDirective.resetForm()方法是何方神圣?

FormGroupDirective是继承于ControlContainer的,类的定义为:

1
export declare class FormGroupDirective extends ControlContainer implements Form, OnChanges {}

这个指令就是为了将FormGroup绑定到DOM元素,所以我们在ts中使用视图查询得到的myForm,就是完整的FormGroup指令,那么它提供的resetForm方法具有重置表单的值和“重置表单的提交状态”这两个功能。

相对应的,FormGroupreset方法就只有重置表单的值的功能了。

但这种方式适合于该组件只有一个FormGroup表单的,那假设有多个的时候这个方法也就不适用了。

使用模板变量精确控制

使用模板变量之前,我们需要知道FormGroupDirective有没有exportAs这个属性。exportAs定义了一个名字,用于在模板中将该指令赋值给一个变量。查看Angular源码中的form_group_directive.ts 我们可以发现,它被定义为了ngForm

1
2
3
4
5
6
@Directive({
selector: '[formGroup]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
exportAs: 'ngForm'
})

那么我们可以使用模板语法获得这个表单:

1
2
3
<form [formGroup]="form" class="create-form" (ngSubmit)="submit()" #myForm="ngForm">
...
</form>

我们声明了一个myForm的模板变量以获得FormGroupDirective。然后在ts中需要用@ViewChild来绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class TodoComponent implements OnInit {

@ViewChild('myForm') myForm: FormGroupDirective;
// other code ...
submit() {
this.form.updateValueAndValidity();
if (this.form.invalid) {
return;
}
const endTime = this.form.getRawValue().endTime;
const params = this.form.getRawValue() as TodoInterface;
params.endTime = (endTime as Moment).valueOf();
this.submitEvent.emit(params);
if (this.myForm) {
this.myForm.resetForm({
content: null,
endTime: moment().add(1, 'days'),
isCompleted: false,
})
}
}
}

非常好,完美!


其实问题不是个大问题,就是比较细节,而且我们粗暴点完全可以不理会,直接使用ngIf来消灭大多数问题。但是简单粗暴代表着不想深究,解决得了一时,以后确实有这个需求的时候不是抓瞎了么。

找到一种比较好的解决方案是比较爽的,而且以后可以复用这个逻辑。o( ̄▽ ̄)d

码字辛苦,打赏个咖啡☕️可好?💘