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>
错误原因分析
- 控件实例重复:TS代码中同时创建了
profileForm表单组和独立的title、description等FormControl,两者是完全独立的实例,模板中混用绑定会导致状态不互通。 - 绑定方式错误:大部分输入字段没有正确绑定到
profileForm的控件,导致Angular无法追踪字段的touched等状态。 - 错误判断逻辑有误:用
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
相关产品推荐
相关产品推荐

