November 2017

Volume 33 Number 11

働くプログラマ - MEAN あれこれ: Angular フォーム

Ted Neward | November 2017

Ted Neward

「MEANers」の皆さん、お帰りなさい。
この連載で毎回注目しているのは Angular です。特に Angular を使用してデータを表示し、複数のページをルーティングすることに重点を置いてきました。Web ブラウザーはデータを表示するために作られています。ただし、これまで扱ってきませんでしたが、データを収集してサーバーに返すことも意図されています。

Angular では確かにフォームを使ってデータを取得できます。ですが、その方法は少し厄介です。ブラウザー フォームを作成して定義する構文はそれほど難しくはありませんが、基盤となる動作の特定の側面に難しい点があります。これについて後ほど取り上げます。順序が逆になってはいけません。まずは、簡単なフォームの定義と取得について見ていきましょう。

フォームのコンポーネント

前回は、「テンプレート ステートメント」(Angular 用語) を Angular コンポーネントが提供するイベントにバインドする方法を取り上げました。その最も一般的な例が、以下に示すように、<button> 要素の click イベントを取り込んで、Angular コンポーネントのメソッドを呼び出す方法です。

<button (click)="console.log('Clicked')">Push me!</button>

これ自体は優れた方法です。でも、入力を取り込む機能は何も提供されません。このしくみをデータ入力に応用する方法は 2 つあります。1 つは、コンポーネントのイベント コードがコンポーネントの内部からページのコントロールを参照できるようにする方法です (数あるフレームワークの中で、ASP.NET Web フォームがこれを行います)。もう 1 つは、呼び出されるイベント コードに入力データをパラメーターとして渡す方法です。ただし、これにはいくつかの異なるフォームが必要になる可能性があります。

まず、最も一般的なのが、$event オブジェクトを参照できる Angular テンプレート ステートメントです。$event オブジェクトは、本質的にはユーザーのセッション中に生成される DOM のイベント オブジェクトです。この場合は、以下のようにステートメントの一部としてパラメーターを参照する必要があるだけです。

<button (click)="capture($event)" >Save</button>

ただし、問題があります。渡されるオブジェクトは、ユーザーの操作を表す DOM イベントです。この場合は、マウスがクリックされた画面上の位置を追跡する MouseEvent です。このイベントは実際にはページ上の他の要素の状態を取り込みません。DOM 階層をナビゲートしてコントロール要素を見つけ、その要素から値を取り出すことも考えられますが、Angular 的やり方ではありません。コンポーネントは DOM から切り離す必要があります。そのため、テンプレート ステートメントでは、必要なデータを取得して、このデータを使用するコンポーネントのメソッドに渡す必要があります。

このアプローチでは、テンプレート ステートメントが値を取り出して渡せるように、Angular によってページ上の入力フィールドを特定する方法が必要になります。フォーム要素を特定する必要があるということは、<input> 要素自体の「識別子」の形を取ることになります。Angular ではこの識別子を「テンプレート参照変数」と呼びます。 他の Angular 構文と同様に、意図的に HTML にはみえない構文を使用します。

<input #firstName>

これにより、同じ名前の通常の HTML タグと同様に HTML に Input フィールドが作成されます。ただし、その結果としてテンプレート ステートメントのスコープに firstName という名前の新しい変数が導入されます。この変数はフィールドのイベントにバインドするテンプレート ステートメントから参照できます。以下に例を示します。

<button (click)="addSpeaker(firstName, lastName)" >Save</button>

これは説明するまでもないでしょう。ボタンをクリックすると、コンポーネントの addSpeaker メソッドが呼び出され、firstName と lastName 変数がこの順に渡されます (図 1 参照)。

図 1 addSpeaker メソッドの呼び出し

import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-speaker-edit',
templateUrl: './speaker-edit.component.html',
styleUrls: ['./speaker-edit.component.css']
})
export class SpeakerEditComponent implements OnInit {
constructor() { }
ngOnInit() { }
addSpeaker(fname: string, lname: string) {
console.log("addSpeaker(", fname, ",", lname, ")")
}
}

ですが、このように作成すると、ブラウザーのコンソールに表示されるのは入力から期待する文字列ではなく、<input _ngcontent-crf-2> などの値です。その理由は単純です。ブラウザー コンソールが返すのは、入力データではなく、DOM 要素の実際の Angular 表現です。この解決策も同様に単純です。テンプレート ステートメントの両辺で "value" プロパティを使用し、ユーザーが入力したデータを取得します。

そこで、新しい speaker を作成するためにコンポーネントを 1 つ構築する必要がある場合、2 つの <input> フィールドと (click) イベントを備えた <button> を表示するコンポーネントを作成します。(click) イベントでは firstName.value と lastName.value に渡して addSpeaker を呼び出します。addSpeaker メソッドでは (前回扱った) SpeakerService を呼び出し、受け取った値をデータベースに保存します。ですが、「新しい Speaker を構築する」 という考え方は、「既存の Speaker を編集する」こととほとんど同じ意味を持ちます。そのため、最新データベースの中には、Insert (挿入) 操作と Update (更新) 操作が本質的には 1 つの操作と考えられ「Upsert」と表現されるようになっています。こうした考え方は非常にすばらしく、コンポーネント指向と言えます。SpeakerEdit コンポーネントが同じコンポーネント内で作成または編集として機能することになります。

前回扱った @Input ディレクティブと @Output ディレクティブの能力のおかげで、実際これを行うのはとても簡単です。渡される側の speaker に @Input フィールドを、ユーザーが [Save] をクリックしたことを知らせるために @Output フィールドを追加します (SpeakerEdit コンポーネントでは常にデータベースに保存することに決め、そのコンポーネントの他にクライアントが行いたい操作がない場合、@Output は必須ではありません。これは、チーム ミーティングでは非常に状況に依存した話し合いになるでしょう)。

SpeakerEdit コンポーネントのコードを 図 2 に示します。

図 2 SpeakerEdit コンポーネント

export class SpeakerEditComponent implements OnInit {
@Input() speaker: Speaker;
@Output() onSave = new EventEmitter<Speaker>();
constructor(private speakerService: SpeakerService) { }
ngOnInit() {
if (this.speaker === undefined) {
this.speaker = new Speaker();
}
}
save(fn: string, ln: string) {
this.speaker.firstName = fn;
this.speaker.lastName = ln;
this.onSave.emit(this.speaker);
}
}

コラムの設計スキルから読者が想像されているとおり、このテンプレートは非常に基本的なものです。ですが、機能します。

<div>
Speaker Details: <br>
FirstName: <input #firstName><br>
LastName:  <input #lastName><br>
<button (click)="save(firstName.value, lastName.value)">Save</button>
</div>

繰り返しになりますが、firstName 入力フィールドと lastName 入力フィールドから文字列値を抽出するために "value" を使用しています。

(この演習のメインの App­Component から) このコンポーネントを使用するのは実に簡単です。

<h3>Create a new Speaker</h3>
<app-speaker-edit (onSave)="speakerSvc.save($event)"></app-speaker-edit>

この場合、コンポーネント内から Speaker を保存しないことにしています。クライアントは発生するイベントから保存を行います。speakerSvc オブジェクトは、Angular でのサービスの構築に関する前回のコラム (msdn.microsoft.com/magazine/mt826349) で扱った、依存関係が挿入される SpeakerService です。

コンポーネント化によって得られるのは、UI 「コントロール」を、内部でどのように機能するかを考えることなく作成し、ドロップして使用できる機能です。

イベントの発生

ここでまとめに入っても良いのですが、もう 1 つ注目すべき Angular イベントの注意事項があります。開発者は、オートコンプリートによる値の提案を表示するなど、キー ストロークをトラップして入力されたデータに対処するというテクニックをよく使います。これに対応する従来の DOM イベントは、onBlur や onKeyUp などのメソッドです。たとえば、各キー ストロークを追跡して、ユーザーの入力に合わせて表示するとします。Angular に不慣れな開発者は、以下のようにすれば機能すると考えるかもしれません。

@Component({
  selector: 'loop-back',
  template: `
    <input #box>
    <p>{{box.value}}</p>
  `
})
export class LoopbackComponent { }

しかし、このコードを実行しても、入力される文字は表示されません。実際、何も起きません。これは、ブラウザーがイベントを発生させない限り、Angular もイベントを発生させないためです。そのため、Angular では、以下に示すテンプレート ステートメント 0 のように、イベンドで呼び出されるコードが何の操作も完了しなくても、イベントを発生させる必要があります。

@Component({
  selector: 'loop-back',
  template: `
    <input #box (keyup)="0">
    <p>{{box.value}}</p>
  `
})
export class LoopbackComponent { }

"keyup" バインディングに注目してください。これは Angular に、Input 要素にキー イベントを登録するように指示します。それによって Angular にはイベントをトリガーし、ビューを更新する機会が与えられます。最初は、この方法を用いるのは少し厄介です。しかし、この方法だと、Angular は常にどのようなイベントもポーリングしないため、大量の CPU サイクルを消費する必要がありません。

まとめ

一部のベテラン Web 開発者は、今回の内容に多少混乱し、厄介に感じて、「古き良き HTML フォームに戻ることはできないのか」と思われるかもしれません。 Angular のやり方は、常に明確なわけではありません。しかし、一度その考え方と原理が明確になれば、通常は分かりやすく、合理的です。今回の例では、Angular のやり方というのは、コンポーネントの概念を取り入れ、入力を取り込む方法と処理方法を把握する便利な「構造」を組み立てるという観点から考えることです。これにより、単一のページにこのようなコンポーネントを複数用いるといったような「従来の Web」の考え方にはみられなかったある程度の柔軟性を得られます (ページ全体を更新して結果を表示する必要がある場合、入力要件ごとに 1 回の入力、検証、保存、レンダリングのサイクルを行うことになります)。

しかし、ユーザーが既存の Speaker を編集した場合にシステムの他の部分を更新する方法を把握する必要があるなど、他にも問題はあります。Angular にはその解決策がありますが、反応型プログラミングの域に到達できるまでにはもう少しステップが必要です。それでも、最終回に近づいていますので、あと数回お付き合いください。しかし、いつものように、それまでの間、コーディングを楽しんでください。


Ted Newardは、シアトルを拠点に活躍している、ポリテクノロジーに関するコンサルタント、講演者、および指導者です。現在は、Smartsheet.com で、開発者リレーションのディレクターを務めています。これまでに非常に多くの記事を執筆している Ted は、さまざまな書籍を執筆および共同執筆し、世界を股に掛けて仕事をしています。彼の連絡先は、ted@tedneward.com です。また、blogs.tedneward.com にブログを公開しています。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの James Bender に心より感謝いたします。


この記事について MSDN マガジン フォーラムで議論する