You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular响应式表单:仅在字段触碰后显示验证错误

Angular表单验证:仅在字段触碰且无效时显示错误提示

问题描述

我开发了一个Angular课程表单组件,需求是仅当输入字段被用户触碰(获得焦点后失去焦点)且仍处于无效状态时,才显示对应的验证错误提示。但目前表单刚加载完成就直接显示错误信息,尝试结合touched属性做判断后也没生效,求正确的实现方案。

现有代码

Component.ts 文件

import { Component } from "@angular/core";
import {
  FormBuilder,
  FormControl,
  FormGroup,
  Validators,
} from "@angular/forms";
import { FaIconLibrary } from "@fortawesome/angular-fontawesome";
import { fas } from "@fortawesome/free-solid-svg-icons";

@Component({
  selector: "app-course-form",
  templateUrl: "./course-form.component.html",
  styleUrls: ["./course-form.component.scss"],
})
export class CourseFormComponent {
  constructor(
    public fb: FormBuilder,
    public library: FaIconLibrary
  ) {
    library.addIconPacks(fas);
  }

  profileForm = this.fb.group({
    title: ["", [Validators.required, Validators.minLength(2)]],
    description: ["", [Validators.required, Validators.minLength(10)]],
    author: ["", [Validators.required, Validators.pattern(/^[a-zA-Z0-9 ]+$/)]],
    duration: [0, [Validators.required, Validators.min(1)]],
  });

  courseForm!: FormGroup;
  title = new FormControl("");
  description = new FormControl("");
  author = new FormControl("");
  duration = new FormControl(0);

  authors: any[] = [];
  durationText: string = "";
  submitted = false;

  removeAuthor(author: string) {
    this.authors = this.authors.filter((a) => a !== author);
  }

  getDurationFormatted(minutes: number): string {
    const hours = Math.floor(minutes / 60);
    const mins = minutes % 60;
    return `${hours.toString().padStart(2, "0")}:${mins
      .toString()
      .padStart(2, "0")}`;
  }
}

HTML模板文件

<h2>Create / Edit Course</h2>
<form [formGroup]="profileForm">
  <div class="app-plate">
    <div class="course">
      <h3>Main Info</h3>
      <div class="form__control">
        <label for="title">Title</label>
        <input type="text" id="title" placeholder="Input text" />
        <span
          *ngIf="profileForm.invalid"
          id="titleErrorMessage"
          class="text-danger"
          >Title is required.</span
        >
      </div>
      <div class="form__control">
        <label for="description">Description</label>
        <textarea
          id="description"
          placeholder="Input text"
          rows="4"
          cols="5"
        ></textarea>
        <span
          *ngIf="profileForm.invalid && profileForm.value.description"
          id="descriptionErrorMessage"
          class="text-danger"
          >Description is required.</span
        >
      </div>
      <div class="separator"></div>
      <h3>Authors</h3>
      <div class="form__control">
        <div class="course__flex">
          <input type="text" id="author" placeholder="Input Author Name" />
          <app-button
            id="createAuthor"
            button_text="Create author"
          ></app-button>
        </div>
        <span *ngIf="" id="authorErrorMessage"
          >New author should contain only latin letters and numbers.</span
        >
        <div class="course__authors">
          <ul class="authors-list">
            <li
              *ngFor="let author of authors; track: author"
              class="authors-list-item"
            >
              Id: {{ author.id }}. Name: {{ author.name }}
              <app-button button_text="Remove" (click)="removeAuthor(author)">
              </app-button>
            </li>
          </ul>
        </div>
      </div>
      <div class="separator"></div>
      <h3>Duration</h3>
      <div class="form__control">
        <div class="course__flex">
          <input
            type="number"
            id="duration"
            placeholder="Input duration"
            formControlName="duration"
          />
          <div class="course__duration">
            <strong> </strong>
            hours
          </div>
        </div>
        <span *ngIf="" id="durationErrorMessage" class="text-danger"
          >Duration is required and must be greater than 0.</span
        >
      </div>
      <div class="form__action"></div>
    </div>
  </div>
</form>
<div class="buttons-container">
  <app-button button_text="Cancel"></app-button>
  <app-button button_text="Create course"></app-button>
</div>

错误原因分析

  1. 控件实例重复:TS代码中同时创建了profileForm表单组和独立的title、description等FormControl,两者是完全独立的实例,模板中混用绑定会导致状态不互通。
  2. 绑定方式错误:大部分输入字段没有正确绑定到profileForm的控件,导致Angular无法追踪字段的touched等状态。
  3. 错误判断逻辑有误:用profileForm.invalid(整个表单的无效状态)代替单个控件的无效状态,且没有正确关联控件的touched属性。

解决方案

步骤1:清理TS代码

删除独立的title、description、author、duration这些FormControl,只保留profileForm表单组,避免实例冲突:

// 删除以下代码块
courseForm!: FormGroup;
title = new FormControl("");
description = new FormControl("");
author = new FormControl("");
duration = new FormControl(0);

步骤2:模板中正确绑定控件

所有输入字段都通过formControlName绑定到profileForm对应的控件,确保状态能被Angular追踪:
比如标题输入框:

<input type="text" id="title" placeholder="Input text" formControlName="title" />

步骤3:修正错误提示的判断逻辑

针对每个控件,判断控件.invalid && 控件.touched,同时可以细化不同验证规则的错误提示(比如区分必填和最小长度):
可以在TS中定义getter简化模板代码:

get title() {
  return this.profileForm.get('title');
}

模板中对应错误提示:

<span *ngIf="title?.invalid && title?.touched" class="text-danger">
  <span *ngIf="title?.errors?.['required']">Title is required.</span>
  <span *ngIf="title?.errors?.['minlength']">Title must be at least 2 characters long.</span>
</span>

步骤4:处理表单提交场景(可选)

如果希望用户点击提交按钮后,自动标记所有控件为touched并显示错误,可以添加提交方法:

onSubmit() {
  this.submitted = true;
  if (this.profileForm.invalid) {
    // 标记所有控件为已触碰
    this.profileForm.markAllAsTouched();
    return;
  }
  // 表单有效,执行提交逻辑
  console.log(this.profileForm.value);
}

模板中绑定提交事件:

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <!-- 表单内容 -->
  <div class="buttons-container">
    <app-button button_text="Cancel"></app-button>
    <app-button button_text="Create course" type="submit"></app-button>
  </div>
</form>

完整修正后的代码示例

修正后的Component.ts

import { Component } from "@angular/core";
import {
  FormBuilder,
  FormGroup,
  Validators,
} from "@angular/forms";
import { FaIconLibrary } from "@fortawesome/angular-fontawesome";
import { fas } from "@fortawesome/free-solid-svg-icons";

@Component({
  selector: "app-course-form",
  templateUrl: "./course-form.component.html",
  styleUrls: ["./course-form.component.scss"],
})
export class CourseFormComponent {
  profileForm: FormGroup;
  authors: any[] = [];
  durationText: string = "";
  submitted = false;

  constructor(
    private fb: FormBuilder,
    private library: FaIconLibrary
  ) {
    library.addIconPacks(fas);
    // 初始化表单
    this.profileForm = this.fb.group({
      title: ["", [Validators.required, Validators.minLength(2)]],
      description: ["", [Validators.required, Validators.minLength(10)]],
      author: ["", [Validators.required, Validators.pattern(/^[a-zA-Z0-9 ]+$/)]],
      duration: [0, [Validators.required, Validators.min(1)]],
    });
  }

  // 定义控件getter,简化模板代码
  get title() { return this.profileForm.get('title'); }
  get description() { return this.profileForm.get('description'); }
  get author() { return this.profileForm.get('author'); }
  get duration() { return this.profileForm.get('duration'); }

  removeAuthor(author: string) {
    this.authors = this.authors.filter((a) => a !== author);
  }

  getDurationFormatted(minutes: number): string {
    const hours = Math.floor(minutes / 60);
    const mins = minutes % 60;
    return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
  }

  onSubmit() {
    this.submitted = true;
    if (this.profileForm.invalid) {
      this.profileForm.markAllAsTouched();
      return;
    }
    // 执行提交逻辑,比如调用API
    console.log("Course data:", this.profileForm.value);
  }
}

修正后的HTML模板

<h2>Create / Edit Course</h2>
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <div class="app-plate">
    <div class="course">
      <h3>Main Info</h3>
      <div class="form__control">
        <label for="title">Title</label>
        <input type="text" id="title" placeholder="Input text" formControlName="title" />
        <span *ngIf="title?.invalid && title?.touched" class="text-danger">
          <span *ngIf="title?.errors?.['required']">Title is required.</span>
          <span *ngIf="title?.errors?.['minlength']">Title must be at least 2 characters long.</span>
        </span>
      </div>
      <div class="form__control">
        <label for="description">Description</label>
        <textarea
          id="description"
          placeholder="Input text"
          rows="4"
          cols="5"
          formControlName="description"
        ></textarea>
        <span *ngIf="description?.invalid && description?.touched" class="text-danger">
          <span *ngIf="description?.errors?.['required']">Description is required.</span>
          <span *ngIf="description?.errors?.['minlength']">Description must be at least 10 characters long.</span>
        </span>
      </div>
      <div class="separator"></div>
      <h3>Authors</h3>
      <div class="form__control">
        <div class="course__flex">
          <input type="text" id="author" placeholder="Input Author Name" formControlName="author" />
          <app-button
            id="createAuthor"
            button_text="Create author"
            [disabled]="author?.invalid"
          ></app-button>
        </div>
        <span *ngIf="author?.invalid && author?.touched" class="text-danger">
          <span *ngIf="author?.errors?.['required']">Author name is required.</span>
          <span *ngIf="author?.errors?.['pattern']">Author name can only contain letters, numbers and spaces.</span>
        </span>
        <div class="course__authors">
          <ul class="authors-list">
            <li
              *ngFor="let author of authors; trackBy: trackByAuthorId"
              class="authors-list-item"
            >
              Id: {{ author.id }}. Name: {{ author.name }}
              <app-button button_text="Remove" (click)="removeAuthor(author)">
              </app-button>
            </li>
          </ul>
        </div>
      </div>
      <div class="separator"></div>
      <h3>Duration</h3>
      <div class="form__control">
        <div class="course__flex">
          <input
            type="number"
            id="duration"
            placeholder="Input duration"
            formControlName="duration"
          />
          <div class="course__duration">
            <strong>{{ getDurationFormatted(duration?.value || 0) }}</strong>
            hours
          </div>
        </div>
        <span *ngIf="duration?.invalid && duration?.touched" class="text-danger">
          <span *ngIf="duration?.errors?.['required']">Duration is required.</span>
          <span *ngIf="duration?.errors?.['min']">Duration must be greater than 0.</span>
        </span>
      </div>
    </div>
  </div>
  <div class="buttons-container">
    <app-button button_text="Cancel"></app-button>
    <app-button button_text="Create course" type="submit"></app-button>
  </div>
</form>

内容的提问来源于stack exchange,提问作者user28273827332

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 06:07:32