模板驱动表单 #

一、表单概述 #

Angular提供两种表单处理方式:

类型 特点 适用场景
模板驱动表单 基于模板,简单直观 简单表单、快速开发
响应式表单 基于模型,灵活可控 复杂表单、动态表单

二、基础配置 #

2.1 导入FormsModule #

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

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

三、基本表单 #

3.1 简单表单 #

typescript
@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
      <div>
        <label for="name">姓名:</label>
        <input 
          type="text" 
          id="name" 
          name="name" 
          [(ngModel)]="user.name"
        />
      </div>
      
      <div>
        <label for="email">邮箱:</label>
        <input 
          type="email" 
          id="email" 
          name="email" 
          [(ngModel)]="user.email"
        />
      </div>
      
      <button type="submit">提交</button>
    </form>
  `
})
export class UserFormComponent {
  user = {
    name: '',
    email: ''
  };
  
  onSubmit(form: NgForm) {
    console.log('表单值:', form.value);
    console.log('用户数据:', this.user);
  }
}

3.2 ngForm指令 #

html
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
  <!-- 表单内容 -->
</form>

NgForm属性:

属性 说明
value 表单值对象
valid 表单是否有效
invalid 表单是否无效
pristine 表单是否未被修改
dirty 表单是否已被修改
touched 表单是否被触摸
untouched 表单未被触摸

3.3 ngModel指令 #

html
<!-- 基本用法 -->
<input [(ngModel)]="user.name" name="name" />

<!-- 添加ngModel以参与表单验证 -->
<input [(ngModel)]="user.name" name="name" ngModel />

<!-- 单向绑定 -->
<input [ngModel]="user.name" name="name" />
<input (ngModelChange)="onNameChange($event)" name="name" />

四、表单验证 #

4.1 内置验证器 #

html
<form #userForm="ngForm">
  <!-- 必填验证 -->
  <input 
    type="text" 
    name="name" 
    [(ngModel)]="user.name"
    required
    #nameInput="ngModel"
  />
  
  <!-- 最小长度 -->
  <input 
    type="text" 
    name="username" 
    [(ngModel)]="user.username"
    required
    minlength="3"
    #usernameInput="ngModel"
  />
  
  <!-- 最大长度 -->
  <input 
    type="text" 
    name="bio" 
    [(ngModel)]="user.bio"
    maxlength="200"
    #bioInput="ngModel"
  />
  
  <!-- 邮箱验证 -->
  <input 
    type="email" 
    name="email" 
    [(ngModel)]="user.email"
    required
    email
    #emailInput="ngModel"
  />
  
  <!-- 最小值/最大值 -->
  <input 
    type="number" 
    name="age" 
    [(ngModel)]="user.age"
    min="18"
    max="100"
    #ageInput="ngModel"
  />
  
  <!-- 正则验证 -->
  <input 
    type="text" 
    name="phone" 
    [(ngModel)]="user.phone"
    pattern="[0-9]{11}"
    #phoneInput="ngModel"
  />
</form>

4.2 显示验证错误 #

html
<form #userForm="ngForm">
  <div>
    <label>姓名:</label>
    <input 
      type="text" 
      name="name" 
      [(ngModel)]="user.name"
      required
      minlength="2"
      #nameInput="ngModel"
    />
    
    <!-- 显示错误 -->
    <div *ngIf="nameInput.invalid && nameInput.touched">
      <span *ngIf="nameInput.errors?.['required']">
        姓名是必填项
      </span>
      <span *ngIf="nameInput.errors?.['minlength']">
        姓名至少需要2个字符
      </span>
    </div>
  </div>
</form>

4.3 验证状态样式 #

html
<style>
  input.ng-valid.ng-touched {
    border-color: green;
  }
  
  input.ng-invalid.ng-touched {
    border-color: red;
  }
  
  .error-message {
    color: red;
    font-size: 12px;
  }
</style>

<form #userForm="ngForm">
  <input 
    type="text" 
    name="name" 
    [(ngModel)]="user.name"
    required
    #nameInput="ngModel"
    [class.is-valid]="nameInput.valid && nameInput.touched"
    [class.is-invalid]="nameInput.invalid && nameInput.touched"
  />
</form>

五、完整表单示例 #

typescript
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-registration-form',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <form #registrationForm="ngForm" (ngSubmit)="onSubmit(registrationForm)">
      <h2>用户注册</h2>
      
      <!-- 用户名 -->
      <div class="form-group">
        <label for="username">用户名 *</label>
        <input 
          type="text" 
          id="username"
          name="username" 
          [(ngModel)]="user.username"
          required
          minlength="3"
          maxlength="20"
          #usernameInput="ngModel"
        />
        <div class="error" *ngIf="usernameInput.invalid && usernameInput.touched">
          <span *ngIf="usernameInput.errors?.['required']">用户名是必填项</span>
          <span *ngIf="usernameInput.errors?.['minlength']">
            用户名至少3个字符
          </span>
        </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 class="error" *ngIf="emailInput.invalid && emailInput.touched">
          <span *ngIf="emailInput.errors?.['required']">邮箱是必填项</span>
          <span *ngIf="emailInput.errors?.['email']">请输入有效的邮箱地址</span>
        </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"
        />
        <div class="error" *ngIf="passwordInput.invalid && passwordInput.touched">
          <span *ngIf="passwordInput.errors?.['required']">密码是必填项</span>
          <span *ngIf="passwordInput.errors?.['minlength']">
            密码至少8个字符
          </span>
        </div>
      </div>
      
      <!-- 确认密码 -->
      <div class="form-group">
        <label for="confirmPassword">确认密码 *</label>
        <input 
          type="password" 
          id="confirmPassword"
          name="confirmPassword" 
          [(ngModel)]="user.confirmPassword"
          required
          #confirmPasswordInput="ngModel"
        />
        <div class="error" *ngIf="confirmPasswordInput.touched">
          <span *ngIf="confirmPasswordInput.errors?.['required']">
            请确认密码
          </span>
          <span *ngIf="user.password !== user.confirmPassword && !confirmPasswordInput.errors?.['required']">
            两次密码不一致
          </span>
        </div>
      </div>
      
      <!-- 年龄 -->
      <div class="form-group">
        <label for="age">年龄</label>
        <input 
          type="number" 
          id="age"
          name="age" 
          [(ngModel)]="user.age"
          min="18"
          max="100"
          #ageInput="ngModel"
        />
        <div class="error" *ngIf="ageInput.invalid && ageInput.touched">
          <span *ngIf="ageInput.errors?.['min']">年龄不能小于18岁</span>
          <span *ngIf="ageInput.errors?.['max']">年龄不能超过100岁</span>
        </div>
      </div>
      
      <!-- 性别 -->
      <div class="form-group">
        <label>性别</label>
        <div>
          <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>
          <label *ngFor="let hobby of hobbies">
            <input 
              type="checkbox" 
              name="hobbies" 
              [value]="hobby"
              [(ngModel)]="user.hobbies"
            /> {{ hobby }}
          </label>
        </div>
      </div>
      
      <!-- 城市 -->
      <div class="form-group">
        <label for="city">城市</label>
        <select 
          id="city"
          name="city" 
          [(ngModel)]="user.city"
          required
          #cityInput="ngModel"
        >
          <option value="">请选择</option>
          <option *ngFor="let city of cities" [value]="city">
            {{ city }}
          </option>
        </select>
        <div class="error" *ngIf="cityInput.invalid && cityInput.touched">
          <span *ngIf="cityInput.errors?.['required']">请选择城市</span>
        </div>
      </div>
      
      <!-- 简介 -->
      <div class="form-group">
        <label for="bio">简介</label>
        <textarea 
          id="bio"
          name="bio" 
          [(ngModel)]="user.bio"
          maxlength="200"
          #bioInput="ngModel"
        ></textarea>
        <div class="hint">{{ user.bio?.length || 0 }}/200</div>
      </div>
      
      <!-- 同意条款 -->
      <div class="form-group">
        <label>
          <input 
            type="checkbox" 
            name="agree" 
            [(ngModel)]="user.agree"
            required
            #agreeInput="ngModel"
          /> 我同意用户条款
        </label>
        <div class="error" *ngIf="agreeInput.invalid && agreeInput.touched">
          <span>请同意用户条款</span>
        </div>
      </div>
      
      <!-- 提交按钮 -->
      <button 
        type="submit" 
        [disabled]="registrationForm.invalid">
        注册
      </button>
      
      <!-- 表单状态 -->
      <div class="form-status">
        <p>表单状态:</p>
        <p>有效: {{ registrationForm.valid }}</p>
        <p>已修改: {{ registrationForm.dirty }}</p>
        <p>已触摸: {{ registrationForm.touched }}</p>
      </div>
    </form>
  `,
  styles: [`
    .form-group { margin-bottom: 15px; }
    .form-group label { display: block; margin-bottom: 5px; }
    .form-group input, .form-group select, .form-group textarea {
      width: 100%;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    .error { color: red; font-size: 12px; margin-top: 5px; }
    .hint { color: #666; font-size: 12px; }
    button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; }
    button:disabled { background: #ccc; }
  `]
})
export class RegistrationFormComponent {
  user = {
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    age: null,
    gender: '',
    hobbies: [],
    city: '',
    bio: '',
    agree: false
  };
  
  hobbies = ['阅读', '运动', '音乐', '旅行', '游戏'];
  cities = ['北京', '上海', '广州', '深圳', '杭州'];
  
  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log('表单提交:', form.value);
      console.log('用户数据:', this.user);
    }
  }
}

六、ngModelGroup #

6.1 分组表单 #

html
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
  <!-- 地址分组 -->
  <div ngModelGroup="address" #addressGroup="ngModelGroup">
    <h3>地址信息</h3>
    
    <div>
      <label>省份:</label>
      <input 
        type="text" 
        name="province" 
        [(ngModel)]="user.address.province"
        required
      />
    </div>
    
    <div>
      <label>城市:</label>
      <input 
        type="text" 
        name="city" 
        [(ngModel)]="user.address.city"
        required
      />
    </div>
    
    <div>
      <label>详细地址:</label>
      <input 
        type="text" 
        name="detail" 
        [(ngModel)]="user.address.detail"
      />
    </div>
    
    <div *ngIf="addressGroup.invalid && addressGroup.touched">
      请填写完整的地址信息
    </div>
  </div>
  
  <button type="submit">提交</button>
</form>

6.2 表单值结构 #

typescript
// 表单值
{
  address: {
    province: '北京',
    city: '北京',
    detail: '朝阳区xxx'
  }
}

七、动态表单 #

7.1 动态添加表单控件 #

typescript
@Component({
  selector: 'app-dynamic-form',
  template: `
    <form #form="ngForm" (ngSubmit)="onSubmit(form)">
      <div *ngFor="let field of fields; let i = index">
        <label>{{ field.label }}:</label>
        <input 
          [type]="field.type"
          [name]="field.name"
          [(ngModel)]="model[field.name]"
          [required]="field.required"
        />
        <button type="button" (click)="removeField(i)">删除</button>
      </div>
      
      <button type="button" (click)="addField()">添加字段</button>
      <button type="submit">提交</button>
    </form>
  `
})
export class DynamicFormComponent {
  fields = [
    { name: 'name', label: '姓名', type: 'text', required: true },
    { name: 'email', label: '邮箱', type: 'email', required: true }
  ];
  
  model: any = {};
  
  addField() {
    const index = this.fields.length + 1;
    this.fields.push({
      name: `field${index}`,
      label: `字段${index}`,
      type: 'text',
      required: false
    });
  }
  
  removeField(index: number) {
    const fieldName = this.fields[index].name;
    delete this.model[fieldName];
    this.fields.splice(index, 1);
  }
  
  onSubmit(form: NgForm) {
    console.log(form.value);
  }
}

八、表单重置 #

8.1 重置表单 #

typescript
@Component({
  template: `
    <form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
      <!-- 表单内容 -->
      <button type="submit">提交</button>
      <button type="button" (click)="resetForm(userForm)">重置</button>
    </form>
  `
})
export class UserFormComponent {
  user = { name: '', email: '' };
  
  onSubmit(form: NgForm) {
    console.log(form.value);
    this.resetForm(form);
  }
  
  resetForm(form: NgForm) {
    form.reset();
    // 或重置为特定值
    // form.resetForm({ name: '', email: '' });
  }
}

九、模板驱动表单最佳实践 #

9.1 适用场景 #

  • 简单表单
  • 快速原型开发
  • 表单逻辑简单

9.2 注意事项 #

typescript
// 推荐:使用独立导入
@Component({
  imports: [FormsModule]
})

// 为每个输入添加name属性
<input name="fieldName" [(ngModel)]="value" />

// 使用模板引用变量访问验证状态
<input #inputRef="ngModel" />

// 提交前检查表单有效性
onSubmit(form: NgForm) {
  if (form.valid) {
    // 提交逻辑
  }
}

十、总结 #

概念 说明
ngForm 表单指令,管理整个表单
ngModel 双向数据绑定
ngModelGroup 表单分组
验证器 required, minlength, maxlength, email, pattern, min, max
验证状态 valid, invalid, touched, untouched, dirty, pristine

下一步:响应式表单

最后更新:2026-03-26