MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular表单验证在复杂表单中的应用

2021-08-211.7k 阅读

Angular 表单验证基础回顾

在深入探讨 Angular 表单验证在复杂表单中的应用之前,我们先来回顾一下 Angular 表单验证的基础知识。Angular 提供了两种主要的表单处理方式:模板驱动表单(Template - Driven Forms)和响应式表单(Reactive Forms),每种方式都有其独特的表单验证机制。

模板驱动表单验证

模板驱动表单是通过在 HTML 模板中使用指令来创建和验证表单。例如,我们可以使用 ngModel 指令结合 HTML5 的原生验证属性来实现基本的表单验证。

<form #myForm="ngForm">
  <input type="text" name="username" [(ngModel)]="user.username" required>
  <button type="submit" [disabled]="!myForm.form.valid">提交</button>
</form>

在上述代码中,required 是 HTML5 的原生验证属性,它确保输入框不能为空。ngModel 指令将输入框的值绑定到组件的 user.username 属性上,同时 #myForm="ngForm" 创建了一个本地变量 myForm,它引用了整个表单。通过 myForm.form.valid 我们可以判断表单是否有效,进而控制提交按钮的禁用状态。

响应式表单验证

响应式表单则更加灵活和强大,它通过在组件类中构建表单模型来创建和验证表单。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app - reactive - form',
  templateUrl: './reactive - form.component.html'
})
export class ReactiveFormComponent {
  myForm: FormGroup;

  constructor() {
    this.myForm = new FormGroup({
      username: new FormControl('', Validators.required),
      email: new FormControl('', [Validators.required, Validators.email])
    });
  }
}
<form [formGroup]="myForm">
  <input type="text" formControlName="username">
  <input type="email" formControlName="email">
  <button type="submit" [disabled]="!myForm.valid">提交</button>
</form>

在这段代码中,我们在组件类中使用 FormGroupFormControl 来构建表单模型。Validators.requiredValidators.email 等验证器被直接应用到 FormControl 上。在模板中,通过 formControlName 指令将输入框与表单模型中的控件进行绑定。

复杂表单的定义与特点

复杂表单通常具有多个字段、嵌套结构、动态生成的部分以及不同字段之间存在逻辑关联等特点。

多个字段与分组

一个复杂表单可能包含数十个甚至上百个字段,例如一个企业级的员工信息录入表单,可能涵盖基本信息(姓名、性别、出生日期等)、工作信息(职位、部门、入职时间等)、联系方式(电话、邮箱、地址等)等多个部分。为了更好地管理和验证这些字段,我们需要将相关字段进行分组。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app - complex - form',
  templateUrl: './complex - form.component.html'
})
export class ComplexFormComponent {
  employeeForm: FormGroup;

  constructor() {
    this.employeeForm = new FormGroup({
      basicInfo: new FormGroup({
        name: new FormControl('', Validators.required),
        gender: new FormControl('', Validators.required),
        birthDate: new FormControl('', Validators.required)
      }),
      workInfo: new FormGroup({
        position: new FormControl('', Validators.required),
        department: new FormControl('', Validators.required),
        hireDate: new FormControl('', Validators.required)
      }),
      contactInfo: new FormGroup({
        phone: new FormControl('', Validators.required),
        email: new FormControl('', [Validators.required, Validators.email]),
        address: new FormControl('', Validators.required)
      })
    });
  }
}
<form [formGroup]="employeeForm">
  <div formGroupName="basicInfo">
    <input type="text" formControlName="name">
    <select formControlName="gender">
      <option value="male">男</option>
      <option value="female">女</option>
    </select>
    <input type="date" formControlName="birthDate">
  </div>
  <div formGroupName="workInfo">
    <input type="text" formControlName="position">
    <input type="text" formControlName="department">
    <input type="date" formControlName="hireDate">
  </div>
  <div formGroupName="contactInfo">
    <input type="tel" formControlName="phone">
    <input type="email" formControlName="email">
    <input type="text" formControlName="address">
  </div>
  <button type="submit" [disabled]="!employeeForm.valid">提交</button>
</form>

在这个例子中,我们将员工信息表单分成了三个组:basicInfoworkInfocontactInfo。每个组都有自己的一组字段和验证规则。

嵌套结构

复杂表单还可能存在嵌套结构,比如在一个订单表单中,订单包含多个订单项,每个订单项又有自己的产品信息、数量、价格等字段。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, FormArray } from '@angular/forms';

@Component({
  selector: 'app - order - form',
  templateUrl: './order - form.component.html'
})
export class OrderFormComponent {
  orderForm: FormGroup;

  constructor() {
    this.orderForm = new FormGroup({
      orderNumber: new FormControl('', Validators.required),
      orderDate: new FormControl('', Validators.required),
      orderItems: new FormArray([this.createOrderItem()])
    });
  }

  createOrderItem(): FormGroup {
    return new FormGroup({
      productName: new FormControl('', Validators.required),
      quantity: new FormControl(1, [Validators.required, Validators.min(1)]),
      price: new FormControl(0, [Validators.required, Validators.min(0)])
    });
  }

  addOrderItem() {
    const orderItems = this.orderForm.get('orderItems') as FormArray;
    orderItems.push(this.createOrderItem());
  }
}
<form [formGroup]="orderForm">
  <input type="text" formControlName="orderNumber">
  <input type="date" formControlName="orderDate">
  <div formArrayName="orderItems">
    <div *ngFor="let item of orderForm.get('orderItems').controls; let i = index" [formGroupName]="i">
      <input type="text" formControlName="productName">
      <input type="number" formControlName="quantity">
      <input type="number" formControlName="price">
    </div>
    <button type="button" (click)="addOrderItem()">添加订单项</button>
  </div>
  <button type="submit" [disabled]="!orderForm.valid">提交</button>
</form>

在上述代码中,orderItems 是一个 FormArray,每个订单项是一个 FormGroup。这种嵌套结构允许我们动态添加订单项,并且每个订单项都有自己的验证规则。

动态生成部分

复杂表单中的某些部分可能是根据用户的操作动态生成的。例如,在一个调查问卷表单中,用户可以根据需要添加更多的问题。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, FormArray } from '@angular/forms';

@Component({
  selector: 'app - survey - form',
  templateUrl: './survey - form.component.html'
})
export class SurveyFormComponent {
  surveyForm: FormGroup;

  constructor() {
    this.surveyForm = new FormGroup({
      questions: new FormArray([this.createQuestion()])
    });
  }

  createQuestion(): FormGroup {
    return new FormGroup({
      questionText: new FormControl('', Validators.required),
      answerType: new FormControl('text', Validators.required)
    });
  }

  addQuestion() {
    const questions = this.surveyForm.get('questions') as FormArray;
    questions.push(this.createQuestion());
  }
}
<form [formGroup]="surveyForm">
  <div formArrayName="questions">
    <div *ngFor="let question of surveyForm.get('questions').controls; let i = index" [formGroupName]="i">
      <input type="text" formControlName="questionText">
      <select formControlName="answerType">
        <option value="text">文本</option>
        <option value="radio">单选</option>
        <option value="checkbox">多选</option>
      </select>
    </div>
    <button type="button" (click)="addQuestion()">添加问题</button>
  </div>
  <button type="submit" [disabled]="!surveyForm.valid">提交</button>
</form>

这里的 questions 是一个 FormArray,用户可以通过点击按钮动态添加问题,每个问题都有自己的验证规则。

字段间逻辑关联

复杂表单中不同字段之间可能存在逻辑关联。比如在一个贷款申请表单中,如果用户选择贷款类型为“房屋贷款”,则需要填写房产地址等相关信息;如果选择“汽车贷款”,则需要填写车辆信息等。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app - loan - form',
  templateUrl: './loan - form.component.html'
})
export class LoanFormComponent {
  loanForm: FormGroup;

  constructor() {
    this.loanForm = new FormGroup({
      loanType: new FormControl('', Validators.required),
      houseAddress: new FormControl(''),
      carInfo: new FormControl('')
    });

    this.loanForm.get('loanType').valueChanges.subscribe((value) => {
      if (value === 'house') {
        this.loanForm.get('houseAddress').setValidators(Validators.required);
        this.loanForm.get('carInfo').clearValidators();
      } else if (value === 'car') {
        this.loanForm.get('carInfo').setValidators(Validators.required);
        this.loanForm.get('houseAddress').clearValidators();
      } else {
        this.loanForm.get('houseAddress').clearValidators();
        this.loanForm.get('carInfo').clearValidators();
      }
      this.loanForm.get('houseAddress').updateValueAndValidity();
      this.loanForm.get('carInfo').updateValueAndValidity();
    });
  }
}
<form [formGroup]="loanForm">
  <select formControlName="loanType">
    <option value="house">房屋贷款</option>
    <option value="car">汽车贷款</option>
  </select>
  <input type="text" formControlName="houseAddress" *ngIf="loanForm.get('loanType').value === 'house'">
  <input type="text" formControlName="carInfo" *ngIf="loanForm.get('loanType').value === 'car'">
  <button type="submit" [disabled]="!loanForm.valid">提交</button>
</form>

在这个例子中,根据 loanType 的值动态设置 houseAddresscarInfo 的验证规则,体现了字段间的逻辑关联。

复杂表单中 Angular 表单验证的应用策略

面对复杂表单的各种特点,我们需要制定相应的表单验证应用策略。

分组验证策略

对于包含多个字段和分组的复杂表单,我们可以对每个分组进行单独验证,同时也可以对整个表单进行综合验证。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app - multi - group - form',
  templateUrl: './multi - group - form.component.html'
})
export class MultiGroupFormComponent {
  mainForm: FormGroup;

  constructor() {
    this.mainForm = new FormGroup({
      group1: new FormGroup({
        field1: new FormControl('', Validators.required),
        field2: new FormControl('', Validators.required)
      }),
      group2: new FormGroup({
        field3: new FormControl('', Validators.required),
        field4: new FormControl('', Validators.required)
      })
    });

    this.mainForm.addValidators([this.group1AndGroup2Required]);
  }

  group1AndGroup2Required(formGroup: FormGroup): { [key: string]: boolean } | null {
    const group1 = formGroup.get('group1');
    const group2 = formGroup.get('group2');

    if (group1 && group1.valid && group2 && group2.valid) {
      return null;
    } else {
      return { group1AndGroup2Required: true };
    }
  }
}
<form [formGroup]="mainForm">
  <div formGroupName="group1">
    <input type="text" formControlName="field1">
    <input type="text" formControlName="field2">
  </div>
  <div formGroupName="group2">
    <input type="text" formControlName="field3">
    <input type="text" formControlName="field4">
  </div>
  <div *ngIf="mainForm.hasError('group1AndGroup2Required')">
    组 1 和组 2 都必须有效
  </div>
  <button type="submit" [disabled]="!mainForm.valid">提交</button>
</form>

在上述代码中,我们不仅对 group1group2 内部的字段进行了验证,还通过自定义验证器 group1AndGroup2Required 对整个表单进行了综合验证,确保两个组都有效。

嵌套表单验证策略

对于嵌套结构的表单,验证需要从最内层的表单控件开始,逐步向外传递验证状态。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, FormArray } from '@angular/forms';

@Component({
  selector: 'app - nested - form',
  templateUrl: './nested - form.component.html'
})
export class NestedFormComponent {
  outerForm: FormGroup;

  constructor() {
    this.outerForm = new FormGroup({
      innerForms: new FormArray([this.createInnerForm()])
    });
  }

  createInnerForm(): FormGroup {
    return new FormGroup({
      subField1: new FormControl('', Validators.required),
      subField2: new FormControl('', Validators.required)
    });
  }

  addInnerForm() {
    const innerForms = this.outerForm.get('innerForms') as FormArray;
    innerForms.push(this.createInnerForm());
  }
}
<form [formGroup]="outerForm">
  <div formArrayName="innerForms">
    <div *ngFor="let innerForm of outerForm.get('innerForms').controls; let i = index" [formGroupName]="i">
      <input type="text" formControlName="subField1">
      <input type="text" formControlName="subField2">
    </div>
    <button type="button" (click)="addInnerForm()">添加内部表单</button>
  </div>
  <button type="submit" [disabled]="!outerForm.valid">提交</button>
</form>

在这个嵌套表单中,每个内部表单(innerForm)都有自己的验证规则,而 outerForm 的有效性取决于所有内部表单的有效性。当添加新的内部表单时,验证机制会自动应用到新的表单上。

动态表单验证策略

对于动态生成部分的表单,我们需要在动态添加或删除表单元素时,动态更新验证规则和验证状态。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, FormArray } from '@angular/forms';

@Component({
  selector: 'app - dynamic - form',
  templateUrl: './dynamic - form.component.html'
})
export class DynamicFormComponent {
  dynamicForm: FormGroup;

  constructor() {
    this.dynamicForm = new FormGroup({
      dynamicFields: new FormArray([this.createDynamicField()])
    });
  }

  createDynamicField(): FormControl {
    return new FormControl('', Validators.required);
  }

  addDynamicField() {
    const dynamicFields = this.dynamicForm.get('dynamicFields') as FormArray;
    dynamicFields.push(this.createDynamicField());
  }

  removeDynamicField(index: number) {
    const dynamicFields = this.dynamicForm.get('dynamicFields') as FormArray;
    dynamicFields.removeAt(index);
  }
}
<form [formGroup]="dynamicForm">
  <div formArrayName="dynamicFields">
    <div *ngFor="let field of dynamicForm.get('dynamicFields').controls; let i = index">
      <input type="text" [formControlName]="i">
      <button type="button" (click)="removeDynamicField(i)">删除</button>
    </div>
    <button type="button" (click)="addDynamicField()">添加字段</button>
  </div>
  <button type="submit" [disabled]="!dynamicForm.valid">提交</button>
</form>

在这个动态表单中,每次添加或删除一个动态字段时,FormArray 会相应地更新,并且表单的整体验证状态也会随之更新。

处理字段逻辑关联验证策略

当表单字段之间存在逻辑关联时,我们可以通过监听字段值的变化来动态调整验证规则。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app - related - fields - form',
  templateUrl: './related - fields - form.component.html'
})
export class RelatedFieldsFormComponent {
  relatedForm: FormGroup;

  constructor() {
    this.relatedForm = new FormGroup({
      fieldA: new FormControl(''),
      fieldB: new FormControl('')
    });

    this.relatedForm.get('fieldA').valueChanges.subscribe((value) => {
      if (value === 'option1') {
        this.relatedForm.get('fieldB').setValidators(Validators.required);
      } else {
        this.relatedForm.get('fieldB').clearValidators();
      }
      this.relatedForm.get('fieldB').updateValueAndValidity();
    });
  }
}
<form [formGroup]="relatedForm">
  <select formControlName="fieldA">
    <option value="option1">选项 1</option>
    <option value="option2">选项 2</option>
  </select>
  <input type="text" formControlName="fieldB">
  <button type="submit" [disabled]="!relatedForm.valid">提交</button>
</form>

在这个例子中,当 fieldA 的值为 option1 时,fieldB 变为必填项,通过监听 fieldA 的值变化来动态调整 fieldB 的验证规则。

自定义验证器在复杂表单中的应用

在复杂表单中,Angular 提供的内置验证器可能无法满足所有需求,这时我们就需要自定义验证器。

自定义同步验证器

同步验证器是立即返回验证结果的验证器。例如,我们可以创建一个自定义验证器来验证密码和确认密码是否匹配。

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function passwordMatchValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const password = control.get('password');
    const confirmPassword = control.get('confirmPassword');

    if (password && confirmPassword && password.value!== confirmPassword.value) {
      return { passwordMismatch: true };
    } else {
      return null;
    }
  };
}
import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { passwordMatchValidator } from './password - match.validator';

@Component({
  selector: 'app - password - form',
  templateUrl: './password - form.component.html'
})
export class PasswordFormComponent {
  passwordForm: FormGroup;

  constructor() {
    this.passwordForm = new FormGroup({
      password: new FormControl('', Validators.required),
      confirmPassword: new FormControl('', Validators.required)
    }, { validators: passwordMatchValidator() });
  }
}
<form [formGroup]="passwordForm">
  <input type="password" formControlName="password">
  <input type="password" formControlName="confirmPassword">
  <div *ngIf="passwordForm.hasError('passwordMismatch')">
    密码和确认密码不匹配
  </div>
  <button type="submit" [disabled]="!passwordForm.valid">提交</button>
</form>

在上述代码中,passwordMatchValidator 是一个自定义同步验证器,它接收一个 AbstractControl,并检查 passwordconfirmPassword 控件的值是否匹配。

自定义异步验证器

异步验证器用于需要进行异步操作(如调用后端 API)的验证场景。比如验证用户名是否已存在。

import { Injectable } from '@angular/core';
import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class UsernameValidator {
  constructor(private http: HttpClient) {}

  checkUsernameExists(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      const username = control.value;
      return this.http.get<any>(`/api/check - username?username=${username}`)
      .pipe(
          map((response) => {
            if (response.exists) {
              return { usernameExists: true };
            } else {
              return null;
            }
          }),
          catchError(() => of(null))
        );
    };
  }
}
import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { UsernameValidator } from './username.validator';

@Component({
  selector: 'app - username - form',
  templateUrl: './username - form.component.html'
})
export class UsernameFormComponent {
  usernameForm: FormGroup;

  constructor(private usernameValidator: UsernameValidator) {
    this.usernameForm = new FormGroup({
      username: new FormControl('', Validators.required, this.usernameValidator.checkUsernameExists())
    });
  }
}
<form [formGroup]="usernameForm">
  <input type="text" formControlName="username">
  <div *ngIf="(usernameForm.get('username').hasError('usernameExists') && (usernameForm.get('username').touched || usernameForm.get('username').dirty))">
    用户名已存在
  </div>
  <button type="submit" [disabled]="!usernameForm.valid">提交</button>
</form>

在这个例子中,checkUsernameExists 是一个自定义异步验证器,它通过调用后端 API 来检查用户名是否已存在,并返回一个 Observable,根据 API 的响应结果返回相应的验证错误。

错误提示与用户反馈优化

在复杂表单中,良好的错误提示和用户反馈对于提高用户体验至关重要。

显示特定字段错误信息

我们可以针对每个表单字段,根据其验证状态显示特定的错误信息。

<form [formGroup]="myForm">
  <input type="text" formControlName="username">
  <div *ngIf="myForm.get('username').hasError('required') && (myForm.get('username').touched || myForm.get('username').dirty)">
    用户名是必填项
  </div>
  <input type="email" formControlName="email">
  <div *ngIf="myForm.get('email').hasError('required') && (myForm.get('email').touched || myForm.get('email').dirty)">
    邮箱是必填项
  </div>
  <div *ngIf="myForm.get('email').hasError('email') && (myForm.get('email').touched || myForm.get('email').dirty)">
    请输入有效的邮箱地址
  </div>
  <button type="submit" [disabled]="!myForm.valid">提交</button>
</form>

在上述代码中,通过 *ngIf 指令根据表单控件的验证错误状态和触摸或脏污状态来显示相应的错误信息。

整体表单错误汇总

对于复杂表单,提供整体的错误汇总可以帮助用户快速定位问题。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app - error - summary - form',
  templateUrl: './error - summary - form.component.html'
})
export class ErrorSummaryFormComponent {
  errorSummaryForm: FormGroup;

  constructor() {
    this.errorSummaryForm = new FormGroup({
      username: new FormControl('', Validators.required),
      email: new FormControl('', [Validators.required, Validators.email])
    });
  }

  getErrorMessages() {
    const messages: string[] = [];
    if (this.errorSummaryForm.get('username').hasError('required')) {
      messages.push('用户名是必填项');
    }
    if (this.errorSummaryForm.get('email').hasError('required')) {
      messages.push('邮箱是必填项');
    }
    if (this.errorSummaryForm.get('email').hasError('email')) {
      messages.push('请输入有效的邮箱地址');
    }
    return messages;
  }
}
<form [formGroup]="errorSummaryForm">
  <input type="text" formControlName="username">
  <input type="email" formControlName="email">
  <div *ngIf="!errorSummaryForm.valid">
    <h3>错误汇总</h3>
    <ul>
      <li *ngFor="let message of getErrorMessages()">{{message}}</li>
    </ul>
  </div>
  <button type="submit" [disabled]="!errorSummaryForm.valid">提交</button>
</form>

在这个例子中,通过 getErrorMessages 方法收集所有字段的错误信息,并在表单无效时显示整体的错误汇总。

实时验证与延迟验证

实时验证可以在用户输入时立即反馈验证结果,但在某些情况下可能会给用户造成干扰。我们可以通过设置延迟验证来优化用户体验。

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { debounceTime } from 'rxjs/operators';

@Component({
  selector: 'app - delayed - validation - form',
  templateUrl: './delayed - validation - form.component.html'
})
export class DelayedValidationFormComponent {
  delayedForm: FormGroup;

  constructor() {
    this.delayedForm = new FormGroup({
      username: new FormControl('', Validators.required)
    });

    this.delayedForm.get('username').valueChanges
    .pipe(debounceTime(500))
    .subscribe(() => {
        this.delayedForm.get('username').updateValueAndValidity();
      });
  }
}
<form [formGroup]="delayedForm">
  <input type="text" formControlName="username">
  <div *ngIf="delayedForm.get('username').hasError('required') && (delayedForm.get('username').touched || delayedForm.get('username').dirty)">
    用户名是必填项
  </div>
  <button type="submit" [disabled]="!delayedForm.valid">提交</button>
</form>

在上述代码中,通过 debounceTime 操作符设置了 500 毫秒的延迟,只有在用户停止输入 500 毫秒后才会触发验证,减少了实时验证带来的干扰。

通过以上对 Angular 表单验证在复杂表单中的应用探讨,我们可以看到,通过合理运用各种验证策略、自定义验证器以及优化错误提示与用户反馈,能够有效地处理复杂表单的验证需求,提高表单的可用性和用户体验。无论是处理多个字段、嵌套结构、动态生成部分还是字段间的逻辑关联,Angular 都提供了强大且灵活的工具来满足我们的开发需求。