アンケートなどの多くのフォームは、フォーマットと意図において非常に類似している場合があります。 このようなフォームの異なるバージョンをより速く簡単に生成できるように、ビジネスオブジェクトモデルを記述するメタデータに基づいて、_動的フォームテンプレート_を作成できます。 その後、テンプレートを使用して、データモデルの変更に応じて、新しいフォームを自動的に生成します。
このテクニックは、ビジネスや規制要件の急速な変化に対応するために、コンテンツを頻繁に変更する必要があるフォームの種類を持つ場合に特に役立ちます。 一般的なユースケースはアンケートです。 さまざまなコンテキストでユーザーからの入力を受け取る必要がある場合があります。 ユーザーが見ているフォームのフォーマットとスタイルは一定に保つ必要がありますが、実際に尋ねる必要がある質問はコンテキストによって異なります。
このチュートリアルでは、基本的なアンケートを表示する動的フォームを作成します。 雇用を求めるヒーローのためのオンラインアプリケーションを構築します。 エージェンシーは常にアプリケーションプロセスをいじっていますが、 動的フォームを使用することでアプリケーションコードを変更せずに新しいフォームをオンザフライで作成できます。
このチュートリアルでは、次の手順について説明します。
- プロジェクトでリアクティブフォームを有効にする。
- フォームコントロールを表すデータモデルを確立する。
- サンプルデータでモデルを埋める。
- フォームコントロールを動的に作成するコンポーネントを開発する。
作成するフォームでは、入力検証とスタイリングを使用してユーザー体験を向上させます。 すべてのユーザー入力が有効な場合にのみ有効になる送信ボタンがあり、無効な入力を色分けとエラーメッセージでフラグ付けします。
この基本バージョンは、より多くの種類の質問、より洗練されたレンダリング、優れたユーザー体験をサポートするように進化させることができます。
プロジェクトでリアクティブフォームを有効にする
動的フォームはリアクティブフォームに基づいています。
アプリケーションにリアクティブフォームディレクティブへのアクセス権を与えるには、必要なコンポーネントに @angular/forms
ライブラリから ReactiveFormsModule
をインポートします。
例からの次のコードは、ルートモジュールでのセットアップを示しています。
dynamic-form.component.ts
import {Component, Input, OnInit} from '@angular/core';import {CommonModule} from '@angular/common';import {FormGroup, ReactiveFormsModule} from '@angular/forms';import {DynamicFormQuestionComponent} from './dynamic-form-question.component';import {QuestionBase} from './question-base';import {QuestionControlService} from './question-control.service';@Component({ selector: 'app-dynamic-form', templateUrl: './dynamic-form.component.html', providers: [QuestionControlService], imports: [CommonModule, DynamicFormQuestionComponent, ReactiveFormsModule],})export class DynamicFormComponent implements OnInit { @Input() questions: QuestionBase<string>[] | null = []; form!: FormGroup; payLoad = ''; constructor(private qcs: QuestionControlService) {} ngOnInit() { this.form = this.qcs.toFormGroup(this.questions as QuestionBase<string>[]); } onSubmit() { this.payLoad = JSON.stringify(this.form.getRawValue()); }}
dynamic-form-question.component.ts
import {Component, Input} from '@angular/core';import {FormGroup, ReactiveFormsModule} from '@angular/forms';import {CommonModule} from '@angular/common';import {QuestionBase} from './question-base';@Component({ selector: 'app-question', templateUrl: './dynamic-form-question.component.html', imports: [CommonModule, ReactiveFormsModule],})export class DynamicFormQuestionComponent { @Input() question!: QuestionBase<string>; @Input() form!: FormGroup; get isValid() { return this.form.controls[this.question.key].valid; }}
フォームオブジェクトモデルを作成する
動的フォームには、フォーム機能に必要なすべてのシナリオを記述できるオブジェクトモデルが必要です。 例のヒーローアプリケーションフォームは、一連の質問です。つまり、フォーム内の各コントロールは質問をし、回答を受け入れる必要があります。
このタイプのフォームのデータモデルは、質問を表す必要があります。
例には、DynamicFormQuestionComponent
が含まれており、質問をモデルの基本オブジェクトとして定義しています。
次の QuestionBase
は、フォーム内の質問とその回答を表すことができる一連のコントロールのベースクラスです。
src/app/question-base.ts
export class QuestionBase<T> { value: T | undefined; key: string; label: string; required: boolean; order: number; controlType: string; type: string; options: {key: string; value: string}[]; constructor( options: { value?: T; key?: string; label?: string; required?: boolean; order?: number; controlType?: string; type?: string; options?: {key: string; value: string}[]; } = {}, ) { this.value = options.value; this.key = options.key || ''; this.label = options.label || ''; this.required = !!options.required; this.order = options.order === undefined ? 1 : options.order; this.controlType = options.controlType || ''; this.type = options.type || ''; this.options = options.options || []; }}
コントロールクラスを定義する
このベースから、例では、異なるコントロールタイプを表す TextboxQuestion
と DropdownQuestion
の2つの新しいクラスを派生させます。
次のステップでフォームテンプレートを作成するときは、適切なコントロールを動的にレンダリングするために、これらの特定の質問タイプをインスタンス化します。
TextboxQuestion
コントロールタイプは、フォームテンプレートでは <input>
要素を使用して表されます。質問を表示し、ユーザーが入力をできるようにします。要素の type
属性は、options
引数で指定された type
フィールドに基づいて定義されます(例:text
、email
、url
)。
question-textbox.ts
import {QuestionBase} from './question-base';export class TextboxQuestion extends QuestionBase<string> { override controlType = 'textbox';}
DropdownQuestion
コントロールタイプは、選択ボックスに選択肢のリストを表示します。
question-dropdown.ts
import {QuestionBase} from './question-base';export class DropdownQuestion extends QuestionBase<string> { override controlType = 'dropdown';}
フォームグループを構成する
動的フォームは、サービスを使用して、質問モデルのメタデータに基づいて、入力コントロールのグループ化されたセットを作成します。
次の QuestionControlService
は、質問モデルからメタデータを使用する FormGroup
インスタンスのセットを収集します。
デフォルト値と検証ルールを指定できます。
src/app/question-control.service.ts
import {Injectable} from '@angular/core';import {FormControl, FormGroup, Validators} from '@angular/forms';import {QuestionBase} from './question-base';@Injectable()export class QuestionControlService { toFormGroup(questions: QuestionBase<string>[]) { const group: any = {}; questions.forEach((question) => { group[question.key] = question.required ? new FormControl(question.value || '', Validators.required) : new FormControl(question.value || ''); }); return new FormGroup(group); }}
動的フォームコンテンツを構成する
動的フォーム自体は、後で追加するコンテナーコンポーネントで表されます。
各質問は、フォームコンポーネントのテンプレートで、DynamicFormQuestionComponent
のインスタンスと一致する <app-question>
タグで表されます。
DynamicFormQuestionComponent
は、データバインドされた質問オブジェクトの値に基づいて、個々の質問の詳細をレンダリングする責任があります。
フォームは、[formGroup]
ディレクティブ に依存して、テンプレートHTMLを基礎となるコントロールオブジェクトに接続します。
DynamicFormQuestionComponent
は、フォームグループを作成し、質問モデルにより定義されたコントロールでそれらを埋め、表示と検証ルールを指定します。
dynamic-form-question.component.html
<div [formGroup]="form"> <label [attr.for]="question.key">{{ question.label }}</label> <div> @switch (question.controlType) { @case ('textbox') { <input [formControlName]="question.key" [id]="question.key" [type]="question.type"> } @case ('dropdown') { <select [id]="question.key" [formControlName]="question.key"> @for (opt of question.options; track opt) { <option [value]="opt.key">{{ opt.value }}</option> } </select> } } </div> @if (!isValid) {<div class="errorMessage">{{ question.label }} is required</div>}</div>
dynamic-form-question.component.ts
import {Component, Input} from '@angular/core';import {FormGroup, ReactiveFormsModule} from '@angular/forms';import {CommonModule} from '@angular/common';import {QuestionBase} from './question-base';@Component({ selector: 'app-question', templateUrl: './dynamic-form-question.component.html', imports: [CommonModule, ReactiveFormsModule],})export class DynamicFormQuestionComponent { @Input() question!: QuestionBase<string>; @Input() form!: FormGroup; get isValid() { return this.form.controls[this.question.key].valid; }}
DynamicFormQuestionComponent
の目標は、モデルで定義された質問タイプを提示することです。
現時点では質問タイプが2つしかありませんが、さらに多くのタイプが考えられます。
テンプレートの ngSwitch
ステートメントは、表示する質問タイプを決定します。
スイッチは、formControlName
と formGroup
セレクターを持つディレクティブを使用します。
両方のディレクティブは ReactiveFormsModule
で定義されています。
データを供給する
個々のフォームを構築するために、特定の質問セットを供給するサービスも必要です。
この演習では、ハードコードされたサンプルデータからこの質問の配列を供給する QuestionService
を作成します。
実際のアプリケーションでは、サービスはバックエンドシステムからデータを取得する可能性があります。
ただし、重要な点は、ヒーローの求人質問を QuestionService
から返されるオブジェクトを通じて完全に制御できることです。
要件が変更された場合にアンケートを維持するには、questions
配列からオブジェクトを追加、更新、削除するだけです。
QuestionService
は、@Input()
questionsにバインドされた配列の形式で、質問セットを供給します。
src/app/question.service.ts
import {Injectable} from '@angular/core';import {DropdownQuestion} from './question-dropdown';import {QuestionBase} from './question-base';import {TextboxQuestion} from './question-textbox';import {of} from 'rxjs';@Injectable()export class QuestionService { // TODO: get from a remote source of question metadata getQuestions() { const questions: QuestionBase<string>[] = [ new DropdownQuestion({ key: 'favoriteAnimal', label: 'Favorite Animal', options: [ {key: 'cat', value: 'Cat'}, {key: 'dog', value: 'Dog'}, {key: 'horse', value: 'Horse'}, {key: 'capybara', value: 'Capybara'}, ], order: 3, }), new TextboxQuestion({ key: 'firstName', label: 'First name', value: 'Alex', required: true, order: 1, }), new TextboxQuestion({ key: 'emailAddress', label: 'Email', type: 'email', order: 2, }), ]; return of(questions.sort((a, b) => a.order - b.order)); }}
動的フォームテンプレートを作成する
DynamicFormComponent
コンポーネントは、テンプレートで <app-dynamic-form>
を使用して表されるフォームのエントリポイントであり、メインコンテナーです。
DynamicFormComponent
コンポーネントは、各質問を、DynamicFormQuestionComponent
と一致する <app-question>
要素にバインドすることによって、質問のリストを表示します。
dynamic-form.component.html
<div> <form (ngSubmit)="onSubmit()" [formGroup]="form"> @for (question of questions; track question) { <div class="form-row"> <app-question [question]="question" [form]="form"></app-question> </div> } <div class="form-row"> <button type="submit" [disabled]="!form.valid">Save</button> </div> </form> @if (payLoad) { <div class="form-row"> <strong>Saved the following values</strong><br>{{ payLoad }} </div> }</div>
dynamic-form.component.ts
import {Component, Input, OnInit} from '@angular/core';import {CommonModule} from '@angular/common';import {FormGroup, ReactiveFormsModule} from '@angular/forms';import {DynamicFormQuestionComponent} from './dynamic-form-question.component';import {QuestionBase} from './question-base';import {QuestionControlService} from './question-control.service';@Component({ selector: 'app-dynamic-form', templateUrl: './dynamic-form.component.html', providers: [QuestionControlService], imports: [CommonModule, DynamicFormQuestionComponent, ReactiveFormsModule],})export class DynamicFormComponent implements OnInit { @Input() questions: QuestionBase<string>[] | null = []; form!: FormGroup; payLoad = ''; constructor(private qcs: QuestionControlService) {} ngOnInit() { this.form = this.qcs.toFormGroup(this.questions as QuestionBase<string>[]); } onSubmit() { this.payLoad = JSON.stringify(this.form.getRawValue()); }}
フォームを表示する
動的フォームのインスタンスを表示するには、AppComponent
シェルテンプレートは、QuestionService
から返された questions
配列を、フォームコンテナーコンポーネント <app-dynamic-form>
に渡します。
app.component.ts
import {Component} from '@angular/core';import {AsyncPipe} from '@angular/common';import {DynamicFormComponent} from './dynamic-form.component';import {QuestionService} from './question.service';import {QuestionBase} from './question-base';import {Observable} from 'rxjs';@Component({ selector: 'app-root', template: ` <div> <h2>Job Application for Heroes</h2> <app-dynamic-form [questions]="questions$ | async"></app-dynamic-form> </div> `, providers: [QuestionService], imports: [AsyncPipe, DynamicFormComponent],})export class AppComponent { questions$: Observable<QuestionBase<any>[]>; constructor(service: QuestionService) { this.questions$ = service.getQuestions(); }}
このモデルとデータの分離により、質問 オブジェクトモデルと互換性がある限り、あらゆるタイプのアンケートにコンポーネントを再利用できます。
有効なデータを確保する
フォームテンプレートは、特定の質問についてハードコードされた仮定を一切行わずに、メタデータの動的データバインドを使用してフォームをレンダリングします。 コントロールメタデータと検証基準の両方を動的に追加します。
有効な入力を確保するために、フォームが有効な状態になるまで、保存 ボタンは無効になっています。 フォームが有効になったら、保存 をクリックすると、アプリケーションは現在のフォーム値をJSONとしてレンダリングします。
次の図は、最終的なフォームを示しています。