Signal Forms で動的な配列フォーム
これは Angular Advent Calendar 2025 5日目の記事です。
はじめに
Angular v21 では、待望の Signal Forms が使えるようになりました。
この記事では、Signal Forms を使って、動的な配列フォームを実装する方法を紹介します。
Signal Forms の基本的な仕組みは説明しません。公式ドキュメントに詳しく書いてあるので、ぜひご一読ください。
動作バージョン
バージョン
% ng version
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI : 21.0.0
Angular : 21.0.0
Node.js : 22.14.0
Package Manager : npm 10.5.0
Operating System : darwin arm64
┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 21.0.0 │ ^21.0.0 │
│ @angular/cli │ 21.0.0 │ ^21.0.0 │
│ @angular/common │ 21.0.0 │ ^21.0.0 │
│ @angular/compiler │ 21.0.0 │ ^21.0.0 │
│ @angular/compiler-cli │ 21.0.0 │ ^21.0.0 │
│ @angular/core │ 21.0.0 │ ^21.0.0 │
│ @angular/forms │ 21.0.0 │ ^21.0.0 │
│ @angular/platform-browser │ 21.0.0 │ ^21.0.0 │
│ @angular/router │ 21.0.0 │ ^21.0.0 │
│ rxjs │ 7.8.2 │ ~7.8.0 │
│ typescript │ 5.9.3 │ ~5.9.2 │
│ vitest │ 4.0.13 │ ^4.0.8 │
└───────────────────────────┴───────────────────┴───────────────────┘
完成したもの
コード解説
配列要素のモデル
まず、名前と年齢を持つ、シンプルなUserModelを定義します。このモデルを配列として追加・削除できるようにします。
interface UserModel {
name: string;
age: number;
}
Userコンポーネントの実装
配列の各要素は再利用できるよう、User コンポーネントとして切り出します。
ここでポイントとなるのがFormValueControlです。
これは、コンポーネントが Signal Forms のフォームシステムとやり取りするためのインターフェースで、従来のテンプレート駆動フォーム/リアクティブフォームにおけるControl Value Accessor (CVA)に相当します。
ただし、CVA と比べると非常にシンプルで、valueという Model Input を実装するだけで、親コンポーネントのフォームと連携できます。
詳しくはカスタムコントロールをご参照ください。
Userコンポーネントでは、テンプレート駆動フォームを使って、valueを更新しています。
@Component({selector: 'app-user'})
export class User implements FormValueControl<UserModel> {
value = model({ name: '', age: 20 });
updateName(name: string) {
this.value.update((curr) => ({ ...curr, name }));
}
updateAge(age: number) {
this.value.update((curr) => ({ ...curr, age }));
}
}
<form>
<div>
<label>名前:
<input
name="name"
[ngModel]="value().name"
(ngModelChange)="updateName($event)"
/>
</label>
</div>
<div>
<label>年齢:
<input
type="number"
name="age"
[ngModel]="value().age"
(ngModelChange)="updateAge($event)"
/>
</label>
</div>
</form>
親コンポーネントの実装
まず、ユーザー一覧をsignal<UserModel[]>として保持します。これは Signal Forms における “Source of Truth” となり、form()に渡すことでフォームインスタンスがリアクティブに生成されます。
次に、[field]ディレクティブを通して、User コンポーネントと Signal Forms を紐付けます。
@Component()
export class Parent {
readonly usersModel = signal<UserModel[]>([]);
readonly usersForm = form(this.usersModel);
}
@for(user of usersForm; track user) {
<app-user [field]="user" />
}
配列の追加と削除
usersModelを更新すると、フォームの状態も自動的に同期されます。
@Component()
export class Parent {
addUser() {
const userModel: UserModel = {
name: '',
age: 20,
};
this.usersModel.update((curr) => [...curr, userModel]);
}
deleteUser(index: number) {
this.usersModel.update((curr) => curr.filter((_, i) => i !== index));
}
}
バリデーションの実装
バリデーションはスキーマ関数として定義します。
例えばUserModelの場合は次のように書けます。
readonly userSchema = schema<UserModel>((schemaPath) => {
required(schemaPath.name, { message: '名前は必須項目です' });
required(schemaPath.age, { message: '年齢は必須項目です' });
min(schemaPath.age, 1, {message: '年齢は0より大きい数値を入力してください'});
});
これをform()の第二引数で適用します。配列の各要素にスキーマを適用するため、applyEachを利用します。
@Component()
export class Parent {
readonly usersForm = form(this.usersModel, (schemaPath) => {
applyEach(schemaPath, userSchema);
});
}
エラーメッセージの表示
フォームの状態を参照して、各フィールドのエラーメッセージを表示します。
<ul>
@if(user.name().invalid()) {
@for(error of user.name().errors(); track error) {
<li>{{ error.message }}</li>
}
}
@if(user.age().invalid()) {
@for(error of user.age().errors(); track error) {
<li>{{ error.message }}</li>
}
}
</ul>
これで完成です!
まとめ
Signal Forms を使って、動的な配列フォームの実装方法を紹介しました。
従来のフォームシステムではどうしても複雑になりがちだった部分も、Signal ベースだとかなりスッキリまとまります。
FormValueControlと組み合わせれば、ありがちだった巨大なフォームコンポーネントともお別れできそうです。
この記事では取り上げていない「痒いところに手が届く」機能もまだ多く、従来のフォームシステムとの比較も公開されています。どのフォームシステムを採用すべきか検討する際に、参考になるはずです。
まだ実験的な位置づけではありますが、早くもプロダクトに取り入れたくなる出来です。
Signal Forms、最高。
明日は、@mashirojsさんです!
Discussion