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

Angular表单验证在用户输入中的应用

2024-07-281.3k 阅读

一、Angular 表单验证基础概念

在前端开发中,表单是用户与应用程序进行交互的重要方式之一。用户通过表单输入各种信息,如用户名、密码、电子邮件地址等。为了确保这些输入信息的准确性和完整性,表单验证是必不可少的环节。Angular 提供了强大且灵活的表单验证机制,帮助开发者轻松实现对用户输入的有效验证。

1.1 验证器类型

Angular 中的验证器主要分为两类:同步验证器和异步验证器。

  • 同步验证器:同步验证器会立即对用户输入进行验证,并返回验证结果。常见的同步验证器包括 required(必填项验证)、minlength(最小长度验证)、maxlength(最大长度验证)等。例如,对于一个用户名输入框,我们可以使用 required 验证器确保用户必须输入内容,使用 minlength 验证器限制用户名至少为 3 个字符。
  • 异步验证器:异步验证器不会立即返回验证结果,而是通过异步操作(如网络请求)来验证用户输入。例如,当验证用户名是否已存在时,我们需要向服务器发送请求来检查数据库中是否已有该用户名,这时就需要使用异步验证器。

1.2 模板驱动表单与响应式表单的验证差异

Angular 支持两种类型的表单:模板驱动表单和响应式表单。这两种表单在验证方式上略有不同。

  • 模板驱动表单:在模板驱动表单中,验证器通常直接在 HTML 模板中声明。例如,要为一个输入框添加必填项验证,我们可以在 <input> 标签中添加 required 属性:
<input type="text" name="username" required>

模板驱动表单的验证逻辑相对简单直观,但对于复杂的验证场景,代码可能会变得冗长和难以维护。

  • 响应式表单:响应式表单的验证器在 TypeScript 代码中定义。我们可以通过创建 FormControlFormGroupFormArray 实例时传入验证器函数来进行验证。例如:
import { FormControl, Validators } from '@angular/forms';

const usernameControl = new FormControl('', [Validators.required, Validators.minLength(3)]);

响应式表单更适合处理复杂的验证逻辑,因为它将验证逻辑与模板分离,提高了代码的可维护性和可测试性。

二、同步验证器的应用

2.1 必填项验证(required)

必填项验证是最常见的验证需求之一。在模板驱动表单中,我们只需在相应的 <input> 标签上添加 required 属性即可:

<input type="text" name="name" required>

当用户提交表单时,如果该输入框为空,Angular 会自动显示默认的错误提示信息。我们也可以自定义错误提示信息,通过 ng-messages 指令来实现:

<input type="text" name="name" required #name="ngModel">
<ng-messages for="name.$error">
  <ng-message when="required">姓名不能为空</ng-message>
</ng-messages>

在响应式表单中,我们在创建 FormControl 时传入 Validators.required

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

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  nameControl = new FormControl('', Validators.required);
}

然后在模板中绑定该 FormControl 并显示错误信息:

<input type="text" [formControl]="nameControl">
<div *ngIf="nameControl.hasError('required') && (nameControl.touched || nameControl.dirty)">
  姓名不能为空
</div>

2.2 长度验证(minlength 和 maxlength)

长度验证用于限制用户输入的字符长度。在模板驱动表单中,我们使用 minlengthmaxlength 属性:

<input type="text" name="password" minlength="6" maxlength="20">

同样,我们可以使用 ng-messages 来自定义错误提示信息:

<input type="text" name="password" minlength="6" maxlength="20" #password="ngModel">
<ng-messages for="password.$error">
  <ng-message when="minlength">密码长度至少为 6 位</ng-message>
  <ng-message when="maxlength">密码长度不能超过 20 位</ng-message>
</ng-messages>

在响应式表单中,我们在创建 FormControl 时传入 Validators.minLengthValidators.maxLength

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

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  passwordControl = new FormControl('', [Validators.minLength(6), Validators.maxLength(20)]);
}

在模板中显示错误信息:

<input type="password" [formControl]="passwordControl">
<div *ngIf="passwordControl.hasError('minlength') && (passwordControl.touched || passwordControl.dirty)">
  密码长度至少为 6 位
</div>
<div *ngIf="passwordControl.hasError('maxlength') && (passwordControl.touched || passwordControl.dirty)">
  密码长度不能超过 20 位
</div>

2.3 正则表达式验证(pattern)

正则表达式验证允许我们根据自定义的正则表达式来验证用户输入。在模板驱动表单中,我们使用 pattern 属性:

<input type="text" name="email" pattern="[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+">

自定义错误提示信息:

<input type="text" name="email" pattern="[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+" #email="ngModel">
<ng-messages for="email.$error">
  <ng-message when="pattern">请输入有效的电子邮件地址</ng-message>
</ng-messages>

在响应式表单中,我们在创建 FormControl 时传入 Validators.pattern

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

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  emailControl = new FormControl('', Validators.pattern('[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+'));
}

在模板中显示错误信息:

<input type="email" [formControl]="emailControl">
<div *ngIf="emailControl.hasError('pattern') && (emailControl.touched || emailControl.dirty)">
  请输入有效的电子邮件地址
</div>

2.4 自定义同步验证器

除了使用 Angular 提供的内置验证器,我们还可以创建自定义同步验证器。假设我们要验证一个输入框的值是否为奇数,我们可以创建如下自定义验证器:

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

export function oddNumberValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    if (value && isNaN(value) || value % 2 === 0) {
      return { oddNumber: true };
    }
    return null;
  };
}

在响应式表单中使用这个自定义验证器:

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { oddNumberValidator } from './odd-number.validator';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  numberControl = new FormControl('', oddNumberValidator());
}

在模板中显示错误信息:

<input type="number" [formControl]="numberControl">
<div *ngIf="numberControl.hasError('oddNumber') && (numberControl.touched || numberControl.dirty)">
  请输入一个奇数
</div>

在模板驱动表单中使用自定义验证器稍微复杂一些,我们需要使用 NG_VALIDATORS 令牌来注册验证器:

import { Directive, forwardRef } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, Validator } from '@angular/forms';
import { oddNumberValidator } from './odd-number.validator';

@Directive({
  selector: '[appOddNumber]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => OddNumberDirective),
      multi: true
    }
  ]
})
export class OddNumberDirective implements Validator {
  validate(control: AbstractControl): { [key: string]: any } | null {
    return oddNumberValidator()(control);
  }
}

在模板中使用:

<input type="number" name="number" appOddNumber #number="ngModel">
<ng-messages for="number.$error">
  <ng-message when="oddNumber">请输入一个奇数</ng-message>
</ng-messages>

三、异步验证器的应用

3.1 异步验证器的基本原理

异步验证器通过返回一个 ObservablePromise 来进行验证。当用户输入发生变化时,异步验证器会触发异步操作,如网络请求。在验证过程中,FormControlstatus 会变为 'PENDING',当异步操作完成后,根据验证结果将 status 变为 'VALID''INVALID'

3.2 用户名唯一性验证示例

假设我们要验证用户名是否已存在,我们需要向服务器发送请求来检查。首先,创建一个异步验证器函数:

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

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

  validate(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 };
          }
          return null;
        })
      );
  }
}

在响应式表单中使用这个异步验证器:

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { UsernameUniqueValidator } from './username-unique.validator';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  usernameControl = new FormControl('', [Validators.required], [new UsernameUniqueValidator()]);
}

在模板中显示错误信息:

<input type="text" [formControl]="usernameControl">
<div *ngIf="usernameControl.hasError('usernameExists') && (usernameControl.touched || usernameControl.dirty)">
  用户名已存在,请选择其他用户名
</div>

3.3 处理异步验证的延迟与防抖

在实际应用中,频繁的异步验证请求可能会给服务器带来压力,并且用户体验也不好。我们可以通过延迟和防抖来优化异步验证。例如,使用 debounceTime 操作符来延迟验证请求的发送:

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

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

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return new Observable(observer => {
      const subscription = control.valueChanges
       .pipe(
          debounceTime(500),
          map(() => {
            const username = control.value;
            return this.http.get<any>(`/api/check-username?username=${username}`)
             .pipe(
                map(response => {
                  if (response.exists) {
                    return { usernameExists: true };
                  }
                  return null;
                })
              );
          })
        )
       .subscribe(result => {
          result.subscribe(observer);
        });

      return () => {
        subscription.unsubscribe();
      };
    });
  }
}

这样,只有当用户停止输入 500 毫秒后,才会触发异步验证请求。

四、表单验证状态与错误处理

4.1 表单验证状态

Angular 表单中的 FormControlFormGroupFormArray 都有 status 属性,用于表示当前的验证状态。常见的状态有:

  • 'VALID':表示表单控件的值通过了所有验证器的验证。
  • 'INVALID':表示表单控件的值没有通过至少一个验证器的验证。
  • 'PENDING':仅用于异步验证器,在异步验证进行过程中,表单控件的状态为 'PENDING'
  • 'DISABLED':表示表单控件被禁用,此时它不会参与验证,并且其值不会包含在提交的表单数据中。

我们可以根据表单控件的 status 来决定是否显示错误信息、禁用提交按钮等。例如,在模板中:

<button [disabled]="formGroup.status === 'INVALID'">提交</button>

4.2 错误处理与显示

除了前面提到的通过 *ngIfng - messages 来显示错误信息外,我们还可以通过 FormControlerrors 属性来获取详细的错误信息。例如,在组件类中:

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

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  passwordControl = new FormControl('', [Validators.minLength(6), Validators.maxLength(20)]);

  getPasswordErrors() {
    return this.passwordControl.errors;
  }
}

在模板中:

<input type="password" [formControl]="passwordControl">
<pre>{{ getPasswordErrors() | json }}</pre>

这样可以在开发过程中方便地查看错误信息的结构,以便更好地进行错误处理和显示。

五、表单验证的高级应用

5.1 跨字段验证

有时我们需要根据多个字段的值进行验证,这就是跨字段验证。例如,在一个注册表单中,我们需要验证两次输入的密码是否一致。在响应式表单中,我们可以通过创建一个自定义验证器来实现:

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 };
    }
    return null;
  };
}

然后在创建 FormGroup 时使用这个验证器:

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { passwordMatchValidator } from './password-match.validator';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  registerForm = new FormGroup({
    password: new FormControl('', Validators.minLength(6)),
    confirmPassword: new FormControl('', Validators.minLength(6))
  }, { validators: passwordMatchValidator() });
}

在模板中显示错误信息:

<form [formGroup]="registerForm">
  <input type="password" formControlName="password">
  <input type="password" formControlName="confirmPassword">
  <div *ngIf="registerForm.hasError('passwordMismatch') && (registerForm.touched || registerForm.dirty)">
    两次输入的密码不一致
  </div>
</form>

5.2 动态验证

在某些情况下,我们需要根据用户的操作动态地添加或移除验证器。例如,当用户选择了一个特定的选项时,才对某个输入框进行验证。在响应式表单中,我们可以通过 FormControladdValidatorsremoveValidators 方法来实现:

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

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent {
  optionControl = new FormControl('');
  inputControl = new FormControl('');

  onOptionChange() {
    if (this.optionControl.value === 'specificOption') {
      this.inputControl.addValidators(Validators.required);
    } else {
      this.inputControl.removeValidators(Validators.required);
    }
    this.inputControl.updateValueAndValidity();
  }
}

在模板中:

<select [formControl]="optionControl" (change)="onOptionChange()">
  <option value="option1">选项 1</option>
  <option value="specificOption">特定选项</option>
</select>
<input type="text" [formControl]="inputControl">
<div *ngIf="inputControl.hasError('required') && (inputControl.touched || inputControl.dirty)">
  该输入框为必填项
</div>

通过这种方式,我们可以根据用户的操作动态地调整表单的验证规则,提供更加灵活和友好的用户体验。

5.3 嵌套表单的验证

在复杂的表单中,我们可能会有嵌套的表单结构,即 FormGroup 中包含其他 FormGroupFormArray。例如,一个订单表单可能包含多个商品项,每个商品项又是一个 FormGroup。在这种情况下,验证会自动级联。父 FormGroupstatus 会根据其包含的所有子 FormControlFormGroupFormArraystatus 来确定。如果任何一个子项无效,父 FormGroupstatus 也会变为 'INVALID'

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

@Component({
  selector: 'app-order-form',
  templateUrl: './order-form.component.html',
  styleUrls: ['./order-form.component.css']
})
export class OrderFormComponent {
  orderForm = new FormGroup({
    customerName: new FormControl('', Validators.required),
    items: new FormArray([
      this.createItemFormGroup()
    ])
  });

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

  addItem() {
    const items = this.orderForm.get('items') as FormArray;
    items.push(this.createItemFormGroup());
  }
}

在模板中:

<form [formGroup]="orderForm">
  <input type="text" formControlName="customerName">
  <div formArrayName="items">
    <div *ngFor="let item of orderForm.get('items').controls; let i = index" [formGroupName]="i">
      <input type="text" formControlName="productName">
      <input type="number" formControlName="quantity">
    </div>
  </div>
  <button type="button" (click)="addItem()">添加商品项</button>
  <button [disabled]="orderForm.status === 'INVALID'">提交订单</button>
</form>

这样,当任何一个商品项的 productNamequantity 验证不通过时,整个订单表单的 status 会变为 'INVALID',提交按钮也会被禁用。

通过以上对 Angular 表单验证在用户输入中的各种应用的详细介绍,开发者可以根据实际项目需求,灵活运用同步验证器、异步验证器以及各种高级验证技巧,打造出高效、准确且用户体验良好的表单验证功能。无论是简单的必填项验证,还是复杂的跨字段、动态和嵌套表单验证,Angular 都提供了丰富且强大的工具来满足我们的需求。在实际开发中,我们需要根据具体场景选择合适的验证方式和技术,以确保用户输入的数据符合我们的业务要求。同时,合理地处理验证状态和错误信息的显示,也是提升用户体验的重要环节。希望本文能够帮助你在 Angular 前端开发中更好地应用表单验证技术。