1. 結論(この記事で得られること)
この記事を読むと、以下が手に入ります。
- Angular Material + ReactiveForms の正しい組み合わせ方がコードレベルで分かる
- バリデーションエラーの表示タイミング、状態管理のベストプラクティスを理解できる
- 実務で必須の動的フォーム生成・配列操作(FormArray)・カスタムバリデーターまで網羅
- AI(Claude / GPT)を使った爆速デバッグ・設計レビュー方法が身につく
- テスト観点・失敗パターン集で、本番リリース前に地雷を踏まずに済む
昔の自分に教えたかった内容を、現場目線で全部詰め込みました。
2. 前提(環境・読者層)
想定読者
- Angular の基礎(Component, Service, DI)は理解している
- ReactiveForms を触ったことはあるが、実務レベルの設計に自信がない
- Angular Material を導入したけど、エラーハンドリングがうまくいかない
検証環境
Angular: 17.x
Angular Material: 17.x
TypeScript: 5.x
※ Angular 14 以降なら大きな差異はありません。Standalone Component でも動作します。
3. Before:よくあるつまずきポイント
実務で何度も見てきた"あるある"を3つ挙げます。
3-1. エラーメッセージが出ない / 出続ける
// ❌ 危険な実装例
<mat-error *ngIf="form.get('email').invalid">
メールアドレスが不正です
</mat-error>
問題点:
- 初期表示時から赤字エラーが出る(UX最悪)
- 「form.get('email')」 が null の場合に実行時エラーになる
- どのバリデーションに引っかかったか分からない
私も新人時代、これでレビューで差し戻されました。
3-2. FormControl と mat-form-field の接続ミス
// ❌ formControlName を書き忘れ
<mat-form-field>
<input matInput placeholder="メールアドレス">
</mat-form-field>
Angular Material は formControlName がないとバリデーション状態を認識しません。
エラーが出ないのに、裏では required が効いてないという事故が起きます。
3-3. disabled 属性の罠
// ❌ これは動かない
<input matInput formControlName="name" [disabled]="true">
ReactiveForms では HTML の 「disabled」 属性は無視されます。
正しくは 「form.get('name').disable()」 を使う必要があります。
4. After:基本的な解決パターン
4-1. 安全なエラー表示(touched 制御)
// user-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html',
})
export class UserFormComponent implements OnInit {
form!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
}
// エラー取得ヘルパー(null 安全)
getErrorMessage(controlName: string): string {
const control = this.form.get(controlName);
if (!control || !control.errors || !control.touched) {
return '';
}
if (control.errors['required']) return '必須項目です';
if (control.errors['email']) return 'メール形式が不正です';
if (control.errors['minlength']) {
const required = control.errors['minlength'].requiredLength;
return `${required}文字以上で入力してください`;
}
return '';
}
onSubmit(): void {
if (this.form.invalid) {
// 全フィールドを touched にしてエラーを表示
this.form.markAllAsTouched();
return;
}
console.log(this.form.value);
}
}
<!-- user-form.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<mat-form-field appearance="outline">
<mat-label>メールアドレス</mat-label>
<input matInput formControlName="email" type="email">
<mat-error>{{ getErrorMessage('email') }}</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>パスワード</mat-label>
<input matInput formControlName="password" type="password">
<mat-error>{{ getErrorMessage('password') }}</mat-error>
</mat-form-field>
<button mat-raised-button color="primary" type="submit">
送信
</button>
</form>
ポイント:
- 「touched」 で「ユーザーが触った後だけ」エラーを出す
- 「markAllAsTouched()」 で送信時に全エラーを表示
- 「getErrorMessage()」 で null 安全とエラー種別を一元管理
4-2. disabled の正しい制御
ngOnInit(): void {
this.form = this.fb.group({
name: [{ value: '', disabled: false }, Validators.required],
age: [{ value: null, disabled: true }], // 初期状態で無効化
});
// 動的に制御
this.form.get('name')?.valueChanges.subscribe(value => {
if (value === 'admin') {
this.form.get('age')?.disable();
} else {
this.form.get('age')?.enable();
}
});
}
注意:
- disabled なコントロールは 「form.value」 に含まれません
- 含めたい場合は 「form.getRawValue()」 を使う