mcpField
D
d2-05-form-page

d2-05-form-page

github.com/softinter-chiangrai開発040 回インストール9 週間前に更新
リポジトリ

d2-05-form-page について

エンティティレコードの作成・編集フォームを実装するAngular開発スキルです。ルートに応じたモード切り替え、既存データの読み込み、Reactive Formsによる検証と送信を説明します。

主な機能

  1. 01ルートから作成・編集モードを判定
  2. 02Reactive 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 ルート

前提条件: app.config.ts に ReactiveFormsModule を追加します。

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') を使用します

コメント