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

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

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

 

场景

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

<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文件中对应的提交方法:

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.) {
            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:

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

ts文件中的改动:

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中进行修改:

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:

<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的,类的定义为:

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

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

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

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

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

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