07_核心语法:表单组件

核心语法:表单组件

表单是Web应用中用于收集和处理用户输入的核心组件,Angular提供了两种强大的表单处理方案:模板驱动表单响应式表单。模板驱动表单适合简单场景,通过模板指令管理表单状态;响应式表单则通过代码控制表单逻辑,更适合复杂场景。本章将详细讲解这两种表单的实现方式、验证机制及实战技巧。

1. 模板驱动表单:基于模板的表单处理

模板驱动表单通过Angular内置指令(如ngModel)在模板中定义表单逻辑,适合结构简单、验证规则较少的场景。其核心特点是"声明式"——表单状态由模板自动管理。

1.1 基础用法与双向绑定

使用模板驱动表单需先导入FormsModule

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

@Component({
  selector: 'app-simple-form',
  standalone: true,
  imports: [FormsModule], // 导入FormsModule以使用模板驱动表单
  template: `
    <form #userForm="ngForm" (ngSubmit)="onSubmit(userForm.value)">
      <div>
        <label>姓名:</label>
        <input 
          type="text" 
          name="username" 
          ngModel 
          required 
          #username="ngModel"
        >
        <!-- 错误提示 -->
        @if (username.invalid && username.touched) {
          <span class="error">姓名为必填项</span>
        }
      </div>

      <div>
        <label>邮箱:</label>
        <input 
          type="email" 
          name="email" 
          ngModel 
          required 
          email
          #email="ngModel"
        >
        @if (email.invalid && email.touched) {
          @if (email.errors?.['required']) {
            <span class="error">邮箱为必填项</span>
          }
          @if (email.errors?.['email']) {
            <span class="error">请输入有效的邮箱格式</span>
          }
        }
      </div>

      <button type="submit" [disabled]="userForm.invalid">提交</button>
    </form>
  `
})
export class SimpleFormComponent {
  onSubmit(formValue: any) {
    console.log('表单提交:', formValue);
    // 发送表单数据到服务器
  }
}

核心知识点:

  • #userForm="ngForm":获取表单引用,用于访问表单状态(如invalidvalid
  • ngModel:实现双向绑定,将输入值与表单模型关联
  • name属性:必填,用于标识表单控件,作为表单值对象的键
  • #username="ngModel":获取单个控件的引用,用于访问控件状态(如touchederrors

1.2 表单控件状态

模板驱动表单的控件拥有丰富的状态属性,用于动态控制UI表现:

状态属性 含义
valid 控件验证通过
invalid 控件验证失败
touched 控件被用户交互过(失去焦点)
untouched 控件未被用户交互过
pristine 控件值未被修改过
dirty 控件值已被修改
pending 异步验证正在进行

示例:根据状态动态添加样式

<style>
  input.ng-valid.ng-touched {
    border: 2px solid green;
  }
  input.ng-invalid.ng-touched {
    border: 2px solid red;
  }
  .error {
    color: red;
    font-size: 0.8em;
  }
</style>

1.3 自定义验证器

除了requiredemail等内置验证器,可通过指令创建自定义验证器:

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

// 自定义密码强度验证器
@Directive({
  selector: '[appPasswordStrength]',
  standalone: true,
  providers: [{
    provide: NG_VALIDATORS,
    useExisting: PasswordStrengthDirective,
    multi: true
  }]
})
export class PasswordStrengthDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    const value = control.value as string;
    if (!value) return null; // 空值由required验证器处理
    
    // 验证规则:至少8位,包含数字和字母
    const hasNumber = /\d/.test(value);
    const hasLetter = /[a-zA-Z]/.test(value);
    const isValidLength = value.length >= 8;
    
    const isValid = hasNumber && hasLetter && isValidLength;
    
    return isValid ? null : { 
      passwordStrength: { 
        message: '密码至少8位,需包含数字和字母' 
      } 
    };
  }
}

使用自定义验证器:

<div>
  <label>密码:</label>
  <input 
    type="password" 
    name="password" 
    ngModel 
    required 
    appPasswordStrength
    #password="ngModel"
  >
  @if (password.invalid && password.touched) {
    @if (password.errors?.['required']) {
      <span class="error">密码为必填项</span>
    }
    @if (password.errors?.['passwordStrength']) {
      <span class="error">{{ password.errors['passwordStrength'].message }}</span>
    }
  }
</div>

2. 响应式表单:基于代码的表单控制

响应式表单通过TypeScript代码创建和管理表单控件,表单状态是"可编程的",适合复杂场景(如动态表单、复杂验证、表单状态监听)。Angular v20推荐结合Signals使用响应式表单,实现更高效的状态管理。

2.1 基础用法与FormGroup

使用响应式表单需导入ReactiveFormsModule

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

@Component({
  selector: 'app-reactive-form',
  standalone: true,
  imports: [ReactiveFormsModule], // 导入响应式表单模块
  template: `
    <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
      <div>
        <label>用户名:</label>
        <input 
          type="text" 
          formControlName="username"
        >
        @if (usernameControl.invalid && usernameControl.touched) {
          <span class="error">用户名不能为空</span>
        }
      </div>

      <div>
        <label>密码:</label>
        <input 
          type="password" 
          formControlName="password"
        >
        @if (passwordControl.invalid && passwordControl.touched) {
          @if (passwordControl.errors?.['required']) {
            <span class="error">密码不能为空</span>
          }
          @if (passwordControl.errors?.['minlength']) {
            <span class="error">密码至少6位</span>
          }
        }
      </div>

      <button type="submit" [disabled]="loginForm.invalid">登录</button>
    </form>
  `
})
export class ReactiveFormComponent {
  // 创建表单组
  loginForm = new FormGroup({
    username: new FormControl('', [Validators.required]),
    password: new FormControl('', [Validators.required, Validators.minLength(6)])
  });

  // 获取单个控件的引用(简化模板访问)
  get usernameControl() {
    return this.loginForm.get('username')!;
  }

  get passwordControl() {
    return this.loginForm.get('password')!;
  }

  onSubmit() {
    if (this.loginForm.valid) {
      console.log('表单提交:', this.loginForm.value);
      // 处理登录逻辑
    } else {
      // 触发表单验证
      this.markAllAsTouched();
    }
  }

  // 手动标记所有控件为touched,触发错误提示
  private markAllAsTouched() {
    Object.values(this.loginForm.controls).forEach(control => {
      control.markAsTouched();
    });
  }
}

核心类说明:

  • FormControl:管理单个表单控件的值和状态
  • FormGroup:管理一组FormControl,聚合它们的状态
  • Validators:内置验证器集合(requiredminLength等)

2.2 与Signals结合(v20推荐)

Angular v20支持将表单状态转换为Signals,实现更高效的响应式更新:

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

@Component({
  selector: 'app-signal-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="searchForm" (ngSubmit)="onSearch()">
      <input 
        type="text" 
        formControlName="query"
        placeholder="搜索..."
      >
      <button type="submit" [disabled]="isFormInvalid()">搜索</button>
      <p>搜索历史:{{ recentQueries().join(', ') }}</p>
    </form>
  `
})
export class SignalFormComponent {
  searchForm = new FormGroup({
    query: new FormControl('', [Validators.required, Validators.minLength(2)])
  });

  // 将表单状态转换为Signal
  formValue = toSignal(this.searchForm.valueChanges, { initialValue: { query: '' } });
  formStatus = toSignal(this.searchForm.statusChanges, { initialValue: 'INVALID' });

  // 计算属性:最近的搜索记录
  recentQueries = signal<string[]>([]);

  // 计算属性:表单是否无效
  isFormInvalid = computed(() => {
    return this.formStatus() === 'INVALID';
  });

  onSearch() {
    const query = this.formValue()?.query;
    if (query) {
      console.log('搜索:', query);
      // 更新搜索历史
      this.recentQueries.update(queries => [query, ...queries.slice(0, 4)]);
      // 重置表单
      this.searchForm.reset();
    }
  }
}

toSignal()将表单的valueChangesstatusChanges(Observable)转换为Signals,结合computed()可创建依赖表单状态的响应式计算属性。

2.3 动态表单:动态添加/移除控件

响应式表单支持动态调整表单结构,适合需要动态增减字段的场景(如多选项表单):

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

@Component({
  selector: 'app-dynamic-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="surveyForm">
      <h3>兴趣爱好</h3>
      
      <!-- 动态表单数组 -->
      <div formArrayName="hobbies">
        @for (hobby of hobbies.controls; track $index; let i = $index) {
          <div>
            <input 
              type="text" 
              [formControlName]="i"
              placeholder="请输入兴趣爱好"
            >
            <button type="button" (click)="removeHobby(i)" [disabled]="hobbies.length <= 1">
              删除
            </button>
          </div>
        }
      </div>
      
      <button type="button" (click)="addHobby()">添加兴趣爱好</button>
      <button type="button" (click)="onSave()">保存</button>
    </form>
  `
})
export class DynamicFormComponent {
  // 包含动态表单数组的表单组
  surveyForm = new FormGroup({
    hobbies: new FormArray([
      new FormControl('') // 初始一个控件
    ])
  });

  // 获取表单数组引用
  get hobbies() {
    return this.surveyForm.get('hobbies') as FormArray;
  }

  // 添加新控件
  addHobby() {
    this.hobbies.push(new FormControl(''));
  }

  // 移除指定索引的控件
  removeHobby(index: number) {
    this.hobbies.removeAt(index);
  }

  onSave() {
    console.log('兴趣爱好:', this.hobbies.value);
  }
}

FormArray用于管理动态数量的FormControl,通过push()removeAt()方法动态调整控件数量。

2.4 异步验证器

对于需要后端验证的场景(如检查用户名是否已存在),可使用异步验证器:

import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { delay, map } from 'rxjs/operators';

@Component({
  selector: 'app-async-validation',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <div>
      <label>用户名:</label>
      <input [formControl]="usernameControl">
      
      @if (usernameControl.pending) {
        <span>检查用户名可用性...</span>
      }
      
      @if (usernameControl.invalid && usernameControl.touched) {
        @if (usernameControl.errors?.['required']) {
          <span class="error">用户名不能为空</span>
        }
        @if (usernameControl.errors?.['usernameTaken']) {
          <span class="error">用户名已被占用</span>
        }
      }
    </div>
  `
})
export class AsyncValidationComponent {
  usernameControl = new FormControl('', 
    [Validators.required], // 同步验证器
    [this.checkUsernameAvailability.bind(this)] // 异步验证器
  );

  constructor(private http: HttpClient) {}

  // 异步验证器:检查用户名是否已存在
  checkUsernameAvailability(control: AbstractControl) {
    return this.http.get<{ available: boolean }>(`/api/check-username?name=${control.value}`)
      .pipe(
        delay(1000), // 模拟网络延迟
        map(response => {
          // 验证失败返回错误对象,成功返回null
          return response.available ? null : { usernameTaken: true };
        })
      );
  }
}

异步验证器返回Observable,当服务器返回验证结果后,表单会自动更新状态。

3. 表单提交与数据处理

无论使用哪种表单类型,都需要处理表单提交、数据转换和错误处理等通用场景。

3.1 表单提交最佳实践

  • 禁用无效提交:通过[disabled]="form.invalid"防止用户提交无效表单
  • 手动触发表单验证:提交前检查表单状态,对无效表单标记所有控件为touched
  • 处理提交状态:添加加载状态防止重复提交
onSubmit() {
  if (this.form.invalid) {
    this.markAllAsTouched();
    return;
  }

  this.isSubmitting = true;
  this.apiService.submitData(this.form.value)
    .subscribe({
      next: () => {
        this.isSubmitting = false;
        alert('提交成功');
        this.form.reset();
      },
      error: () => {
        this.isSubmitting = false;
        alert('提交失败,请重试');
      }
    });
}

3.2 表单数据转换

使用valueChanges监听表单数据变化,或通过patchValue()/setValue()更新表单数据:

// 监听数据变化
this.userForm.valueChanges.subscribe(value => {
  console.log('表单数据变化:', value);
});

// 部分更新表单数据
this.userForm.patchValue({
  username: '默认用户名'
});

// 完全替换表单数据(需匹配所有字段)
this.userForm.setValue({
  username: '新用户名',
  email: 'new@example.com'
});

4. 两种表单方案的对比与选型

特性 模板驱动表单 响应式表单
核心思想 声明式,依赖模板 命令式,依赖代码
状态管理 模板自动管理 手动代码控制
灵活性 适合简单场景 适合复杂场景
测试性 难以单元测试 易于单元测试
动态表单 实现复杂 原生支持
与Signals集成 有限 良好

选型建议

  • 简单表单(如登录、注册):优先使用模板驱动表单
  • 复杂表单(如数据录入、动态表单):使用响应式表单
  • 需要频繁监听或修改表单状态:使用响应式表单+Signals

通过本章学习,你已掌握Angular表单的核心用法,能够根据实际场景选择合适的表单方案,实现高效、可维护的用户输入处理逻辑。

posted @ 2025-09-21 18:19  S&L·chuck  阅读(26)  评论(0)    收藏  举报