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

掌握Angular响应式表单的设计与开发

2023-07-303.8k 阅读

Angular 响应式表单基础

在 Angular 开发中,响应式表单是一种强大的表单处理方式,它基于观察者模式进行构建。与模板驱动表单不同,响应式表单将表单的逻辑控制集中在组件类中,使得代码更加清晰和易于维护。

表单控件

响应式表单的核心是表单控件。在 Angular 中,有三种基本的表单控件类:FormControlFormGroupFormArray

  1. FormControlFormControl 表示单个表单控件,例如输入框、单选按钮等。它可以跟踪控件的值、是否已触摸、是否有效等状态。下面是创建一个简单 FormControl 的示例:
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-form-control-example',
  templateUrl: './form-control-example.component.html'
})
export class FormControlExampleComponent {
  nameControl = new FormControl('');

  getControlValue() {
    return this.nameControl.value;
  }
}

在模板文件 form - control - example.component.html 中,可以这样使用:

<input type="text" [formControl]="nameControl">
<button (click)="getControlValue()">获取值</button>

这里,我们创建了一个 FormControl 实例 nameControl,并将其绑定到输入框。通过点击按钮,可以获取输入框的值。

  1. FormGroupFormGroup 用于将多个 FormControl 组合在一起,形成一个逻辑组。例如,一个用户注册表单可能包含用户名、密码和确认密码等多个控件,这些控件可以组合在一个 FormGroup 中。下面是创建 FormGroup 的示例:
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app - form - group - example',
  templateUrl: './form - group - example.component.html'
})
export class FormGroupExampleComponent {
  userForm = new FormGroup({
    username: new FormControl(''),
    password: new FormControl('')
  });

  getFormValue() {
    return this.userForm.value;
  }
}

在模板文件 form - group - example.component.html 中:

<form [formGroup]="userForm">
  <input type="text" formControlName="username">
  <input type="password" formControlName="password">
  <button (click)="getFormValue()">获取表单值</button>
</form>

这里,我们创建了一个包含 usernamepassword 两个 FormControlFormGroup。通过 formControlName 属性将模板中的输入框与 FormGroup 中的 FormControl 进行绑定。

  1. FormArrayFormArray 用于处理动态数量的表单控件。例如,在一个任务列表表单中,用户可以动态添加或删除任务。下面是一个简单的 FormArray 示例:
import { Component } from '@angular/core';
import { FormArray, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app - form - array - example',
  templateUrl: './form - array - example.component.html'
})
export class FormArrayExampleComponent {
  taskForm = new FormGroup({
    tasks: new FormArray([
      new FormControl('')
    ])
  });

  get tasks() {
    return this.taskForm.get('tasks') as FormArray;
  }

  addTask() {
    this.tasks.push(new FormControl(''));
  }

  removeTask(index: number) {
    this.tasks.removeAt(index);
  }

  getFormValue() {
    return this.taskForm.value;
  }
}

在模板文件 form - array - example.component.html 中:

<form [formGroup]="taskForm">
  <div formArrayName="tasks">
    <div *ngFor="let task of tasks.controls; let i = index">
      <input type="text" [formControlName]="i">
      <button (click)="removeTask(i)">删除任务</button>
    </div>
  </div>
  <button (click)="addTask()">添加任务</button>
  <button (click)="getFormValue()">获取表单值</button>
</form>

这里,我们在 FormGroup 中创建了一个 FormArray,并通过 formArrayName*ngFor 指令在模板中动态渲染和管理任务输入框。

表单验证

表单验证是确保用户输入数据合法性的重要环节。在响应式表单中,验证可以在 FormControlFormGroupFormArray 级别进行。

内置验证器

Angular 提供了许多内置的验证器,如 requiredminLengthmaxLengthpattern 等。下面以 FormControl 为例,展示如何使用内置验证器:

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

@Component({
  selector: 'app - validation - example',
  templateUrl: './validation - example.component.html'
})
export class ValidationExampleComponent {
  emailControl = new FormControl('', {
    validators: [
      { required: true },
      { pattern: /^[a - zA - Z0 - 9_.+-]+@[a - zA - Z0 - 9 -]+\.[a - zA - Z0 - 9 -]+$/ }
    ]
  });

  getErrorMessage() {
    if (this.emailControl.hasError('required')) {
      return '邮箱不能为空';
    }
    if (this.emailControl.hasError('pattern')) {
      return '邮箱格式不正确';
    }
    return '';
  }
}

在模板文件 validation - example.component.html 中:

<input type="text" [formControl]="emailControl">
<div *ngIf="emailControl.hasError('required') || emailControl.hasError('pattern')">
  {{ getErrorMessage() }}
</div>

这里,我们为 emailControl 添加了 requiredpattern 验证器,并在模板中根据验证结果显示错误信息。

自定义验证器

除了内置验证器,我们还可以创建自定义验证器。自定义验证器本质上是一个函数,它接受一个 AbstractControl 参数,并返回一个验证结果对象(如果验证失败)或 null(如果验证成功)。下面是一个自定义密码强度验证器的示例:

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

export function passwordStrengthValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    if (!value) {
      return null;
    }

    const hasUpperCase = /[A - Z]/.test(value);
    const hasLowerCase = /[a - z]/.test(value);
    const hasDigit = /\d/.test(value);

    if (!hasUpperCase ||!hasLowerCase ||!hasDigit) {
      return { passwordStrength: true };
    }

    return null;
  };
}

在组件中使用这个自定义验证器:

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { passwordStrengthValidator } from './password - strength.validator';

@Component({
  selector: 'app - custom - validation - example',
  templateUrl: './custom - validation - example.component.html'
})
export class CustomValidationExampleComponent {
  passwordControl = new FormControl('', {
    validators: [passwordStrengthValidator()]
  });

  getPasswordErrorMessage() {
    if (this.passwordControl.hasError('passwordStrength')) {
      return '密码必须包含大写字母、小写字母和数字';
    }
    return '';
  }
}

在模板文件 custom - validation - example.component.html 中:

<input type="password" [formControl]="passwordControl">
<div *ngIf="passwordControl.hasError('passwordStrength')">
  {{ getPasswordErrorMessage() }}
</div>

通过这种方式,我们实现了一个自定义的密码强度验证器,并在表单中使用它。

表单状态与事件

表单状态

响应式表单的控件和表单组都有多种状态,如 valid(有效)、invalid(无效)、touched(已触摸)、untouched(未触摸)、dirty(已修改)和 pristine(未修改)。这些状态可以帮助我们在模板中根据表单的当前状态进行不同的显示或处理。

例如,我们可以根据表单的有效性来控制提交按钮的禁用状态:

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

@Component({
  selector: 'app - form - state - example',
  templateUrl: './form - state - example.component.html'
})
export class FormStateExampleComponent {
  loginForm = new FormGroup({
    username: new FormControl(''),
    password: new FormControl('')
  });
}

在模板文件 form - state - example.component.html 中:

<form [formGroup]="loginForm">
  <input type="text" formControlName="username">
  <input type="password" formControlName="password">
  <button type="submit" [disabled]="!loginForm.valid">登录</button>
</form>

这里,只有当 loginForm 有效时,登录按钮才会启用。

表单事件

响应式表单还提供了一些事件,如 valueChangesstatusChanges,可以用来监听表单值或状态的变化。

  1. valueChangesvalueChanges 事件会在表单控件的值发生变化时触发。例如,我们可以实时监听输入框的值变化并进行一些处理:
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app - value - changes - example',
  templateUrl: './value - changes - example.component.html'
})
export class ValueChangesExampleComponent {
  searchControl = new FormControl('');

  constructor() {
    this.searchControl.valueChanges.subscribe((value) => {
      console.log('搜索框值变化:', value);
      // 这里可以进行实时搜索等操作
    });
  }
}

在模板文件 value - changes - example.component.html 中:

<input type="text" [formControl]="searchControl">

每次输入框的值发生变化,都会在控制台打印出最新的值。

  1. statusChangesstatusChanges 事件会在表单控件的状态(如 validinvalid 等)发生变化时触发。例如,我们可以根据表单状态的变化来显示不同的提示信息:
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app - status - changes - example',
  templateUrl: './status - changes - example.component.html'
})
export class StatusChangesExampleComponent {
  emailControl = new FormControl('', {
    validators: [
      { required: true },
      { pattern: /^[a - zA - Z0 - 9_.+-]+@[a - zA - Z0 - 9 -]+\.[a - zA - Z0 - 9 -]+$/ }
    ]
  });

  statusMessage = '';

  constructor() {
    this.emailControl.statusChanges.subscribe((status) => {
      if (status === 'valid') {
        this.statusMessage = '邮箱格式正确';
      } else {
        this.statusMessage = '邮箱格式不正确或为空';
      }
    });
  }
}

在模板文件 status - changes - example.component.html 中:

<input type="text" [formControl]="emailControl">
<div>{{ statusMessage }}</div>

emailControl 的状态发生变化时,会根据新的状态更新提示信息。

响应式表单与服务端交互

在实际应用中,响应式表单通常需要与服务端进行交互,如提交表单数据到服务端进行保存或验证。

提交表单数据

假设我们有一个用户注册表单,需要将用户输入的数据发送到服务端。首先,我们创建一个服务来处理与服务端的通信:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

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

  registerUser(userData: any) {
    return this.http.post('/api/register', userData);
  }
}

然后,在组件中使用这个服务来提交表单数据:

import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { UserService } from './user.service';

@Component({
  selector: 'app - register - form',
  templateUrl: './register - form.component.html'
})
export class RegisterFormComponent {
  registerForm = new FormGroup({
    username: new FormControl(''),
    password: new FormControl('')
  });

  constructor(private userService: UserService) {}

  onSubmit() {
    if (this.registerForm.valid) {
      const userData = this.registerForm.value;
      this.userService.registerUser(userData).subscribe((response) => {
        console.log('注册成功:', response);
        // 这里可以进行成功后的处理,如跳转到登录页面等
      }, (error) => {
        console.error('注册失败:', error);
        // 这里可以处理错误,如显示错误信息给用户
      });
    }
  }
}

在模板文件 register - form.component.html 中:

<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
  <input type="text" formControlName="username">
  <input type="password" formControlName="password">
  <button type="submit">注册</button>
</form>

这里,当用户点击注册按钮时,如果表单有效,会将表单数据发送到服务端,并根据服务端的响应进行相应的处理。

从服务端加载数据填充表单

有时候,我们需要从服务端加载已有的数据来填充表单,以便用户进行编辑。例如,在用户编辑个人资料的场景中:

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { UserService } from './user.service';

@Component({
  selector: 'app - edit - profile - form',
  templateUrl: './edit - profile - form.component.html'
})
export class EditProfileFormComponent implements OnInit {
  profileForm: FormGroup;

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getProfile().subscribe((userProfile) => {
      this.profileForm = new FormGroup({
        username: new FormControl(userProfile.username),
        email: new FormControl(userProfile.email)
      });
    });
  }

  onSubmit() {
    if (this.profileForm.valid) {
      const updatedProfile = this.profileForm.value;
      this.userService.updateProfile(updatedProfile).subscribe((response) => {
        console.log('资料更新成功:', response);
      }, (error) => {
        console.error('资料更新失败:', error);
      });
    }
  }
}

在模板文件 edit - profile - form.component.html 中:

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <input type="text" formControlName="username">
  <input type="email" formControlName="email">
  <button type="submit">保存</button>
</form>

这里,在组件初始化时,从服务端获取用户资料并填充到表单中。用户修改并提交表单后,将更新的数据发送回服务端。

高级响应式表单技巧

嵌套表单组

在复杂的表单中,可能需要使用嵌套的表单组。例如,一个包含用户基本信息和地址信息的表单:

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

@Component({
  selector: 'app - nested - form - group - example',
  templateUrl: './nested - form - group - example.component.html'
})
export class NestedFormGroupExampleComponent {
  userForm = new FormGroup({
    basicInfo: new FormGroup({
      username: new FormControl(''),
      email: new FormControl('')
    }),
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      zipCode: new FormControl('')
    })
  });

  getFormValue() {
    return this.userForm.value;
  }
}

在模板文件 nested - form - group - example.component.html 中:

<form [formGroup]="userForm">
  <div formGroupName="basicInfo">
    <input type="text" formControlName="username">
    <input type="email" formControlName="email">
  </div>
  <div formGroupName="address">
    <input type="text" formControlName="street">
    <input type="text" formControlName="city">
    <input type="text" formControlName="zipCode">
  </div>
  <button (click)="getFormValue()">获取表单值</button>
</form>

通过 formGroupName 属性,我们可以在模板中方便地嵌套和管理表单组。

动态表单生成

在某些情况下,我们可能需要根据动态数据生成表单。例如,根据从服务端获取的问卷题目来生成问卷表单。下面是一个简单的示例:

import { Component, OnInit } from '@angular/core';
import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { QuestionService } from './question.service';

@Component({
  selector: 'app - dynamic - form - example',
  templateUrl: './dynamic - form - example.component.html'
})
export class DynamicFormExampleComponent implements OnInit {
  surveyForm: FormGroup;

  constructor(private questionService: QuestionService) {}

  ngOnInit() {
    this.questionService.getQuestions().subscribe((questions) => {
      const formControls: { [key: string]: FormControl } = {};
      questions.forEach((question) => {
        formControls[question.id] = new FormControl('');
      });
      this.surveyForm = new FormGroup(formControls);
    });
  }

  onSubmit() {
    if (this.surveyForm.valid) {
      const surveyData = this.surveyForm.value;
      console.log('提交问卷数据:', surveyData);
      // 这里可以将问卷数据发送到服务端
    }
  }
}

在模板文件 dynamic - form - example.component.html 中:

<form [formGroup]="surveyForm" (ngSubmit)="onSubmit()">
  <div *ngFor="let control of surveyForm.controls | keyvalue">
    <label>{{ control.key }}</label>
    <input type="text" [formControlName]="control.key">
  </div>
  <button type="submit">提交问卷</button>
</form>

这里,通过从服务端获取问卷题目,动态生成表单控件,并在模板中渲染。

通过以上内容,我们全面地了解了 Angular 响应式表单的设计与开发,从基础的表单控件、验证,到表单状态与事件,再到与服务端的交互以及一些高级技巧。掌握这些知识,将能够开发出功能强大且易于维护的表单应用。