关于 d2-05-form-page
面向 Angular 开发的技能,用于创建和编辑实体记录的表单页。它说明如何按 CRUD 能力选择模式、加载现有数据,并通过表单验证控制提交。
主要功能
- 01根据路由识别创建或编辑模式
- 02使用 Reactive Forms 构建表单
- 03按字段配置验证规则
- 04编辑时加载并填充记录
- 05校验通过后保存或更新
适用场景
- 01为实体字段创建带验证的表单
- 02复用页面处理新建与编辑
- 03阻止无效表单提交
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ softinter-chiangrai/ up-ai-skill-training --skill d2-05-form-page安装后在 Claude Code 中输入/d2-05-form-page确认已加载。
详细介绍
第 2 天 · 第 5 步 — 基于功能创建/更新的表单页面
注意: 此技能会根据在 d2-03-main-menu 中选择的功能进行调整:
- 如果选择
create:允许创建新记录 - 如果选择
save:允许更新现有记录 - 两者都可以选择,以实现完整的创建/更新功能
路由: 已在 d2-03-main-menu 中定义(完整的 app.routes.ts)。此技能使用:
/<entity>/new路由创建新记录/<entity>/:id/edit路由编辑现有记录
前置条件: 将 ReactiveFormsModule 添加到 app.config.ts:
import { ReactiveFormsModule } from '@angular/forms';
import { provideClientHydration } from '@angular/platform-browser';
import { ApplicationConfig } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
ReactiveFormsModule,
// ... other providers
],
};
up-app/src/app/pages/<entity>-form/<entity>-form.component.ts(包含 FormGroup 的独立组件):
- 导入
ReactiveFormsModule、FormBuilder、FormGroup、Validators。 - 使用实体的每个字段创建包含
FormControl的form: FormGroup,并添加Validators.required和其他规则(例如minLength、min)。 - 初始化时,从路由中读取
id(ActivatedRoute)。没有id(或id === 'new')→ 创建模式(空表单)。存在id→ 编辑模式(调用getById(id),并通过form.patchValue(data)预填表单)。 - 取消按钮 → 调用
router.navigate(['/<entity>']),不保存。 - 保存按钮 → 提交前检查
form.valid;创建模式调用save(form.value),编辑模式调用update(id, form.value);成功后调用router.navigate(['/<entity>'])。 - 表单无效时禁用提交按钮(
[disabled]="form.invalid")。
组件 TypeScript:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { <Entity>Service } from '../../services/<entity>.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-<entity>-form',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
templateUrl: './<entity>-form.component.html',
styleUrls: ['./<entity>-form.component.css']
})
export class <Entity>FormComponent implements OnInit {
form: FormGroup;
isEdit = false;
constructor(
private fb: FormBuilder,
private <entity>Service: <Entity>Service,
private route: ActivatedRoute,
private router: Router
) {
this.form = this.fb.group({
empCode: ['', [Validators.required, Validators.minLength(2)]],
empName: ['', [Validators.required, Validators.minLength(3)]],
address: [''],
phoneNo: ['']
});
}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
if (id && id !== 'new') {
this.isEdit = true;
this.<entity>Service.getById(+id).subscribe(data => {
this.form.patchValue(data);
});
}
}
onSave() {
if (this.form.invalid) return;
const id = this.route.snapshot.paramMap.get('id');
if (this.isEdit && id) {
this.<entity>Service.update(+id, this.form.value).subscribe(() => {
this.router.navigate(['/<entity>']);
});
} else {
this.<entity>Service.save(this.form.value).subscribe(() => {
this.router.navigate(['/<entity>']);
});
}
}
onCancel() {
this.router.navigate(['/<entity>']);
}
}
模板(包含验证、焦点环和标准 Tailwind):
<div class="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full">
<h2 class="text-3xl font-bold mb-6 text-gray-900">{{ isEdit ? "Edit" : "Add New" }} <Entity></h2>
<form [formGroup]="form" (ngSubmit)="onSave()">
<div class="mb-6">
<label class="block font-semibold mb-2 text-gray-700">Employee Code</label>
<input
type="text"
formControlName="empCode"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
placeholder="e.g., EMP001"
/>
@if (form.get("empCode")?.hasError("required") && form.get("empCode")?.touched) {
<p class="text-red-500 text-sm mt-1 font-semibold">Employee Code is required</p>
}
@if (form.get("empCode")?.hasError("minlength") && form.get("empCode")?.touched) {
<p class="text-red-500 text-sm mt-1 font-semibold">Code must be at least 2 characters</p>
}
</div>
<div class="mb-6">
<label class="block font-semibold mb-2 text-gray-700">Employee Name</label>
<input
type="text"
formControlName="empName"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
placeholder="e.g., John Doe"
/>
@if (form.get("empName")?.hasError("required") && form.get("empName")?.touched) {
<p class="text-red-500 text-sm mt-1 font-semibold">Employee Name is required</p>
}
@if (form.get("empName")?.hasError("minlength") && form.get("empName")?.touched) {
<p class="text-red-500 text-sm mt-1 font-semibold">Name must be at least 3 characters</p>
}
</div>
<div class="mb-6">
<label class="block font-semibold mb-2 text-gray-700">Address</label>
<input
type="text"
formControlName="address"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
placeholder="Optional"
/>
</div>
<div class="mb-6">
<label class="block font-semibold mb-2 text-gray-700">Phone Number</label>
<input
type="text"
formControlName="phoneNo"
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
placeholder="Optional"
/>
</div>
<div class="flex gap-4 mt-8">
<button
type="submit"
[disabled]="form.invalid"
class="flex-1 bg-green-500 hover:bg-green-600 disabled:bg-gray-300 text-white font-semibold px-6 py-3 rounded-lg transition disabled:cursor-not-allowed"
>
Save
</button>
<button
type="button"
(click)="onCancel()"
class="flex-1 bg-gray-400 hover:bg-gray-500 text-white font-semibold px-6 py-3 rounded-lg transition"
>
Cancel
</button>
</div>
</form>
</div>
</div>
表单自定义:
- 将
empCode、empName、address、phoneNo替换为实体的实际属性名称和验证器 - 添加或移除表单字段,以匹配实体结构
- 根据各字段调整验证器规则(required、minLength、pattern 等)
验证器参考:
Validators.required:字段不能为空Validators.minLength(n):最小长度要求Validators.pattern(regex):自定义正则表达式匹配Validators.min(n)/Validators.max(n):数值范围验证
错误显示:
- 仅当字段有错误且已被触碰时,显示错误消息
- 使用
form.get('fieldName')?.hasError('errorType')检查特定错误
评论
支持 Markdown