【ITニュース解説】Angular Signals Form: Validation and Logic
2025年09月16日に「Dev.to」が公開したITニュース「Angular Signals Form: Validation and Logic」について初心者にもわかりやすく解説しています。
ITニュース概要
Angular Signals Formは、スキーマを用いてフォームのバリデーションやビジネスロジックを宣言的に定義する。組み込みバリデータや独自のカスタムバリデーションに加え、スキーマの構成により複雑なフォームロジックもシンプルに記述できる。これにより、開発者はフォーム作成の複雑さから解放され、効率的な開発が可能になる。
ITニュース解説
AngularのSignalsを使ったフォームにおいて、単にデータを受け付けるだけでなく、そのデータが正しいかチェックしたり(バリデーション)、特定の条件でフォームの見た目や動作を変えたり(ビジネスロジック)する方法について解説する。
以前の記事では、Signalsを使って基本的なフォームを作る方法を紹介したが、そこにはバリデーションや複雑なロジックは含まれていなかった。本記事では、そうしたビジネスロジックをシンプルかつ拡張しやすい形で実装する仕組みに焦点を当てる。
フォームの基本的な構造は、まずsignalという機能を使って、フォーム全体で扱うデータのひな形を定義することから始まる。例えば、タスク管理アプリの「Todo」フォームであれば、タスクのタイトル、説明、ステータス、担当者などの情報を持つTodo型のデータを用意し、これをtodoModelというsignalとして初期化する。このtodoModelが、フォームの全ての情報源となる。次に、このtodoModelをform関数に渡すことで、実際にユーザーが操作するフォームのインスタンスであるtodoFormが作成される。
ここで重要なのは、フォーム内の個々の入力欄(フィールド)の状態が、直接定義されるのではなく、「派生した状態」(専門用語ではcomputedと呼ばれる)として自動的に算出される点だ。例えば、ある入力欄が「有効(valid)」であるか、「無効(disabled)」であるかといった状態は、そのフィールドのルールに基づいてリアルタイムで計算される。これは、signalという仕組みが、データの変化を自動的に検知し、関連する状態を更新する能力を持つためである。
ビジネスロジックやバリデーション(入力値の検証)は、フォームを定義するTypeScriptコードの中で「宣言的」に記述されるのが、Signalsベースのフォームの大きな特徴だ。これは、後から特定のフィールドの状態を「無効にする」といった命令的なコマンドを使わず、フィールドのルールそのものを記述することで、その状態が自動的に決まることを意味する。例えば、「このフィールドは必須である」とか、「このフィールドは、別のフィールドの値が特定の条件を満たす場合にのみ有効になる」といったルールを最初から定義する。
これらのビジネスルールやバリデーションロジックは、「Schema(スキーマ)」と呼ばれる設計図のようなものを使って表現される。Schemaは、フォーム全体、またはフォーム内の特定のフィールドに対する全てのビジネスルールをまとめたものだと考えればよい。
Schemaを定義するには、schema関数を利用する。この関数は、どの型のデータに対するルールを定義するかを示すFieldPathという型情報と、その型に対する具体的なビジネスロジックを記述する関数を引数に取る。FieldPathは、フォーム内のデータのどの部分(例えばtodo.titleやtodo.description)にルールを適用するかを指定するための仕組みだ。この型情報が非常に重要で、Todo型のSchemaであればTodo型フィールドのルールを定義し、Boolean型SchemaであればBoolean型フィールドのルールを定義するというように、型とルール定義が密接に連携している。これにより、フィールドのロジックがフォームの構造と完全に一致することが保証される。
定義されたSchemaは、form()関数に2番目の引数として渡すことで、フォームのフィールド構造と関連付けられる。
初めてSchemaを記述する際、form関数の中で直接関数としてSchemaを定義することもできる。
todoForm = form(this.todoModel, (path: FieldPath<Todo>) => {});
しかし、この方法では、定義したSchemaを他の場所で再利用したり、別のファイルに分けて管理したりすることができないため、大規模なアプリケーションには向かない。
より拡張性を持たせるためには、Schemaを独立した定数として定義し、それをエクスポート可能な形にするのが一般的だ。
export const TodoSchema = schema<Todo>(path => {});
このようにすることで、TodoSchemaを他のファイルや、別のアプリケーションで共有されるライブラリとして利用できるようになり、アプリケーション全体でのコードの再利用性や管理性が向上する。そして、todoForm = form(this.todoModel, TodoSchema); のように、定義したSchemaをform関数に渡すことで利用する。
この時点ではSchemaは何も動作しないが、ここに具体的なロジックを追加していく。既存のフォームライブラリと同様に、Signals Formにも便利な組み込みバリデーターが用意されている。例えば、「必須入力(required)」や「メールアドレス形式(email)」といったよく使うバリデーションルールがある。
これらをSchemaに追加するには、TodoSchemaの定義内で、pathを使って対象のフィールドを指定し、バリデーター関数を呼び出す。
例えば、required(path.title); と書けば、titleフィールドが必須入力になる。minLength(path.description, 10); と書けば、descriptionフィールドの入力が10文字以上である必要があるというルールを設定できる。
組み込みのバリデーターでは対応できないような、独自の複雑なバリデーションロジックが必要な場合は、validate関数やerror関数を使う。
validate関数は、フィールドに対してカスタムバリデーションルールを追加するために使用する。一つのフィールドに複数のvalidateルールを設定でき、発生した全てのエラー情報はFieldStateから取得できる。
validate関数は、対象のFieldPathと、実際のバリデーションロジックを記述する関数を引数に取る。このバリデーションロジック関数は、検証対象のフィールドの値を受け取り、ルールに違反がなければ空の配列を、違反があればエラー情報を格納した配列を返す。例えば、特定の形式のメールアドレスを検証するカスタムバリデーションを定義できる。
validate関数の強力な点は、単一のフィールドだけでなく、FieldPathを使ってフォーム全体を検証できることだ。これにより、例えばパスワードと確認用パスワードの一致チェックなど、複数のフィールドにまたがるバリデーションも容易に実装できる。また、Zodのような外部のバリデーションライブラリと連携することも可能で、より複雑なデータスキーマ検証を統合できる。
validate関数に渡すバリデーションロジック関数は、value(対象フィールドの値)、valueOf(他のフィールドの値を取得する)、stateOf(他のフィールドの状態を取得する)、fieldOf(他のフィールドインスタンスを取得する)といったヘルパー関数を含むオブジェクトを引数として受け取ることができる。これにより、バリデーションロジック内でフォーム内の他のフィールドの値や状態にアクセスし、より高度な条件分岐を実装できる。
error関数は、validate関数の簡略版と考えることができる。error関数は、バリデーションが成功した場合はtrueを、失敗した場合はfalseを返すブール値を返すロジックと、オプションでユーザーに表示するエラーメッセージを引数に取る。validate関数のようにエラーオブジェクトを返す手間がなく、シンプルな条件チェックとメッセージ表示に適している。
上記のように、Signals Formを使ったバリデーションは非常に強力だが、一つのSchemaの中に全てのロジックを記述していくと、コードが長くなり、複雑化しやすいという問題がある。ここで、「Schemaの構成(Composition)」という考え方が非常に役立つ。
Compositionパターンは、「複雑な問題を解決するために、より小さな要素を組み合わせていく」というシンプルな考え方だ。Signals Formでは、apply関数を使ってSchemaを構成できる。
apply関数は、指定したFieldPathに別のSchemaを適用することを可能にする。これにより、以下のようなメリットがある。
- バリデーションロジックの分解: 複雑なフォームのバリデーションを、小さな独立したSchemaに分割できる。例えば、
descriptionフィールドの必須、最小文字数、最大文字数といった複数のバリデーションを、descriptionSchemaという独立したSchemaにまとめ、それをTodoSchema内のpath.descriptionにapplyできる。 - Schemaの共有: アプリケーション全体で、または複数のアプリケーション間で再利用可能な汎用的なSchemaライブラリを作成し、共有できる。
apply関数には、いくつかの仲間もいる。
applyEachは、配列型のFieldPathに対して、その配列の各要素に同じSchemaを適用する機能を提供する。例えば、ユーザーのリストに対するフォームで、各ユーザーのバリデーションルールを一括で適用する場合に便利だ。
applyWhenは、特定の条件が満たされた場合にのみSchemaを適用したい場合に使う。例えば、「パスワード」フィールドに値が入力された場合にのみ「確認用パスワード」フィールドが必須になる、といった条件付きバリデーションを実現できる。applyWhenは、対象のFieldPath、バリデーションを適用する条件を判定する関数、そして適用するSchemaの3つの引数を取る。条件判定関数の中では、valueOfヘルパー関数を使って他のフィールドの値を取得できる。
結論として、Signals Formにおけるビジネスロジックとバリデーションは、「Schema」という設計図を用いて宣言的に定義される。このschemaパッケージは、組み込みバリデーターのほか、カスタムバリデーションを記述するためのvalidateやerror関数、そして複雑なフォームロジックを分解・再利用可能にするためのapply、applyEach、applyWhenといった強力なCompositionツールを提供する。
これにより、以前は多くのコードと複雑なフォーム設計の考察が必要だったタスクが、数行のシンプルなコードで実現できるようになった。これは、開発者がフォーム作成の複雑さから解放され、アプリケーションのビジネスロジックそのものに集中できるようにするという、開発者体験を大幅に向上させる強い意欲の表れと言える。