响应式表单 #

一、响应式表单概述 #

响应式表单使用模型驱动的方式管理表单状态,提供更灵活的表单控制能力。

text
模板驱动表单                    响应式表单
┌─────────────────┐            ┌─────────────────┐
│     模板        │            │     组件类      │
│  ngModel        │            │  FormControl    │
│  ngForm         │            │  FormGroup      │
└─────────────────┘            │  FormArray      │
                               └─────────────────┘
     简单直观                        灵活可控

二、基础配置 #

2.1 导入ReactiveFormsModule #

typescript
import { ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `...`
})
export class UserFormComponent {}

三、FormControl #

3.1 创建FormControl #

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

@Component({
  selector: 'app-example',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="nameControl" />
    <p>值: {{ nameControl.value }}</p>
    <p>有效: {{ nameControl.valid }}</p>
  `
})
export class ExampleComponent {
  nameControl = new FormControl('');
}

3.2 FormControl配置 #

typescript
// 带初始值
nameControl = new FormControl('初始值');

// 带验证器
nameControl = new FormControl('', [Validators.required, Validators.minLength(3)]);

// 带配置对象
nameControl = new FormControl('', {
  validators: [Validators.required],
  asyncValidators: [this.asyncValidator.bind(this)],
  updateOn: 'blur'
});

// 禁用状态
disabledControl = new FormControl({ value: '', disabled: true });

3.3 FormControl方法 #

typescript
// 获取值
const value = this.nameControl.value;

// 设置值
this.nameControl.setValue('新值');

// 补丁更新(FormGroup中使用)
this.nameControl.patchValue('新值');

// 重置
this.nameControl.reset();
this.nameControl.reset('默认值');

// 启用/禁用
this.nameControl.enable();
this.nameControl.disable();

// 标记状态
this.nameControl.markAsTouched();
this.nameControl.markAsDirty();
this.nameControl.markAsPristine();

// 设置错误
this.nameControl.setErrors({ customError: true });

// 清除错误
this.nameControl.setErrors(null);

四、FormGroup #

4.1 创建FormGroup #

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

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <div>
        <label>姓名:</label>
        <input formControlName="name" />
      </div>
      
      <div>
        <label>邮箱:</label>
        <input formControlName="email" />
      </div>
      
      <button type="submit" [disabled]="userForm.invalid">提交</button>
    </form>
  `
})
export class UserFormComponent {
  userForm = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.minLength(2)]),
    email: new FormControl('', [Validators.required, Validators.email])
  });
  
  onSubmit() {
    if (this.userForm.valid) {
      console.log(this.userForm.value);
    }
  }
}

4.2 FormGroup方法 #

typescript
// 获取值
const value = this.userForm.value;

// 设置值
this.userForm.setValue({
  name: 'John',
  email: 'john@example.com'
});

// 补丁更新(部分更新)
this.userForm.patchValue({
  name: 'John'
});

// 重置
this.userForm.reset();
this.userForm.reset({ name: '', email: '' });

// 获取控件
const nameControl = this.userForm.get('name');

// 检查控件
this.userForm.contains('name');

// 添加/移除控件
this.userForm.addControl('age', new FormControl(''));
this.userForm.removeControl('age');

// 启用/禁用
this.userForm.enable();
this.userForm.disable();

五、FormBuilder #

5.1 使用FormBuilder #

typescript
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `...`
})
export class UserFormComponent {
  constructor(private fb: FormBuilder) {}
  
  userForm = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    age: [null, [Validators.min(18), Validators.max(100)]],
    address: this.fb.group({
      province: [''],
      city: [''],
      detail: ['']
    })
  });
}

5.2 FormBuilder方法 #

typescript
// control
this.fb.control('初始值', Validators.required);

// group
this.fb.group({
  name: ['', Validators.required],
  email: ['', Validators.email]
});

// array
this.fb.array([
  this.fb.control('项目1'),
  this.fb.control('项目2')
]);

// 非空断言
this.fb.group({
  name: this.fb.nonNullable.control('')
});

六、嵌套FormGroup #

6.1 创建嵌套表单 #

typescript
userForm = this.fb.group({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
  address: this.fb.group({
    province: ['', Validators.required],
    city: ['', Validators.required],
    detail: ['']
  })
});

6.2 模板绑定 #

html
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
  <div>
    <label>姓名:</label>
    <input formControlName="name" />
  </div>
  
  <div>
    <label>邮箱:</label>
    <input formControlName="email" />
  </div>
  
  <!-- 嵌套FormGroup -->
  <div formGroupName="address">
    <h3>地址信息</h3>
    
    <div>
      <label>省份:</label>
      <input formControlName="province" />
    </div>
    
    <div>
      <label>城市:</label>
      <input formControlName="city" />
    </div>
    
    <div>
      <label>详细地址:</label>
      <input formControlName="detail" />
    </div>
  </div>
  
  <button type="submit">提交</button>
</form>

6.3 访问嵌套控件 #

typescript
// 获取嵌套控件
const provinceControl = this.userForm.get('address.province');
const addressGroup = this.userForm.get('address') as FormGroup;

七、FormArray #

7.1 创建FormArray #

typescript
userForm = this.fb.group({
  name: ['', Validators.required],
  emails: this.fb.array([
    this.fb.control('', Validators.email)
  ]),
  phones: this.fb.array([
    this.fb.group({
      type: ['mobile'],
      number: ['', Validators.required]
    })
  ])
});

get emails() {
  return this.userForm.get('emails') as FormArray;
}

get phones() {
  return this.userForm.get('phones') as FormArray;
}

7.2 模板绑定 #

html
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
  <div>
    <label>姓名:</label>
    <input formControlName="name" />
  </div>
  
  <!-- FormArray - 简单控件 -->
  <div>
    <label>邮箱列表:</label>
    <div formArrayName="emails">
      <div *ngFor="let email of emails.controls; let i = index">
        <input [formControlName]="i" />
        <button type="button" (click)="removeEmail(i)">删除</button>
      </div>
    </div>
    <button type="button" (click)="addEmail()">添加邮箱</button>
  </div>
  
  <!-- FormArray - FormGroup -->
  <div>
    <label>电话列表:</label>
    <div formArrayName="phones">
      <div *ngFor="let phone of phones.controls; let i = index" [formGroupName]="i">
        <select formControlName="type">
          <option value="mobile">手机</option>
          <option value="home">家庭</option>
          <option value="work">工作</option>
        </select>
        <input formControlName="number" placeholder="电话号码" />
        <button type="button" (click)="removePhone(i)">删除</button>
      </div>
    </div>
    <button type="button" (click)="addPhone()">添加电话</button>
  </div>
  
  <button type="submit">提交</button>
</form>

7.3 FormArray操作 #

typescript
// 添加控件
addEmail() {
  this.emails.push(this.fb.control('', Validators.email));
}

addPhone() {
  this.phones.push(this.fb.group({
    type: ['mobile'],
    number: ['', Validators.required]
  }));
}

// 移除控件
removeEmail(index: number) {
  this.emails.removeAt(index);
}

removePhone(index: number) {
  this.phones.removeAt(index);
}

// 其他操作
// 在指定位置插入
this.emails.insert(0, this.fb.control(''));

// 清空
this.emails.clear();

// 获取长度
const length = this.emails.length;

八、表单验证 #

8.1 内置验证器 #

typescript
import { Validators } from '@angular/forms';

name: ['', [
  Validators.required,
  Validators.minLength(3),
  Validators.maxLength(20)
]]

email: ['', [
  Validators.required,
  Validators.email
]]

age: [null, [
  Validators.min(18),
  Validators.max(100)
]]

phone: ['', [
  Validators.required,
  Validators.pattern(/^1[3-9]\d{9}$/)
]]

8.2 显示验证错误 #

html
<div>
  <label>姓名:</label>
  <input formControlName="name" />
  
  <div *ngIf="nameControl.invalid && nameControl.touched">
    <span *ngIf="nameControl.errors?.['required']">姓名是必填项</span>
    <span *ngIf="nameControl.errors?.['minlength']">
      姓名至少{{ nameControl.errors?.['minlength']?.requiredLength }}个字符
    </span>
  </div>
</div>
typescript
get nameControl() {
  return this.userForm.get('name');
}

8.3 自定义验证器 #

同步验证器:

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

// 函数式验证器
export function forbiddenNameValidator(name: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = name.test(control.value);
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// 使用
name: ['', [
  Validators.required,
  forbiddenNameValidator(/admin/i)
]]

验证器类:

typescript
import { AbstractControl, Validator, NG_VALIDATORS } from '@angular/forms';
import { Directive, Input } from '@angular/core';

@Directive({
  selector: '[appForbiddenName]',
  providers: [{
    provide: NG_VALIDATORS,
    useExisting: ForbiddenNameDirective,
    multi: true
  }]
})
export class ForbiddenNameDirective implements Validator {
  @Input('appForbiddenName') forbiddenName: string;
  
  validate(control: AbstractControl): ValidationErrors | null {
    if (control.value === this.forbiddenName) {
      return { forbiddenName: { value: control.value } };
    }
    return null;
  }
}

8.4 跨字段验证 #

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

export function passwordMatchValidator(): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const password = group.get('password');
    const confirmPassword = group.get('confirmPassword');
    
    if (!password || !confirmPassword) {
      return null;
    }
    
    return password.value === confirmPassword.value 
      ? null 
      : { passwordMismatch: true };
  };
}

// 使用
userForm = this.fb.group({
  password: ['', Validators.required],
  confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator() });

8.5 异步验证器 #

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

export function uniqueEmailValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }
    
    return userService.checkEmailExists(control.value).pipe(
      map(exists => exists ? { emailExists: true } : null),
      catchError(() => of(null))
    );
  };
}

// 使用
email: ['', 
  [Validators.required, Validators.email],
  [uniqueEmailValidator(this.userService)]
]

九、完整示例 #

typescript
import { Component, OnInit } from '@angular/core';
import { 
  FormBuilder, 
  FormGroup, 
  FormArray, 
  Validators,
  ReactiveFormsModule 
} from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <h2>用户注册</h2>
      
      <!-- 基本信息 -->
      <div class="form-section">
        <h3>基本信息</h3>
        
        <div class="form-group">
          <label>用户名:</label>
          <input formControlName="username" />
          <div class="error" *ngIf="username?.invalid && username?.touched">
            <span *ngIf="username?.errors?.['required']">用户名是必填项</span>
            <span *ngIf="username?.errors?.['minlength']">用户名至少3个字符</span>
          </div>
        </div>
        
        <div class="form-group">
          <label>邮箱:</label>
          <input formControlName="email" />
          <div class="error" *ngIf="email?.invalid && email?.touched">
            <span *ngIf="email?.errors?.['required']">邮箱是必填项</span>
            <span *ngIf="email?.errors?.['email']">请输入有效的邮箱</span>
          </div>
        </div>
        
        <div class="form-group">
          <label>密码:</label>
          <input type="password" formControlName="password" />
          <div class="error" *ngIf="password?.invalid && password?.touched">
            <span *ngIf="password?.errors?.['required']">密码是必填项</span>
            <span *ngIf="password?.errors?.['minlength']">密码至少8个字符</span>
          </div>
        </div>
        
        <div class="form-group">
          <label>确认密码:</label>
          <input type="password" formControlName="confirmPassword" />
          <div class="error" *ngIf="userForm.errors?.['passwordMismatch'] && confirmPassword?.touched">
            两次密码不一致
          </div>
        </div>
      </div>
      
      <!-- 地址信息 -->
      <div class="form-section" formGroupName="address">
        <h3>地址信息</h3>
        
        <div class="form-group">
          <label>省份:</label>
          <input formControlName="province" />
        </div>
        
        <div class="form-group">
          <label>城市:</label>
          <input formControlName="city" />
        </div>
        
        <div class="form-group">
          <label>详细地址:</label>
          <input formControlName="detail" />
        </div>
      </div>
      
      <!-- 电话列表 -->
      <div class="form-section">
        <h3>电话列表</h3>
        <div formArrayName="phones">
          <div *ngFor="let phone of phones.controls; let i = index" [formGroupName]="i">
            <select formControlName="type">
              <option value="mobile">手机</option>
              <option value="home">家庭</option>
              <option value="work">工作</option>
            </select>
            <input formControlName="number" placeholder="电话号码" />
            <button type="button" (click)="removePhone(i)">删除</button>
          </div>
        </div>
        <button type="button" (click)="addPhone()">添加电话</button>
      </div>
      
      <button type="submit" [disabled]="userForm.invalid">提交</button>
      <button type="button" (click)="resetForm()">重置</button>
    </form>
    
    <pre>{{ userForm.value | json }}</pre>
  `
})
export class UserFormComponent implements OnInit {
  userForm: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.userForm = this.fb.group({
      username: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
      confirmPassword: ['', Validators.required],
      address: this.fb.group({
        province: [''],
        city: [''],
        detail: ['']
      }),
      phones: this.fb.array([
        this.fb.group({
          type: ['mobile'],
          number: ['', Validators.required]
        })
      ])
    }, { validators: this.passwordMatchValidator });
  }
  
  get username() { return this.userForm.get('username'); }
  get email() { return this.userForm.get('email'); }
  get password() { return this.userForm.get('password'); }
  get confirmPassword() { return this.userForm.get('confirmPassword'); }
  get phones() { return this.userForm.get('phones') as FormArray; }
  
  passwordMatchValidator(group: FormGroup): { [key: string]: boolean } | null {
    const password = group.get('password');
    const confirmPassword = group.get('confirmPassword');
    
    if (!password || !confirmPassword) return null;
    
    return password.value === confirmPassword.value ? null : { passwordMismatch: true };
  }
  
  addPhone() {
    this.phones.push(this.fb.group({
      type: ['mobile'],
      number: ['', Validators.required]
    }));
  }
  
  removePhone(index: number) {
    this.phones.removeAt(index);
  }
  
  onSubmit() {
    if (this.userForm.valid) {
      console.log('表单提交:', this.userForm.value);
    }
  }
  
  resetForm() {
    this.userForm.reset();
  }
}

十、总结 #

概念 说明
FormControl 单个表单控件
FormGroup 表单控件组
FormArray 动态表单控件数组
FormBuilder 简化表单创建
Validators 内置验证器
自定义验证器 自定义验证逻辑
异步验证器 异步验证逻辑

下一步:HttpClient基础

最后更新:2026-03-26