Agent Skills by ALSEL
OpenAIソフトウェア開発⭐ リポ 0品質スコア 65/100

angular-forms

Angular v21以上で新しいSignal Forms APIを使用して、シグナルベースのフォームを構築できます。自動的な双方向バインディング、スキーマベースの検証、フィールド状態管理、動的フォーム機能を備えたフォーム作成に使用します。フォーム実装の追加、検証の実装、マルチステップフォームの作成、条件付きフィールドを含むフォーム構築の際に活用できます。Signal Formsは実験的機能ですが、新規のAngularプロジェクトに推奨されています。

description の原文を見る

Build signal-based forms in Angular v21+ using the new Signal Forms API. Use for form creation with automatic two-way binding, schema-based validation, field state management, and dynamic forms. Triggers on form implementation, adding validation, creating multi-step forms, or building forms with conditional fields. Signal Forms are experimental but recommended for new Angular projects.

SKILL.md 本文

Angular Signal Forms

Angular の Signal Forms API を使用して、タイプセーフでリアクティブなフォームを構築します。Signal Forms は自動的な双方向バインディング、スキーマベースの検証、リアクティブなフィールド状態を提供します。

注釈: Signal Forms は Angular v21 では実験的機能です。本番環境で安定性が必要なアプリケーションについては、references/form-patterns.md の Reactive Forms パターンを参照してください。

基本的なセットアップ

import { Component, signal } from '@angular/core';
import { form, FormField, required, email } from '@angular/forms/signals';

interface LoginData {
  email: string;
  password: string;
}

@Component({
  selector: 'app-login',
  imports: [FormField],
  template: `
    <form (submit)="onSubmit($event)">
      <label>
        Email
        <input type="email" [formField]="loginForm.email" />
      </label>
      @if (loginForm.email().touched() && loginForm.email().invalid()) {
        <p class="error">{{ loginForm.email().errors()[0].message }}</p>
      }
      
      <label>
        Password
        <input type="password" [formField]="loginForm.password" />
      </label>
      @if (loginForm.password().touched() && loginForm.password().invalid()) {
        <p class="error">{{ loginForm.password().errors()[0].message }}</p>
      }
      
      <button type="submit" [disabled]="loginForm().invalid()">Login</button>
    </form>
  `,
})
export class LoginComponent {
  // フォームモデル - 書き込み可能なシグナル
  loginModel = signal<LoginData>({
    email: '',
    password: '',
  });
  
  // 検証スキーマを使用してフォームを作成
  loginForm = form(this.loginModel, (schemaPath) => {
    required(schemaPath.email, { message: 'Email is required' });
    email(schemaPath.email, { message: 'Enter a valid email address' });
    required(schemaPath.password, { message: 'Password is required' });
  });
  
  onSubmit(event: Event) {
    event.preventDefault();
    if (this.loginForm().valid()) {
      const credentials = this.loginModel();
      console.log('Submitting:', credentials);
    }
  }
}

フォームモデル

フォームモデルは書き込み可能なシグナルであり、単一の情報源として機能します。

// 型安全性のためのインターフェースを定義
interface UserProfile {
  name: string;
  email: string;
  age: number | null;
  preferences: {
    newsletter: boolean;
    theme: 'light' | 'dark';
  };
}

// 初期値を使用してモデルシグナルを作成
const userModel = signal<UserProfile>({
  name: '',
  email: '',
  age: null,
  preferences: {
    newsletter: false,
    theme: 'light',
  },
});

// モデルからフォームを作成
const userForm = form(userModel);

// ドット記法でネストされたフィールドにアクセス
userForm.name                    // FieldTree<string>
userForm.preferences.theme       // FieldTree<'light' | 'dark'>

値の読み込み

// モデル全体を読み込み
const data = this.userModel();

// フィールド状態を使用してフィールド値を読み込み
const name = this.userForm.name().value();
const theme = this.userForm.preferences.theme().value();

値の更新

// モデル全体を置き換え
this.userModel.set({
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
  preferences: { newsletter: true, theme: 'dark' },
});

// 単一フィールドを更新
this.userForm.name().value.set('Bob');
this.userForm.age().value.update(age => (age ?? 0) + 1);

フィールド状態

各フィールドは検証、インタラクション、および利用可能性のためのリアクティブシグナルを提供します。

const emailField = this.form.email();

// 検証状態
emailField.valid()      // すべての検証に合格した場合は true
emailField.invalid()    // 検証エラーがある場合は true
emailField.errors()     // エラーオブジェクトの配列
emailField.pending()    // 非同期検証が進行中の場合は true

// インタラクション状態
emailField.touched()    // フォーカスとブラーの後は true
emailField.dirty()      // ユーザー変更後は true

// 利用可能性状態
emailField.disabled()   // フィールドが無効な場合は true
emailField.hidden()     // フィールドを非表示にする場合は true
emailField.readonly()   // フィールドが読み込み専用の場合は true

// 値
emailField.value()      // 現在のフィールド値 (シグナル)

フォームレベルの状態

フォーム自体も集計された状態を持つフィールドです。

// すべてのインタラクティブフィールドが有効な場合、フォームは有効です
this.form().valid()

// いずれかのフィールドがタッチされた場合、フォームはタッチされます
this.form().touched()

// いずれかのフィールドが変更された場合、フォームはダーティーになります
this.form().dirty()

検証

組み込みバリデーター

import { 
  form, required, email, min, max, 
  minLength, maxLength, pattern 
} from '@angular/forms/signals';

const userForm = form(this.userModel, (schemaPath) => {
  // 必須フィールド
  required(schemaPath.name, { message: 'Name is required' });
  
  // メールアドレス形式
  email(schemaPath.email, { message: 'Invalid email' });
  
  // 数値の範囲
  min(schemaPath.age, 18, { message: 'Must be 18+' });
  max(schemaPath.age, 120, { message: 'Invalid age' });
  
  // 文字列/配列の長さ
  minLength(schemaPath.password, 8, { message: 'Min 8 characters' });
  maxLength(schemaPath.bio, 500, { message: 'Max 500 characters' });
  
  // 正規表現パターン
  pattern(schemaPath.phone, /^\d{3}-\d{3}-\d{4}$/, {
    message: 'Format: 555-123-4567',
  });
});

条件付き検証

const orderForm = form(this.orderModel, (schemaPath) => {
  required(schemaPath.promoCode, {
    message: 'Promo code required for discounts',
    when: ({ valueOf }) => valueOf(schemaPath.applyDiscount),
  });
});

カスタムバリデーター

import { validate } from '@angular/forms/signals';

const signupForm = form(this.signupModel, (schemaPath) => {
  // カスタム検証ロジック
  validate(schemaPath.username, ({ value }) => {
    if (value().includes(' ')) {
      return { kind: 'noSpaces', message: 'Username cannot contain spaces' };
    }
    return null;
  });
});

フィールド間の検証

const passwordForm = form(this.passwordModel, (schemaPath) => {
  required(schemaPath.password);
  required(schemaPath.confirmPassword);
  
  // フィールドを比較
  validate(schemaPath.confirmPassword, ({ value, valueOf }) => {
    if (value() !== valueOf(schemaPath.password)) {
      return { kind: 'mismatch', message: 'Passwords do not match' };
    }
    return null;
  });
});

非同期検証

import { validateHttp } from '@angular/forms/signals';

const signupForm = form(this.signupModel, (schemaPath) => {
  validateHttp(schemaPath.username, {
    request: ({ value }) => `/api/check-username?u=${value()}`,
    onSuccess: (response: { taken: boolean }) => {
      if (response.taken) {
        return { kind: 'taken', message: 'Username already taken' };
      }
      return null;
    },
    onError: () => ({
      kind: 'networkError',
      message: 'Could not verify username',
    }),
  });
});

条件付きフィールド

非表示フィールド

import { hidden } from '@angular/forms/signals';

const profileForm = form(this.profileModel, (schemaPath) => {
  hidden(schemaPath.publicUrl, ({ valueOf }) => !valueOf(schemaPath.isPublic));
});
@if (!profileForm.publicUrl().hidden()) {
  <input [formField]="profileForm.publicUrl" />
}

無効フィールド

import { disabled } from '@angular/forms/signals';

const orderForm = form(this.orderModel, (schemaPath) => {
  disabled(schemaPath.couponCode, ({ valueOf }) => valueOf(schemaPath.total) < 50);
});

読み込み専用フィールド

import { readonly } from '@angular/forms/signals';

const accountForm = form(this.accountModel, (schemaPath) => {
  readonly(schemaPath.username); // 常に読み込み専用
});

フォーム送信

import { submit } from '@angular/forms/signals';

@Component({
  template: `
    <form (submit)="onSubmit($event)">
      <input [formField]="form.email" />
      <input [formField]="form.password" />
      <button type="submit" [disabled]="form().invalid()">Submit</button>
    </form>
  `,
})
export class LoginComponent {
  model = signal({ email: '', password: '' });
  form = form(this.model, (schemaPath) => {
    required(schemaPath.email);
    required(schemaPath.password);
  });
  
  onSubmit(event: Event) {
    event.preventDefault();
    
    // submit() はすべてのフィールドをタッチ済みにマークし、有効な場合はコールバックを実行します
    submit(this.form, async () => {
      await this.authService.login(this.model());
    });
  }
}

配列と動的フィールド

interface Order {
  items: Array<{ product: string; quantity: number }>;
}

@Component({
  template: `
    @for (item of orderForm.items; track $index; let i = $index) {
      <div>
        <input [formField]="item.product" placeholder="Product" />
        <input [formField]="item.quantity" type="number" />
        <button type="button" (click)="removeItem(i)">Remove</button>
      </div>
    }
    <button type="button" (click)="addItem()">Add Item</button>
  `,
})
export class OrderComponent {
  orderModel = signal<Order>({
    items: [{ product: '', quantity: 1 }],
  });
  
  orderForm = form(this.orderModel, (schemaPath) => {
    applyEach(schemaPath.items, (item) => {
      required(item.product, { message: 'Product required' });
      min(item.quantity, 1, { message: 'Min quantity is 1' });
    });
  });
  
  addItem() {
    this.orderModel.update(m => ({
      ...m,
      items: [...m.items, { product: '', quantity: 1 }],
    }));
  }
  
  removeItem(index: number) {
    this.orderModel.update(m => ({
      ...m,
      items: m.items.filter((_, i) => i !== index),
    }));
  }
}

エラーを表示

<input [formField]="form.email" />

@if (form.email().touched() && form.email().invalid()) {
  <ul class="errors">
    @for (error of form.email().errors(); track error) {
      <li>{{ error.message }}</li>
    }
  </ul>
}

@if (form.email().pending()) {
  <span>Validating...</span>
}

状態に基づくスタイル設定

<input
  [formField]="form.email"
  [class.is-invalid]="form.email().touched() && form.email().invalid()"
  [class.is-valid]="form.email().touched() && form.email().valid()"
/>

フォームをリセット

async onSubmit() {
  if (!this.form().valid()) return;
  
  await this.api.submit(this.model());
  
  // インタラクション状態をクリア
  this.form().reset();
  
  // 値をクリア
  this.model.set({ email: '', password: '' });
}

本番環境で安定した Reactive Forms パターンについては、references/form-patterns.md を参照してください。

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
ROU-Technology
リポジトリ
ROU-Technology/ng-utils
ライセンス
MIT
最終更新
2026/1/26

Source: https://github.com/ROU-Technology/ng-utils / ライセンス: MIT

本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: ROU-Technology · ROU-Technology/ng-utils · ライセンス: MIT