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

响应式表单的Angular表单组与表单控件

2021-12-212.0k 阅读

响应式表单基础概念

在深入探讨 Angular 响应式表单的表单组与表单控件之前,我们先来了解一些基础概念。响应式表单是 Angular 提供的一种构建表单的方式,它基于 observable 流来管理表单数据和状态变化。与模板驱动表单不同,响应式表单在组件类中构建和管理表单模型,这使得表单的创建和更新更加灵活和可控。

响应式表单主要由两个核心部分组成:表单控件(FormControl)和表单组(FormGroup)。表单控件用于处理单个输入字段,而表单组则用于组合多个表单控件,以便对一组相关的表单字段进行整体管理。

表单控件(FormControl)

1. 创建表单控件

在 Angular 中,要创建一个表单控件,我们使用 FormControl 类。可以在组件的构造函数中进行初始化。例如,假设我们要创建一个用于输入用户名的表单控件:

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

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

在上述代码中,我们创建了一个名为 usernameControl 的表单控件,并初始化为空字符串。FormControl 构造函数接受一个初始值作为参数,如果不传递,则初始值为 null

2. 表单控件的属性

  • value:用于获取或设置表单控件的值。例如:
// 获取值
const username = this.usernameControl.value;
// 设置值
this.usernameControl.setValue('newUsername');
  • valid:判断表单控件的值是否有效。有效性取决于所设置的验证器,例如:
if (this.usernameControl.valid) {
  // 执行相关逻辑
}
  • touched:表示表单控件是否被用户触摸(聚焦并失去聚焦)过。这在显示错误信息时非常有用,比如只有当用户输入并离开输入框后才显示错误信息:
if (this.usernameControl.touched &&!this.usernameControl.valid) {
  // 显示错误信息
}
  • dirty:表示表单控件的值是否被用户修改过。

3. 表单控件的验证

验证是表单中非常重要的一部分,它确保用户输入的数据符合特定的规则。Angular 提供了一些内置的验证器,例如 requiredminLengthmaxLength 等。我们可以在创建表单控件时添加这些验证器。

以用户名输入框为例,要求用户名不能为空且长度至少为 3 个字符:

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

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

  get usernameError() {
    if (this.usernameControl.hasError('required')) {
      return '用户名不能为空';
    }
    if (this.usernameControl.hasError('minLength')) {
      return '用户名长度至少为 3 个字符';
    }
    return '';
  }
}

在上述代码中,我们通过 Validators.requiredValidators.minLength(3) 分别添加了必填和最小长度验证。get usernameError() 方法用于根据验证结果返回相应的错误信息。在模板中,可以这样使用:

<input type="text" [formControl]="usernameControl">
<div *ngIf="usernameControl.touched && usernameError">{{ usernameError }}</div>

4. 监听表单控件的变化

表单控件的值变化或状态变化时,我们常常需要执行一些操作。Angular 提供了 valueChangesstatusChanges 两个 observable 来监听这些变化。

  • valueChanges:当表单控件的值发生变化时,会发出新的值。例如,我们要实时根据用户名的输入来更新页面上的显示:
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  usernameControl = new FormControl('');
  displayedUsername = '';

  constructor() {
    this.usernameControl.valueChanges.subscribe((value) => {
      this.displayedUsername = value;
    });
  }
}
<input type="text" [formControl]="usernameControl">
<p>显示的用户名: {{ displayedUsername }}</p>
  • statusChanges:当表单控件的状态(如 validinvalidpristinedirty 等)发生变化时,会发出新的状态。例如,我们要根据表单控件的有效性来显示不同的样式:
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  usernameControl = new FormControl('');
  isInvalid = false;

  constructor() {
    this.usernameControl.statusChanges.subscribe((status) => {
      this.isInvalid = status === 'INVALID';
    });
  }
}
<input type="text" [formControl]="usernameControl" [ngClass]="{ 'invalid': isInvalid }">

表单组(FormGroup)

1. 创建表单组

表单组用于将多个表单控件组合在一起,形成一个逻辑上的组。例如,一个登录表单可能包含用户名和密码两个表单控件,我们可以将它们组合在一个表单组中。

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

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  loginForm = new FormGroup({
    username: new FormControl('', [
      Validators.required,
      Validators.minLength(3)
    ]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(6)
    ])
  });
}

在上述代码中,我们创建了一个名为 loginForm 的表单组,其中包含 usernamepassword 两个表单控件,并分别为它们添加了验证器。

2. 表单组的属性

  • value:获取整个表单组的值,它是一个包含所有表单控件值的对象。例如:
const formValues = this.loginForm.value;
// formValues 类似 { username: 'user', password: 'pass' }
  • valid:判断表单组是否有效。只有当表单组中的所有表单控件都有效时,表单组才有效。例如:
if (this.loginForm.valid) {
  // 提交表单等操作
}
  • controls:通过该属性可以访问表单组中的各个表单控件。例如,要获取 username 表单控件:
const usernameControl = this.loginForm.controls['username'];

3. 监听表单组的变化

与表单控件类似,表单组也提供了 valueChangesstatusChanges 来监听变化。

  • valueChanges:当表单组中任何一个表单控件的值发生变化时,会发出包含整个表单组最新值的对象。例如:
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  loginForm = new FormGroup({
    username: new FormControl(''),
    password: new FormControl('')
  });

  constructor() {
    this.loginForm.valueChanges.subscribe((values) => {
      console.log('表单值变化:', values);
    });
  }
}
  • statusChanges:当表单组中任何一个表单控件的状态发生变化时,会发出表单组的最新状态。例如:
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  loginForm = new FormGroup({
    username: new FormControl(''),
    password: new FormControl('')
  });

  constructor() {
    this.loginForm.statusChanges.subscribe((status) => {
      console.log('表单状态变化:', status);
    });
  }
}

4. 嵌套表单组

在复杂的表单中,可能需要嵌套表单组。例如,一个用户注册表单可能包含个人信息和联系方式两个部分,每个部分都可以作为一个表单组进行管理。

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

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
})
export class RegisterComponent {
  registerForm = new FormGroup({
    personalInfo: new FormGroup({
      firstName: new FormControl('', Validators.required),
      lastName: new FormControl('', Validators.required)
    }),
    contactInfo: new FormGroup({
      email: new FormControl('', [Validators.required, Validators.email]),
      phone: new FormControl('', [Validators.required, Validators.pattern('^[0-9]{11}$')])
    })
  });
}

在上述代码中,registerForm 包含了 personalInfocontactInfo 两个嵌套的表单组。在模板中,可以这样使用:

<form [formGroup]="registerForm">
  <div formGroupName="personalInfo">
    <label for="firstName">名字:</label>
    <input type="text" id="firstName" formControlName="firstName">
    <label for="lastName">姓氏:</label>
    <input type="text" id="lastName" formControlName="lastName">
  </div>
  <div formGroupName="contactInfo">
    <label for="email">邮箱:</label>
    <input type="email" id="email" formControlName="email">
    <label for="phone">电话:</label>
    <input type="text" id="phone" formControlName="phone">
  </div>
</form>

表单数组(FormArray)

在某些情况下,我们需要动态地添加或删除表单控件,这时表单数组就派上用场了。表单数组是 FormGroup 的一种特殊形式,它专门用于管理一组相同类型的表单控件。

1. 创建表单数组

假设我们要创建一个可以动态添加爱好的表单,每个爱好都是一个表单控件,我们可以使用表单数组来实现。

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

@Component({
  selector: 'app-hobbies',
  templateUrl: './hobbies.component.html',
  styleUrls: ['./hobbies.component.css']
})
export class HobbiesComponent {
  hobbiesForm = new FormGroup({
    hobbies: new FormArray([
      new FormControl('')
    ])
  });

  get hobbies() {
    return this.hobbiesForm.get('hobbies') as FormArray;
  }

  addHobby() {
    this.hobbies.push(new FormControl(''));
  }
}

在上述代码中,我们在 hobbiesForm 中创建了一个名为 hobbies 的表单数组,并初始化为包含一个空的表单控件。get hobbies() 方法用于方便地获取表单数组,addHobby() 方法用于向表单数组中添加新的表单控件。

2. 在模板中使用表单数组

在模板中,我们可以通过 *ngFor 指令来遍历表单数组并显示每个表单控件。

<form [formGroup]="hobbiesForm">
  <div formArrayName="hobbies">
    <div *ngFor="let hobby of hobbies.controls; let i = index">
      <label for="hobby{{ i }}">爱好 {{ i + 1 }}:</label>
      <input type="text" [formControlName]="i" id="hobby{{ i }}">
    </div>
    <button type="button" (click)="addHobby()">添加爱好</button>
  </div>
</form>

3. 删除表单数组中的控件

要删除表单数组中的某个表单控件,可以使用 removeAt(index) 方法。例如,我们在模板中添加一个删除按钮:

<form [formGroup]="hobbiesForm">
  <div formArrayName="hobbies">
    <div *ngFor="let hobby of hobbies.controls; let i = index">
      <label for="hobby{{ i }}">爱好 {{ i + 1 }}:</label>
      <input type="text" [formControlName]="i" id="hobby{{ i }}">
      <button type="button" (click)="hobbies.removeAt(i)">删除</button>
    </div>
    <button type="button" (click)="addHobby()">添加爱好</button>
  </div>
</form>

表单的提交与重置

1. 表单提交

在 Angular 响应式表单中,当表单有效时,我们通常会提交表单数据。可以在模板中绑定 (ngSubmit) 事件,并在组件类中定义相应的提交方法。

以登录表单为例:

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

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  loginForm = new FormGroup({
    username: new FormControl('', [
      Validators.required,
      Validators.minLength(3)
    ]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(6)
    ])
  });

  onSubmit() {
    if (this.loginForm.valid) {
      const { username, password } = this.loginForm.value;
      // 这里可以进行登录逻辑,如发送 HTTP 请求
      console.log('登录信息:', { username, password });
    }
  }
}

在模板中:

<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
  <label for="username">用户名:</label>
  <input type="text" id="username" formControlName="username">
  <label for="password">密码:</label>
  <input type="password" id="password" formControlName="password">
  <button type="submit">登录</button>
</form>

2. 表单重置

有时候,我们需要重置表单,将表单控件的值恢复到初始状态。可以使用表单组的 reset() 方法。例如,在登录表单中添加一个重置按钮:

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

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  loginForm = new FormGroup({
    username: new FormControl('', [
      Validators.required,
      Validators.minLength(3)
    ]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(6)
    ])
  });

  onSubmit() {
    if (this.loginForm.valid) {
      const { username, password } = this.loginForm.value;
      // 这里可以进行登录逻辑,如发送 HTTP 请求
      console.log('登录信息:', { username, password });
    }
  }

  onReset() {
    this.loginForm.reset();
  }
}

在模板中:

<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
  <label for="username">用户名:</label>
  <input type="text" id="username" formControlName="username">
  <label for="password">密码:</label>
  <input type="password" id="password" formControlName="password">
  <button type="submit">登录</button>
  <button type="button" (click)="onReset()">重置</button>
</form>

自定义验证器

除了使用 Angular 提供的内置验证器外,我们还可以创建自定义验证器来满足特定的验证需求。

1. 创建同步自定义验证器

假设我们要创建一个验证用户名是否包含特定字符串的自定义验证器。

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

export function customUsernameValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    if (value && value.includes('admin')) {
      return { customUsernameError: '用户名不能包含 "admin"' };
    }
    return null;
  };
}

然后在表单控件中使用这个自定义验证器:

import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { customUsernameValidator } from './custom-validators';

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

  get usernameError() {
    if (this.usernameControl.hasError('required')) {
      return '用户名不能为空';
    }
    if (this.usernameControl.hasError('customUsernameError')) {
      return '用户名不能包含 "admin"';
    }
    return '';
  }
}

2. 创建异步自定义验证器

异步验证器常用于需要通过网络请求来验证数据的场景,比如验证用户名是否已存在。

import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, delay } from 'rxjs/operators';

export function asyncUsernameValidator(): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    const username = control.value;
    // 模拟网络请求
    return of({})
    .pipe(
        delay(1000),
        map(() => {
          if (username === 'existingUser') {
            return { asyncUsernameError: '用户名已存在' };
          }
          return null;
        })
      );
  };
}

在表单控件中使用异步自定义验证器:

import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { asyncUsernameValidator } from './async-custom-validators';

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

  get usernameError() {
    if (this.usernameControl.hasError('required')) {
      return '用户名不能为空';
    }
    if (this.usernameControl.hasError('asyncUsernameError')) {
      return '用户名已存在';
    }
    return '';
  }
}

响应式表单与服务端交互

在实际应用中,响应式表单的数据通常需要发送到服务端进行处理。我们可以使用 Angular 的 HttpClient 模块来发送 HTTP 请求。

以登录表单为例,假设服务端有一个 /login 的接口用于处理登录请求:

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

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
  loginForm = new FormGroup({
    username: new FormControl('', [
      Validators.required,
      Validators.minLength(3)
    ]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(6)
    ])
  });

  constructor(private http: HttpClient) {}

  onSubmit() {
    if (this.loginForm.valid) {
      const { username, password } = this.loginForm.value;
      this.http.post('/login', { username, password })
      .subscribe((response) => {
          console.log('登录成功:', response);
        }, (error) => {
          console.error('登录失败:', error);
        });
    }
  }
}

在上述代码中,我们在组件的构造函数中注入了 HttpClient,然后在 onSubmit 方法中,当表单有效时,使用 http.post 方法向 /login 接口发送登录数据,并处理响应结果。

总结与最佳实践

  • 合理组织表单结构:根据表单的复杂程度,合理使用表单组、表单数组来组织表单控件,使表单结构清晰,易于维护。
  • 及时显示错误信息:结合表单控件的 touchedvalid 属性,在用户输入并离开输入框后及时显示错误信息,提升用户体验。
  • 验证顺序:对于同步验证器,按照添加的顺序依次执行验证。对于异步验证器,在同步验证通过后才会执行。确保验证逻辑的正确性和顺序合理性。
  • 表单状态管理:利用表单组和表单控件的各种状态属性(如 validdirtytouched 等)来控制表单的交互,例如禁用提交按钮、显示加载状态等。
  • 代码复用:将常用的验证器、表单逻辑封装成可复用的函数或服务,提高代码的可维护性和复用性。

通过深入理解和掌握 Angular 响应式表单的表单组与表单控件,我们能够构建出灵活、健壮且用户体验良好的表单,为前端应用的交互性和数据完整性提供有力保障。在实际项目中,结合具体需求,运用上述知识和最佳实践,能够高效地完成表单相关功能的开发。