Skip to content

10 - 模板驱动表单

📖 学习目标

通过本章节学习,您将掌握:

  • 模板驱动表单基础
  • 表单验证
  • 双向数据绑定
  • 表单状态管理
  • 自定义验证器
  • 表单提交处理

🎯 核心概念

1. 什么是模板驱动表单?

模板驱动表单是Angular提供的一种表单处理方式,特点:

  • 声明式:在模板中定义表单结构
  • 双向绑定:使用ngModel实现数据同步
  • 简单易用:适合简单的表单场景
  • 自动验证:内置验证器自动工作

2. 表单指令

typescript
// 核心指令
ngForm        // 表单指令
ngModel       // 双向绑定
ngModelGroup  // 模型组
ngSubmit      // 提交事件

3. 表单状态

typescript
// 表单状态
form.valid        // 表单是否有效
form.invalid      // 表单是否无效
form.pristine     // 表单是否未修改
form.dirty        // 表单是否已修改
form.touched      // 表单是否已触摸
form.untouched    // 表单是否未触摸
form.submitted    // 表单是否已提交

🏗️ 基础表单创建

1. 启用FormsModule

typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // 导入FormsModule

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule // 添加到imports数组
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

2. 基本表单结构

html
<!-- 基本表单 -->
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
  <div class="form-group">
    <label for="name">姓名:</label>
    <input 
      type="text" 
      id="name" 
      name="name" 
      [(ngModel)]="user.name"
      required
      minlength="2"
      #nameInput="ngModel">
    
    <!-- 验证错误信息 -->
    <div *ngIf="nameInput.invalid && (nameInput.dirty || nameInput.touched)" 
         class="error-messages">
      <div *ngIf="nameInput.errors?.['required']">姓名是必填项</div>
      <div *ngIf="nameInput.errors?.['minlength']">
        姓名至少需要{{ nameInput.errors?.['minlength'].requiredLength }}个字符
      </div>
    </div>
  </div>
  
  <div class="form-group">
    <label for="email">邮箱:</label>
    <input 
      type="email" 
      id="email" 
      name="email" 
      [(ngModel)]="user.email"
      required
      email
      #emailInput="ngModel">
    
    <div *ngIf="emailInput.invalid && (emailInput.dirty || emailInput.touched)" 
         class="error-messages">
      <div *ngIf="emailInput.errors?.['required']">邮箱是必填项</div>
      <div *ngIf="emailInput.errors?.['email']">请输入有效的邮箱地址</div>
    </div>
  </div>
  
  <div class="form-group">
    <label for="age">年龄:</label>
    <input 
      type="number" 
      id="age" 
      name="age" 
      [(ngModel)]="user.age"
      required
      min="18"
      max="100"
      #ageInput="ngModel">
    
    <div *ngIf="ageInput.invalid && (ageInput.dirty || ageInput.touched)" 
         class="error-messages">
      <div *ngIf="ageInput.errors?.['required']">年龄是必填项</div>
      <div *ngIf="ageInput.errors?.['min']">年龄不能小于18岁</div>
      <div *ngIf="ageInput.errors?.['max']">年龄不能大于100岁</div>
    </div>
  </div>
  
  <button type="submit" [disabled]="userForm.invalid">提交</button>
</form>

3. 组件代码

typescript
// user-form.component.ts
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';

export interface User {
  name: string;
  email: string;
  age: number;
  role: string;
  interests: string[];
}

@Component({
  selector: 'app-user-form',
  templateUrl: './user-form.component.html',
  styleUrls: ['./user-form.component.css']
})
export class UserFormComponent implements OnInit {
  user: User = {
    name: '',
    email: '',
    age: 0,
    role: 'user',
    interests: []
  };
  
  roles = [
    { value: 'user', label: '普通用户' },
    { value: 'admin', label: '管理员' },
    { value: 'moderator', label: '版主' }
  ];
  
  interests = [
    { value: 'programming', label: '编程' },
    { value: 'reading', label: '阅读' },
    { value: 'sports', label: '运动' },
    { value: 'music', label: '音乐' }
  ];
  
  constructor() {}
  
  ngOnInit() {}
  
  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log('表单数据:', this.user);
      // 这里可以调用服务提交数据
      // this.userService.createUser(this.user).subscribe(...)
    } else {
      console.log('表单验证失败');
      this.markFormGroupTouched(form);
    }
  }
  
  onInterestChange(interest: string, isChecked: boolean) {
    if (isChecked) {
      this.user.interests.push(interest);
    } else {
      const index = this.user.interests.indexOf(interest);
      if (index > -1) {
        this.user.interests.splice(index, 1);
      }
    }
  }
  
  isInterestSelected(interest: string): boolean {
    return this.user.interests.includes(interest);
  }
  
  private markFormGroupTouched(form: NgForm) {
    Object.keys(form.controls).forEach(key => {
      const control = form.controls[key];
      control.markAsTouched();
    });
  }
}

🔧 高级表单功能

1. 表单组

html
<!-- 使用ngModelGroup分组 -->
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
  <!-- 基本信息组 -->
  <fieldset ngModelGroup="basicInfo">
    <legend>基本信息</legend>
    
    <div class="form-group">
      <label for="firstName">名:</label>
      <input 
        type="text" 
        id="firstName" 
        name="firstName" 
        [(ngModel)]="user.firstName"
        required
        #firstNameInput="ngModel">
    </div>
    
    <div class="form-group">
      <label for="lastName">姓:</label>
      <input 
        type="text" 
        id="lastName" 
        name="lastName" 
        [(ngModel)]="user.lastName"
        required
        #lastNameInput="ngModel">
    </div>
  </fieldset>
  
  <!-- 联系信息组 -->
  <fieldset ngModelGroup="contactInfo">
    <legend>联系信息</legend>
    
    <div class="form-group">
      <label for="email">邮箱:</label>
      <input 
        type="email" 
        id="email" 
        name="email" 
        [(ngModel)]="user.email"
        required
        email
        #emailInput="ngModel">
    </div>
    
    <div class="form-group">
      <label for="phone">电话:</label>
      <input 
        type="tel" 
        id="phone" 
        name="phone" 
        [(ngModel)]="user.phone"
        pattern="[0-9]{11}"
        #phoneInput="ngModel">
    </div>
  </fieldset>
  
  <button type="submit" [disabled]="userForm.invalid">提交</button>
</form>

2. 选择框和复选框

html
<!-- 下拉选择框 -->
<div class="form-group">
  <label for="role">角色:</label>
  <select 
    id="role" 
    name="role" 
    [(ngModel)]="user.role"
    required
    #roleSelect="ngModel">
    <option value="">请选择角色</option>
    <option *ngFor="let role of roles" [value]="role.value">
      {{ role.label }}
    </option>
  </select>
  
  <div *ngIf="roleSelect.invalid && roleSelect.touched" class="error-messages">
    <div *ngIf="roleSelect.errors?.['required']">请选择角色</div>
  </div>
</div>

<!-- 单选按钮组 -->
<div class="form-group">
  <label>性别:</label>
  <div class="radio-group">
    <label>
      <input 
        type="radio" 
        name="gender" 
        value="male" 
        [(ngModel)]="user.gender">

    </label>
    <label>
      <input 
        type="radio" 
        name="gender" 
        value="female" 
        [(ngModel)]="user.gender">

    </label>
  </div>
</div>

<!-- 复选框组 -->
<div class="form-group">
  <label>兴趣爱好:</label>
  <div class="checkbox-group">
    <label *ngFor="let interest of interests">
      <input 
        type="checkbox" 
        [value]="interest.value"
        [checked]="isInterestSelected(interest.value)"
        (change)="onInterestChange(interest.value, $event.target.checked)">
      {{ interest.label }}
    </label>
  </div>
</div>

3. 文本域和文件上传

html
<!-- 文本域 -->
<div class="form-group">
  <label for="bio">个人简介:</label>
  <textarea 
    id="bio" 
    name="bio" 
    [(ngModel)]="user.bio"
    rows="4"
    cols="50"
    maxlength="500"
    #bioTextarea="ngModel">
  </textarea>
  <div class="char-count">
    {{ user.bio?.length || 0 }}/500
  </div>
</div>

<!-- 文件上传 -->
<div class="form-group">
  <label for="avatar">头像:</label>
  <input 
    type="file" 
    id="avatar" 
    name="avatar" 
    accept="image/*"
    (change)="onFileSelected($event)"
    #fileInput>
  
  <div *ngIf="selectedFile" class="file-info">
    <p>已选择文件: {{ selectedFile.name }}</p>
    <p>文件大小: {{ formatFileSize(selectedFile.size) }}</p>
  </div>
</div>

🎯 表单验证

1. 内置验证器

html
<!-- 必填验证 -->
<input 
  type="text" 
  name="name" 
  [(ngModel)]="user.name"
  required
  #nameInput="ngModel">

<!-- 最小长度验证 -->
<input 
  type="text" 
  name="name" 
  [(ngModel)]="user.name"
  minlength="2"
  #nameInput="ngModel">

<!-- 最大长度验证 -->
<input 
  type="text" 
  name="name" 
  [(ngModel)]="user.name"
  maxlength="50"
  #nameInput="ngModel">

<!-- 邮箱格式验证 -->
<input 
  type="email" 
  name="email" 
  [(ngModel)]="user.email"
  email
  #emailInput="ngModel">

<!-- 数字范围验证 -->
<input 
  type="number" 
  name="age" 
  [(ngModel)]="user.age"
  min="18"
  max="100"
  #ageInput="ngModel">

<!-- 正则表达式验证 -->
<input 
  type="tel" 
  name="phone" 
  [(ngModel)]="user.phone"
  pattern="[0-9]{11}"
  #phoneInput="ngModel">

2. 自定义验证器

typescript
// custom-validators.ts
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 hasNumeric = /[0-9]/.test(value);
    const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value);
    
    const valid = hasUpperCase && hasLowerCase && hasNumeric && hasSpecialChar;
    return valid ? null : { passwordStrength: true };
  };
}

// 确认密码验证器
export function confirmPasswordValidator(passwordField: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.parent) return null;
    
    const password = control.parent.get(passwordField)?.value;
    const confirmPassword = control.value;
    
    return password === confirmPassword ? null : { passwordMismatch: true };
  };
}

// 用户名唯一性验证器(异步)
export function uniqueUsernameValidator(userService: any): ValidatorFn {
  return (control: AbstractControl): Promise<ValidationErrors | null> => {
    if (!control.value) return Promise.resolve(null);
    
    return userService.checkUsernameExists(control.value).then((exists: boolean) => {
      return exists ? { usernameExists: true } : null;
    });
  };
}

3. 使用自定义验证器

html
<!-- 密码输入 -->
<div class="form-group">
  <label for="password">密码:</label>
  <input 
    type="password" 
    id="password" 
    name="password" 
    [(ngModel)]="user.password"
    required
    minlength="8"
    [ngModelOptions]="{ updateOn: 'blur' }"
    #passwordInput="ngModel">
  
  <div *ngIf="passwordInput.invalid && passwordInput.touched" class="error-messages">
    <div *ngIf="passwordInput.errors?.['required']">密码是必填项</div>
    <div *ngIf="passwordInput.errors?.['minlength']">
      密码至少需要{{ passwordInput.errors?.['minlength'].requiredLength }}个字符
    </div>
    <div *ngIf="passwordInput.errors?.['passwordStrength']">
      密码必须包含大写字母、小写字母、数字和特殊字符
    </div>
  </div>
</div>

<!-- 确认密码 -->
<div class="form-group">
  <label for="confirmPassword">确认密码:</label>
  <input 
    type="password" 
    id="confirmPassword" 
    name="confirmPassword" 
    [(ngModel)]="user.confirmPassword"
    required
    [ngModelOptions]="{ updateOn: 'blur' }"
    #confirmPasswordInput="ngModel">
  
  <div *ngIf="confirmPasswordInput.invalid && confirmPasswordInput.touched" class="error-messages">
    <div *ngIf="confirmPasswordInput.errors?.['required']">请确认密码</div>
    <div *ngIf="confirmPasswordInput.errors?.['passwordMismatch']">两次输入的密码不一致</div>
  </div>
</div>

🎮 实践练习

练习1:创建用户注册表单

创建一个完整的用户注册表单,包含:

  • 基本信息(姓名、邮箱、电话)
  • 密码设置(密码、确认密码)
  • 个人偏好(角色、兴趣爱好)
  • 完整的验证和错误提示

练习2:创建动态表单

创建一个可以动态添加/删除字段的表单,支持:

  • 动态添加联系信息
  • 动态添加工作经历
  • 表单验证和提交

📚 详细示例

完整的用户注册表单

html
<!-- user-registration.component.html -->
<form #registrationForm="ngForm" (ngSubmit)="onSubmit(registrationForm)" class="registration-form">
  <h2>用户注册</h2>
  
  <!-- 基本信息 -->
  <fieldset class="form-section">
    <legend>基本信息</legend>
    
    <div class="form-row">
      <div class="form-group">
        <label for="firstName">名 *</label>
        <input 
          type="text" 
          id="firstName" 
          name="firstName" 
          [(ngModel)]="user.firstName"
          required
          minlength="2"
          maxlength="20"
          #firstNameInput="ngModel"
          class="form-control"
          [class.is-invalid]="firstNameInput.invalid && firstNameInput.touched">
        
        <div *ngIf="firstNameInput.invalid && firstNameInput.touched" class="invalid-feedback">
          <div *ngIf="firstNameInput.errors?.['required']">请输入您的名字</div>
          <div *ngIf="firstNameInput.errors?.['minlength']">
            名字至少需要{{ firstNameInput.errors?.['minlength'].requiredLength }}个字符
          </div>
          <div *ngIf="firstNameInput.errors?.['maxlength']">
            名字不能超过{{ firstNameInput.errors?.['maxlength'].requiredLength }}个字符
          </div>
        </div>
      </div>
      
      <div class="form-group">
        <label for="lastName">姓 *</label>
        <input 
          type="text" 
          id="lastName" 
          name="lastName" 
          [(ngModel)]="user.lastName"
          required
          minlength="2"
          maxlength="20"
          #lastNameInput="ngModel"
          class="form-control"
          [class.is-invalid]="lastNameInput.invalid && lastNameInput.touched">
        
        <div *ngIf="lastNameInput.invalid && lastNameInput.touched" class="invalid-feedback">
          <div *ngIf="lastNameInput.errors?.['required']">请输入您的姓氏</div>
          <div *ngIf="lastNameInput.errors?.['minlength']">
            姓氏至少需要{{ lastNameInput.errors?.['minlength'].requiredLength }}个字符
          </div>
          <div *ngIf="lastNameInput.errors?.['maxlength']">
            姓氏不能超过{{ lastNameInput.errors?.['maxlength'].requiredLength }}个字符
          </div>
        </div>
      </div>
    </div>
    
    <div class="form-group">
      <label for="email">邮箱地址 *</label>
      <input 
        type="email" 
        id="email" 
        name="email" 
        [(ngModel)]="user.email"
        required
        email
        #emailInput="ngModel"
        class="form-control"
        [class.is-invalid]="emailInput.invalid && emailInput.touched">
      
      <div *ngIf="emailInput.invalid && emailInput.touched" class="invalid-feedback">
        <div *ngIf="emailInput.errors?.['required']">请输入邮箱地址</div>
        <div *ngIf="emailInput.errors?.['email']">请输入有效的邮箱地址</div>
      </div>
    </div>
    
    <div class="form-group">
      <label for="phone">手机号码</label>
      <input 
        type="tel" 
        id="phone" 
        name="phone" 
        [(ngModel)]="user.phone"
        pattern="[0-9]{11}"
        #phoneInput="ngModel"
        class="form-control"
        [class.is-invalid]="phoneInput.invalid && phoneInput.touched">
      
      <div *ngIf="phoneInput.invalid && phoneInput.touched" class="invalid-feedback">
        <div *ngIf="phoneInput.errors?.['pattern']">请输入11位手机号码</div>
      </div>
    </div>
  </fieldset>
  
  <!-- 账户信息 -->
  <fieldset class="form-section">
    <legend>账户信息</legend>
    
    <div class="form-group">
      <label for="username">用户名 *</label>
      <input 
        type="text" 
        id="username" 
        name="username" 
        [(ngModel)]="user.username"
        required
        minlength="3"
        maxlength="20"
        pattern="[a-zA-Z0-9_]+"
        #usernameInput="ngModel"
        class="form-control"
        [class.is-invalid]="usernameInput.invalid && usernameInput.touched">
      
      <div *ngIf="usernameInput.invalid && usernameInput.touched" class="invalid-feedback">
        <div *ngIf="usernameInput.errors?.['required']">请输入用户名</div>
        <div *ngIf="usernameInput.errors?.['minlength']">
          用户名至少需要{{ usernameInput.errors?.['minlength'].requiredLength }}个字符
        </div>
        <div *ngIf="usernameInput.errors?.['maxlength']">
          用户名不能超过{{ usernameInput.errors?.['maxlength'].requiredLength }}个字符
        </div>
        <div *ngIf="usernameInput.errors?.['pattern']">
          用户名只能包含字母、数字和下划线
        </div>
      </div>
    </div>
    
    <div class="form-group">
      <label for="password">密码 *</label>
      <input 
        type="password" 
        id="password" 
        name="password" 
        [(ngModel)]="user.password"
        required
        minlength="8"
        #passwordInput="ngModel"
        class="form-control"
        [class.is-invalid]="passwordInput.invalid && passwordInput.touched">
      
      <div *ngIf="passwordInput.invalid && passwordInput.touched" class="invalid-feedback">
        <div *ngIf="passwordInput.errors?.['required']">请输入密码</div>
        <div *ngIf="passwordInput.errors?.['minlength']">
          密码至少需要{{ passwordInput.errors?.['minlength'].requiredLength }}个字符
        </div>
      </div>
      
      <div class="password-requirements">
        <small>密码要求:</small>
        <ul>
          <li [class.valid]="hasUpperCase">至少包含一个大写字母</li>
          <li [class.valid]="hasLowerCase">至少包含一个小写字母</li>
          <li [class.valid]="hasNumber">至少包含一个数字</li>
          <li [class.valid]="hasSpecialChar">至少包含一个特殊字符</li>
        </ul>
      </div>
    </div>
    
    <div class="form-group">
      <label for="confirmPassword">确认密码 *</label>
      <input 
        type="password" 
        id="confirmPassword" 
        name="confirmPassword" 
        [(ngModel)]="user.confirmPassword"
        required
        #confirmPasswordInput="ngModel"
        class="form-control"
        [class.is-invalid]="confirmPasswordInput.invalid && confirmPasswordInput.touched">
      
      <div *ngIf="confirmPasswordInput.invalid && confirmPasswordInput.touched" class="invalid-feedback">
        <div *ngIf="confirmPasswordInput.errors?.['required']">请确认密码</div>
        <div *ngIf="confirmPasswordInput.errors?.['passwordMismatch']">两次输入的密码不一致</div>
      </div>
    </div>
  </fieldset>
  
  <!-- 个人偏好 -->
  <fieldset class="form-section">
    <legend>个人偏好</legend>
    
    <div class="form-group">
      <label>性别</label>
      <div class="radio-group">
        <label class="radio-label">
          <input 
            type="radio" 
            name="gender" 
            value="male" 
            [(ngModel)]="user.gender">
          <span>男</span>
        </label>
        <label class="radio-label">
          <input 
            type="radio" 
            name="gender" 
            value="female" 
            [(ngModel)]="user.gender">
          <span>女</span>
        </label>
        <label class="radio-label">
          <input 
            type="radio" 
            name="gender" 
            value="other" 
            [(ngModel)]="user.gender">
          <span>其他</span>
        </label>
      </div>
    </div>
    
    <div class="form-group">
      <label for="birthDate">出生日期</label>
      <input 
        type="date" 
        id="birthDate" 
        name="birthDate" 
        [(ngModel)]="user.birthDate"
        #birthDateInput="ngModel"
        class="form-control">
    </div>
    
    <div class="form-group">
      <label>兴趣爱好</label>
      <div class="checkbox-group">
        <label *ngFor="let interest of interests" class="checkbox-label">
          <input 
            type="checkbox" 
            [value]="interest.value"
            [checked]="isInterestSelected(interest.value)"
            (change)="onInterestChange(interest.value, $event.target.checked)">
          <span>{{ interest.label }}</span>
        </label>
      </div>
    </div>
    
    <div class="form-group">
      <label for="bio">个人简介</label>
      <textarea 
        id="bio" 
        name="bio" 
        [(ngModel)]="user.bio"
        rows="4"
        maxlength="500"
        placeholder="介绍一下自己..."
        class="form-control">
      </textarea>
      <div class="char-count">
        {{ user.bio?.length || 0 }}/500
      </div>
    </div>
  </fieldset>
  
  <!-- 服务条款 -->
  <div class="form-group">
    <label class="checkbox-label">
      <input 
        type="checkbox" 
        name="agreeTerms" 
        [(ngModel)]="user.agreeTerms"
        required
        #agreeTermsInput="ngModel">
      <span>我同意 <a href="#" target="_blank">服务条款</a> 和 <a href="#" target="_blank">隐私政策</a></span>
    </label>
    
    <div *ngIf="agreeTermsInput.invalid && agreeTermsInput.touched" class="invalid-feedback">
      <div *ngIf="agreeTermsInput.errors?.['required']">请同意服务条款和隐私政策</div>
    </div>
  </div>
  
  <!-- 提交按钮 -->
  <div class="form-actions">
    <button 
      type="submit" 
      [disabled]="registrationForm.invalid || isSubmitting"
      class="btn btn-primary">
      <span *ngIf="isSubmitting" class="spinner"></span>
      {{ isSubmitting ? '注册中...' : '注册' }}
    </button>
    
    <button 
      type="button" 
      (click)="resetForm(registrationForm)"
      class="btn btn-secondary">
      重置
    </button>
  </div>
</form>
typescript
// user-registration.component.ts
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';

export interface User {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  username: string;
  password: string;
  confirmPassword: string;
  gender: string;
  birthDate: string;
  interests: string[];
  bio: string;
  agreeTerms: boolean;
}

@Component({
  selector: 'app-user-registration',
  templateUrl: './user-registration.component.html',
  styleUrls: ['./user-registration.component.css']
})
export class UserRegistrationComponent implements OnInit {
  user: User = {
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
    username: '',
    password: '',
    confirmPassword: '',
    gender: '',
    birthDate: '',
    interests: [],
    bio: '',
    agreeTerms: false
  };
  
  interests = [
    { value: 'programming', label: '编程' },
    { value: 'reading', label: '阅读' },
    { value: 'sports', label: '运动' },
    { value: 'music', label: '音乐' },
    { value: 'travel', label: '旅行' },
    { value: 'photography', label: '摄影' },
    { value: 'cooking', label: '烹饪' },
    { value: 'gaming', label: '游戏' }
  ];
  
  isSubmitting = false;
  
  constructor() {}
  
  ngOnInit() {}
  
  onSubmit(form: NgForm) {
    if (form.valid) {
      this.isSubmitting = true;
      
      // 模拟API调用
      setTimeout(() => {
        console.log('注册用户:', this.user);
        this.isSubmitting = false;
        alert('注册成功!');
        this.resetForm(form);
      }, 2000);
    } else {
      this.markFormGroupTouched(form);
    }
  }
  
  resetForm(form: NgForm) {
    form.resetForm();
    this.user = {
      firstName: '',
      lastName: '',
      email: '',
      phone: '',
      username: '',
      password: '',
      confirmPassword: '',
      gender: '',
      birthDate: '',
      interests: [],
      bio: '',
      agreeTerms: false
    };
  }
  
  onInterestChange(interest: string, isChecked: boolean) {
    if (isChecked) {
      this.user.interests.push(interest);
    } else {
      const index = this.user.interests.indexOf(interest);
      if (index > -1) {
        this.user.interests.splice(index, 1);
      }
    }
  }
  
  isInterestSelected(interest: string): boolean {
    return this.user.interests.includes(interest);
  }
  
  // 密码强度检查
  get hasUpperCase(): boolean {
    return /[A-Z]/.test(this.user.password);
  }
  
  get hasLowerCase(): boolean {
    return /[a-z]/.test(this.user.password);
  }
  
  get hasNumber(): boolean {
    return /[0-9]/.test(this.user.password);
  }
  
  get hasSpecialChar(): boolean {
    return /[!@#$%^&*(),.?":{}|<>]/.test(this.user.password);
  }
  
  private markFormGroupTouched(form: NgForm) {
    Object.keys(form.controls).forEach(key => {
      const control = form.controls[key];
      control.markAsTouched();
    });
  }
}

✅ 学习检查

完成本章节后,请确认您能够:

  • [ ] 创建模板驱动表单
  • [ ] 使用ngModel进行双向绑定
  • [ ] 实现表单验证
  • [ ] 处理表单提交
  • [ ] 使用表单组
  • [ ] 创建自定义验证器

🚀 下一步

完成本章节学习后,请继续学习11-响应式表单