mcpField
D
d2-05-form-page

d2-05-form-page

github.com/softinter-chiangrai开发040 次安装9 周前更新
代码仓库

关于 d2-05-form-page

面向 Angular 开发的技能,用于创建和编辑实体记录的表单页。它说明如何按 CRUD 能力选择模式、加载现有数据,并通过表单验证控制提交。

主要功能

  1. 01根据路由识别创建或编辑模式
  2. 02使用 Reactive Forms 构建表单
  3. 03按字段配置验证规则
  4. 04编辑时加载并填充记录
  5. 05校验通过后保存或更新

适用场景

  1. 01为实体字段创建带验证的表单
  2. 02复用页面处理新建与编辑
  3. 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