トピック
Angular2 CORE DOCUMENTATIONのGUIDEの翻訳です。
- DOCUMENTATION OVERVIEW - ドキュメント概要 -
- ARCHITECTURE OVERVIEW - 構文概要 -
- DISPLAYING DATA - データ表示 -
- USER INPUT - ユーザー入力 -
- FORMS - フォーム -
- DEPENDENCY INJECTION - 依存性注入 -
- STYLE GUIDE - スタイルガイド -
注意1)ここに掲載されていない項目は、Angular2 CORE DOCUMENTATIONのGUIDEを直接参照してください。
注意2)2016年11月10日時点の翻訳です。翻訳者はTOEICで700点くらいの英語力なので、英訳が間違っている可能性があります。しかもかなり意訳している箇所もあります。もし意訳を通り越して、誤訳になっているような箇所がありましたらご指摘ください。
FORMS - フォーム -
フォームは使い勝手がよく、一度にまとめてデータ入力を実行できる強力なツールです。Angularのフォームでは、データとユーザーコントロールを結びつけ、変化を監視し、入力内容を確認してエラーを出すことができます。
フォームはログインやヘルプリクエストの送信、注文や飛行機の予約、会議のスケジュールなど、数えきれないほどの様々なデータ入力タスクで使われています。フォームは業務アプリにとって、なくてはならない存在といえるでしょう。
熟練したweb開発者であれば、HTMLをすべて正しいタグで打つこともできます。しかし、フォームをベースにしたワークフローで、ユーザーに複数のデータ入力を行わせ、効率的、効果的に進めていくことは彼らにとってもなかなか難しいところです。
その難解さはデザインスキルによるところもありますが、今回の章でデザインについては扱いません。
今回扱うAngularフォームの章では、デザインスキルと同様に必要となる双方向データバインディングや変更の追跡、バリデーション、エラーハンドリングなどのフレームワークによるサポートを取り上げます。
スクラッチで簡単なフォームを一つずつ構築していき、次にあげる手法を学んでいきます。
- コンポーネントとテンプレートでAngularフォームを構築する方法
-
[(ngModel)]
シンタックスを使ってインプットコントロールにある値を読んだり、書き込んだりする双方向データバインディングを行う方法 - フォームと合わせて
ngModel
を使い、フォームコントロールの状態や正当性の変化を追う方法 - コントロールの状態を追跡する特殊なCSSクラスを使って、わかりやすく見た目にフィードバックさせる方法
- ユーザーにバリデーションエラーを表示し、フォームコントロールができるかできないかを示す方法
- テンプレート参照関数 を使い、HTML要素の情報を共有する方法
live exampleを動かしてみてください。
## テンプレート駆動フォーム
私たちの多くはフォームを構築する際、この章で取り上げているフォーム特有のディレクティブや技術を有するAngularテンプレートシンタックスを使ってテンプレートを書いています。
この章で取り上げているのは、フォームを作る方法ではなく、テンプレートシンタックスを扱う方法です。
Angularテンプレートを使えば、必要となりうるほぼすべてのフォーム、たとえばログインフォームやコンタクトフォーム、その他多くの業務用フォームを作ることができます。創造的にコントロールを設計し、それをデータと結びつけ、バリデーションルールを特定してバリデーションエラーを表示し、条件によって特定のコントロールの使用可否を決め、あらかじめ用意された視覚的なフィードバックを引き起こすなど、もう本当に色々なことができるのです。
しかもAngularは、私たちがよく悪戦苦闘する、何度も出てくる面倒臭い処理の多くをやってくれるので、フォームの構築がとても簡単になります。
次のテンプレート駆動フォームで構築方法を検討し、学習していきます。
ここヒーロー職業紹介所では、私たちが契約しているヒーローの個人情報を管理するため、このフォームを使っています。どんなヒーローでも職は必要です。私たちの社命は、ちゃんとしたヒーローをちゃんとした危機に遭遇させることなのです!※訳者注:原文ママです。
このフォームの3つのフィールドのうち、2つは必須項目です。必須のフィールドは見分けやすくするため、左側に緑色のバーを設けています。
ヒーローの名前を消すと、スタイルと紐づいた注意喚起のバリデーションエラーが表示されます。
このとき、submitボタンが使えず、インプットコントロールの左側にある"必須"バーが緑から赤に変わることに注意してください。
標準的なCSSを使って、"必須"バーの色と表示位置を変更できます。
次から始まる一連の小項目で、このフォームを構築していきます。
-
Hero
モデルのクラスを作る - フォームをコントロールするコンポーネントを作る
- テンプレートで初期のフォームレイアウトを作る
- 双方向データバインディングのシンタックスである
ngModel
を使って、各フォームにあるインプットコントロールのデータプロパティを作る - 各フォームにあるインプットコントロールに
name
属性を加える - 視覚的なフィードバックを付与するため、カスタムCSSを加える
- バリデーションエラーメッセージを表示、非表示にする
- ngSubmitを使って、フォームの送信を扱う
- フォームが有効(valid)になるまで、フォームのsubmit ボタンを利用できなくする
セットアップ
新しいプロジェクトフォルダ(angular-forms
)を作り、QuickStartの指示に従って構築してください。
もしくは、QuickStartのソースをダウンロードして始めてください。
Hero
モデルのクラスを作る
ユーザーがフォームデータを入力するときは、そのデータの変更内容をとらえてモデルのインスタンスを更新します。モデルがどのようになっているかわからないと、フォームを設定することはできません。
モデルは、アプリケーションの重要事項をおさえている"プロパティバッグ"と同じくらいシンプルであるべきです。今回のモデルには、3つの必須フィールド(id
、 name
、power
)と1つのオプションフィールド(alterEgo
)をもったHeroクラスを書きます。
アプリのフォルダの中にhero.ts
という新しいファイルを作成し、次のようにクラスを定義します。
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}
requireも振舞いもない貧相なモデル。完璧ですね。
TypeScriptのコンパイラは、publicなコンストラクタのパラメータ1つ1つに対してpublicなフィールドを与え、新しいヒーローを作成したときは自動的にパラメータの値をそのフィールドに割り当てます。
alterEgo
はオプションなので、コンストラクタでは省かれます。※alterEgo?
の中にある (?)に注意してください。
次のようにして、新しいヒーローを作ることができます。
let myHero = new Hero(42, 'SkyDog',
'Fetch any object at any distance',
'Leslie Rollover');
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
フォームコンポーネントを作る
Angularフォームは2つの構成要素でできています。HTMLをベースにしたテンプレートと、データとユーザーインタラクションを扱うコードをベースにしたコンポーネントです。
まずはヒーロー編集者のできることを簡単に示してくれるコンポーネントから始めます。
hero-form.component.ts
という新しいファイルを作って、次のように定義します。
import { Component } from '@angular/core';
import { Hero } from './hero';
@Component({
moduleId: module.id,
selector: 'hero-form',
templateUrl: 'hero-form.component.html'
})
export class HeroFormComponent {
powers = ['Really Smart', 'Super Flexible',
'Super Hot', 'Weather Changer'];
model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');
submitted = false;
onSubmit() { this.submitted = true; }
// TODO: Remove this when we're done
get diagnostic() { return JSON.stringify(this.model); }
}
こののコンポーネントに特別なことはなく、フォーム特有のものや、これまでにみてきたコンポーネントと区別できるようなものは何もありません。
前の章で学んだAngularのコンセプトさえわかっていれば、このコンポーネントを理解することができるでしょう。
- いつものようにAngular のライブラリから
Component
デコレータをインポートする。 - さきほど作成した
Hero
モデルをインポートする。 -
@Component
のセレクタにある"hero-form"という値は、親テンプレートに<hero-form>
タグを使ってこのフォームを配置するということを意味する。 -
moduleId: module.id
というプロパティは、モジュールに関連するtemplateUrl
を読み込むための基盤を設定する。 -
templateUrl
プロパティは、hero-form.component.html
というテンプレートHTMLが書かれた別のファイルを指定している。 - デモ用に
model
とpowers
というダミーのデータを定義。いずれ実データを取得、保存するためのデータサービスを導入するようになれば、おそらくこれらのプロパティは親コンポーネントとバインディングするためにinputsとoutputsを設定することになる。 - モデルをJSONデータにして返すために、最後のところで
diagnostic
プロパティを設置している。これで開発中の確認作業が楽になる。(後でコードを削除するため、クリーンアップ・メモを残しました)
なぜ、よくほかの開発者用ガイドでやられているように、コンポーネントファイルの中にインラインでテンプレートを書かないのでしょうか。
どんな場面でも通用する"正しい"回答などありません。テンプレートが短いのであれば、テンプレートをインラインにするのもいいと思います。しかし、多くのフォーム用テンプレートは短くありません。TypeScriptやJavaScriptのファイルはたいてい肥大化したHTMLを書く(もしくは読む)ものとしては向いておらず、HTMLとコードが混在したファイルを積極的にサポートしてくれるエディタもほとんどないのです。このようにはっきりと明確な目的があるのであれば、小さなファイルがあったとしても問題ありません。
私たちは、別の場所にHTMLテンプレートを置くことを良い選択肢としました。では、早速テンプレートをそのように書いてみましょう。新しいものを書く前に、少し戻ってapp.module.ts
と app.component.ts
を書き直し、新しくHeroFormComponent
を作ります。
app.module.tsの書き直し
app.module.ts
はアプリケーションのルートモジュールを定義しています。その中でアプリケーション内で使用する外部モジュールを特定し、HeroFormComponent
のようなこのモジュールで使うコンポーネントを宣言しています。
テンプレート駆動フォームはそれ自身がモジュールでもあるので、フォームを使う前にアプリケーションモジュールのimports
の配列にFormsModule
を加える必要があります。
"QuickStart"バージョンの内容を次のように書き換えます。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroFormComponent } from './hero-form.component';
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
AppComponent,
HeroFormComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
3点変更しました。
FormsModule
と新規作成したHeroFormComponent
をインポートしました。ngModule
デコレータで定義されたimports
のリストにFormsModule
を加えました。これにより、このアプリケーションはngModel
を含むすべてのテンプレート駆動フォームの機能にアクセスできるようになります。ngModule
デコレータで定義されたdeclarations
のリストにHeroFormComponent
を加えました。これにより、HeroFormComponent
コンポーネントはこのモジュール全体を通して適用されるようになります。
コンポーネント、ディレクティブ、パイプがimports
配列にあるモジュールに属している場合、declarations
配列では宣言しないでください。書き込んだものがこのモジュールに属すべきものであれば、declarations
配列で宣言してください。
app.component.tsを書き直す
app.component.ts
はアプリケーションのルートコンポーネントです。そこを新規作成したHeroFormComponent
のホストにします。
"QuickStart"バージョンの内容を次のように書き換えます。
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: '<hero-form></hero-form>'
})
export class AppComponent { }
変更は1点のみです。
template
は、コンポーネントのselector
プロパティで特定された単なる新しいタグ要素です。これにより、アプリケーションコンポーネントが読み込まれるときにheroフォームが表示されます。
イニシャルHTMLフォームテンプレートを作る
hero-form.component.html
という新しいテンプレートファイルを作り、次のように定義してください。
<div class="container">
<h1>Hero Form</h1>
<form>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
これは普通の古いHTML5で書いたものです。name
とalterEgo
の2つのHero
フィールドを提示し、インプットボックスを使ったユーザー入力でそれらを展開しています。
Name <input>
コントロールはHTML5のrequired
属性を有しています。一方、Alter Ego <input>
はオプションなので、required
属性は持っていません。
スタイリングするためのクラスをつけた、一番下にあるSubmitボタンを押してみました。
**Angularはまだ動きません。**この時点では、バインディングもなく、何のディレクティブもついていない、ただのレイアウトです。
container
、form-group
、form-control
、btn
はTwitter Bootstrap由来のもので、純粋に見栄え用です。ちょっと自分のフォームをカッコつけたくて、Bootstrapを使っています。やあ、なんてスタイルの少ないフォームなんだろうか!
ANGULAR FORMS DO NOT REQUIRE A STYLE LIBRARY
Angularはcontainer
、form-group
、form-control
、btn
なんてクラスを使う必要はなく、どんな外部ライブラリのスタイルだって必要ありません。AngularアプリはすべてのCSSライブラリを使用することができますし、何も使わないこともできます。
スタイルシートを加えてみよう。
1. アプリケーションルートフォルダでターミナルウィンドウを開き、次のコマンドを入力します。
npm install bootstrap --save
2.index.html
を開き、<head>
に次のリンクを加えます。
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
*ngForとPowersを加える
Add Powers with *ngFor
私たちのヒーローは、紹介所公認のpowersリストの中から、1つのスーパーパワーを選ぶことができます。そのリストは内部的に(HeroFormComponent
の中で)管理しています。
フォームにselect
を加え、*ngFor
( 前にDisplaying Data の章で確認した技術)を使って powers
リストにオプションを紐づけていきます。
次のHTMLをAlter Egoグループの直下に加えてみましょう。
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" id="power" required>
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
</div>
Powersリストの中で、各power用に<options>
タグが繰り返されています。p
というテンプレート入力変数は、それぞれのイテレーションの中で異なるpowerの値をとっており、二重引用符がついたインターポレーションのシンタックスを使って、その名称を表示しています。
ngModelを使った双方向データバインディング
ここですぐにアプリを起動すると、がっかりします。
まだヒーローとのバインディングを行っていないので、ヒーローのデータが見えません。以前の章で学んだことを覚えていますね。そう、 Displaying Dataで、私たちはプロパティバインディングを学習しました。 また、User Input では、イベントバインディングを使ってDOMイベントを読み取る方法や、表示された値を使ってコンポーネントプロパティを更新する方法が示されていました。
今、私たちは表示と読み取りと抽出を一度に行う必要があります。
これらの技術を私たちのフォームに再び使うことで、新しい何かを紹介されるまでもなく、[(ngModel)]
シンタックスが私たちのフォームとモデルを超簡単にバインディングしてくれるのです。
"Name"を持つ<input>
タグを見つけて、次のように更新します。
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name">
TODO: remove this: {{model.name}}
私たちのやっていることを確認できるように、インプットタグの後に診断用のインターポレーションを加えています。確認が終わったときに破棄するための注意書きも残しておきました。
バインディングシンタックス( [(ngModel)]="..."
)に焦点を絞ってみます。
アプリを起動し、Nameのインプットボックスにタイピングして、文字を入力したり消したりしてみてください。挿入されたテキストの表示、非表示が確認できます。この時点のものは、次のように見えるはずです。
インターポレーションの診断によって、値がインプットボックスからモデルへと流れ込み、再びビューに反映されていることが証明されました。これが双方向データバインディングです!
name
属性を<input>
タグに加え、それに"name"をセットしたことにも注意してください(ここでいう"name"とは、ヒーローのname
のことです)。どんなユニークな値であってもそうですが、説明的な名称を使うことは有効です。name
属性の定義は、フォームと合わせて[(ngModel)]
を使うときに必要となります。
Angularは内部的に
FormControls
を作り、それらをAngularが<form>
タグに付属させるNgForm
ディレクティブに登録します。FormControl
は、name属性に割り当てたname
の下に登録されます。NgForm
については、この章の後の方でふれていきます。
同じように、[(ngModel)]
バインディングとname
属性をAlter EgoとHero Powerに加えてみましょう。メッセージと紐づいたインプットボックスを削除し、一番上にコンポーネントのdiagnostic
プロパティとの新しいバインディングを加えます。そして双方向データバインディングが、ヒーローモデル全体で動いていることを確認します。
修正後、フォームの核となる部分は、次のような3つの[(ngModel)]
バインディングとname
属性になります。
{{diagnostic}}
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo"
[(ngModel)]="model.alterEgo" name="alterEgo">
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" id="power"
required
[(ngModel)]="model.power" name="power">
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
</div>
- インプットコントロールとラベルを合わせるため、各インプット要素にはラベル要素の
for
属性に使われているid
プロパティが配置されています。- Angularフォームがフォームにコントロールを組み込むため、各インプット要素には
name
プロパティが配置されています。
ここでアプリを起動し、すべてのヒーローモデルのプロパティを変更してみると、フォームは次のように表示されます。
フォームの一番上の方にある診断を見ると、モデルに行った変更内容がすべて反映されていることが確認できます。
当初の目的が満たされたので、一番上にある{{diagnostic}}
バインディングを削除しておきます。
[(ngModel)]の内側
このセクションでは補足として[(ngModel)]を深く追求してみます。興味がなかったら、見送ってどうぞ。
バインディングシンタックス[()]という符号は、何が起こっているかを知るためのいい手がかりとなります。
プロパティバインディングでは、値がモデルから画面上のターゲットとなるプロパティに流れ込みます。ターゲットとなるプロパティは、名前をかぎ括弧[]でくくられて特定されます。これはモデルからビューへの単方向データバインディングです。
イベントバインディングでは、画面上のターゲットとなるプロパティからモデルへと値が流れ込みます。ターゲットとなるプロパティは、、名前を丸括弧()でくくられて特定されます。これはビューからモデルへと、逆方向になる単方向データバインディングです。
なので、Angularが双方向データバインディングを表記し、両方向へのデータの流れを示す方法として[()]という符号で連結することを選んだのは自然な流れといえるでしょう。
事実、
NgModel
バインディングを分割し、別々の2つのモードにわけることもできます。その場合、"Name"<input>
バインディングを書き直すとこのようになります。
app/hero-form.component.html(excerpt)
<input type="text" class="form-control" id="name"
required
[ngModel]="model.name" name="name"
(ngModelChange)="model.name = $event" >
TODO: remove this: {{model.name}}
> プロパティバインディングは馴染み深く感じられますが、イベントバインディングはちょっと変な感じがしますね。
> `ngModelChange`は`<input>`要素のイベントではありません。実を言うと、`ngModelChange`は、`NgModel`ディレクティブのイベントプロパティなのです。Angularはform [(x)]の中にあるバインディングのターゲットを確認すると、`x`ディレクティブには`x`インプットプロパティと`xChange`アウトプットプロパティがあると予測します。
> 他で変わっている点といえば、`model.name = $event`というテンプレートの表記でしょうか。DOMイベントから発生した`$event`オブジェクトはよくみることがあります。しかし、`ngModelChange` プロパティはDOMイベントを発生させるわけではありません。Angularの`EventEmitter`が、DOMイベントを発生させ、発火時にインプットボックスの値を返しています(正確にいえば、このとき返す値はモデルの`name`プロパティに割り当てられたものです)。
> さて、知ったはいいですが、これは実践的なことなのでしょうか?私たちはたいていの場合、 `[(ngModel)]`を使うことをよしとしますが、キー操作のデバウンスやスロットル処理のようなイベントバインディングで、特殊な方法を使わざるを得ないケースではバインディングを分割することもありえます。
> `NgModel`や他のテンプレートシンタックスについてもっと学びたいときは、[Template Syntax](https://angular.io/docs/ts/latest/guide/template-syntax.html)の章を参照してください。
### ngModelを使って、状態の変化と正当性を追跡する
フォームはただデータをバインディングさせるたけではありません。フォーム上にあるコントロールの状態を把握しておくことも必要です。
フォームで`ngModel`を使う場合、もたらされるメリットは双方向データバインディングだけてはありません。ユーザーがコントロールを触ったこと、値を変更したこと、または値が不正になったことなどを知らせてくれます。
NgModelディレクティブはただ状態を追跡するたけではありません。状態を反映したAngularの特殊なCSSを使い、コントロールを更新します。そのCSSの名前を入れ込むことで、コントロールの見かけを変更したり、メッセージの表示、非表示を行うことができるようになります。
| 状態| trueの場合のクラス | falseの場合のクラス |
|:-:|:-:|:-:|
|Control has been visited|`ng-touched`|`ng-untouched`|
|Control's value has changed|`ng-dirty`|`ng-pristine`|
|Control's value is valid|`ng-valid`|`ng-invalid`|
一時的に**spy**という名前の[テンプレート参照変数](https://angular.io/docs/ts/latest/guide/template-syntax.html#ref-vars)を"Name" `<input>`タグに加え、クラス名が表示されるspyを使ってみましょう。
```ts:app/hero-form.component.html(excerpt)
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#spy >
<br>TODO: remove this: {{spy.className}}
アプリを起動し、Nameインプットボックスを見てみます。次の4つのステップに、正確に従ってください。
- 触らずに、見てみる
- インプットボックスをクリックし、そのあとテキストインプットボックスの外をクリックする
- 名前の最後にスラッシュを加える
- 名前を消す
アクションと効果は次のようになります。
クラスの名前とその変化を、次の4つのセットで確認できるようにしてみました。
ng-valid
と ng-invalid
のペアは、大変興味深いです。データが不正なときは視覚的に強い信号を送りたいですし、必須フィールドには印をつけておきたいです。なので、視覚的フィードバックのためにカスタムCSSを加えてみます。
当初の目的が満たされたので、#spy
テンプレート参照変数とTODO
を削除してください。
視覚的フィードバックのためにカスタムCSSを加える
必須フィールドと不正なデータに対し、インプットボックスの左側に色のついたバーを同時につけることができます。
プロジェクト上のindex.html
と同階層にforms.css
という新しいファイルを加え、2つの新しいスタイルを追記することで、この効果を再現してみましょう。
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
これらのスタイルは、Angularの正当性にかかるクラスと、HTML5の"required"属性の2つを選択しています。
これらのスタイルシートを反映するために、index.html
の<head>
を更新します。
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="forms.css">
バリデーションエラーメッセージの表示と非表示
もっとうまくやる方法があります。
"Name"インプットボックスは必須項目です。空欄にすると、赤いバーが表示されます。それは何かが間違っていることを示していますが、何が間違っているのか、何をすべきなのかがわかりません。ヘルプ用のメッセージを表示するために、ng-invalid
クラスに手を加えます。
ユーザーが名前を削除したとき、ヘルプ用のメッセージが見えるようにしてみました。
この効果を実現させるため、<input>
タグを次のように拡張しました。
- テンプレート参照変数を加える
- コントロールが不正な場合のみ
<input>
タグの近くに表示される<div>
の中に、*"is required"*というメッセージを追加する
nameインプットボックスは次のようになります。
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#name="ngModel" >
<div [hidden]="name.valid || name.pristine"
class="alert alert-danger">
Name is required
</div>
テンプレートの中からインプットボックスにあるAngularコントロールにアクセスするためには、テンプレート参照変数が必要になります。ここではname
という変数を作り、"ngModel"という値を与えています。
なぜ"ngModel"なんでしょうか。ディレクティブの exportAs プロパティは、参照変数をディレクティブにリンクさせる方法をAngularに伝えています。
ngModel
ディレクティブのexportAs
プロパティが図らずも"ngModel"になったので、name
にngModel
を設定しています。
name
コントロールのプロパティと、メッセージを扱う<div>
要素のhidden
プロパティをバインディングしたので、"name"にかかるエラーメッセージの視覚化も管理できるようになりました。
<div [hidden]="name.valid || name.pristine"
class="alert alert-danger">
このサンプルの場合、コントロールがvalid、もしくはpristineのときはメッセージが非表示になります。pristineは、このフォームが表示されてからユーザーによって値が変更されていないという意味です。
このUXは開発者の選択次第です。人によっては、常にメッセージを確認できるようにしたいかもしれません。pristine
という状態を無視すれば、メッセージは値が正当なときのみ隠されるようになります。私たちが新しい(空欄の)ヒーローや不正なヒーローを伴ってこのコンポーネントにたどり着いたとき、何もしないうちにすぐエラーメッセージが表示されます。
しかし一方で、ある人たちはこのフォームのふるまいに面を食らうかもしれません。そういう人は、ユーザーが不正な変更を行ったときのみ、そのメッセージを確認したいと思っています。この場合は、コントロールが"pristine"の間メッセージを隠していれば目的は達成されます。フォームに新しくヒーローを追加するとき、この選択肢が重要となってきます。
ヒーローのAlter Ego
はオプションなので、そのままにしておいて構いません。
ヒーローのPower
というセレクションは必須項目です。必要であれば同様に<select>
に対してエラーハンドリングを行うこともできますが、セレクションボックスはpowerに対し正当な値で制限されているので、あまり意味がありません。
ヒーローを追加し、フォームをリセットする
このフォームで新しいヒーローを追加しましょう。"New Hero"ボタンをフォームの底部に設置し、そのクリックイベントをコンポーネントのメソッドに紐づけます。
<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>
newHero() {
this.model = new Hero(42, '', '');
}
アプリケーションを再起動し、New Heroボタンをクリックするとフォームがクリアになります。インプットボックスの左にあるrequired
バーが赤くなり、name
とpower
のプロパティが不正であることを示しています。これらは必須フィールドなので当然といえば当然ですが、まだフォームは初期状態(この部分は何も変更していません)なので、エラーメッセージは隠れたままです。
名前でエンターを押して、再度New Heroをクリックしてみましょう。今度はエラーメッセージが表示されました!なぜでしょうか。新しい(空欄の)ヒーローを表示するとき、そのような挙動は望ましくありません。
ブラウザツールで要素を調査してみると、nameインプットボックスがもはや初期状態ではなくなっていることがわかります。ヒーローを置き換えても、コントロールの初期状態を復元するわけではありません。
この反応で、Angularはヒーローそのものを置き換えているのか、
name
プロパティをクリアにしているのかをプログラム上では区別できないことがわかります。Angularは仮定を行わず、コントローラを現在の、dirtyな状態のままにしておきます。
小技を使って、フォームコントロールを手動でリセットしなければいけません。コンポーネントに初期状態がtrue
のactive
フラグを加えます。新しいヒーローを加えるときは、active
をfalse に切り替え、さっとsetTimeout
を使ってすぐにtrueへ戻します。
active = true;
newHero() {
this.model = new Hero(42, '', '');
this.active = false;
setTimeout(() => this.active = true, 0);
}
そのあと、フォーム要素をこのactive
フラグに紐づけます。
<form *ngIf="active">
NgIf
をactive
フラグと紐づければ、"New Hero"をクリックしたときにDOMからフォームを削除して瞬く間に再構築を行います。再構築されたフォームは初期状態となっており、エラーメッセージは隠れたままです。
これは適切なフォームリセット機能を実装するまでの、一時的な措置です。
ngSubmitでフォームを送信する
ユーザーがデータをフォームに入力したあとは、その内容を送信できないといけません。フォームの一番下にあるSubmitボタンそれ自体は特になんでもないですが、type (type="submit"
)によってフォーム送信のトリガーとなります。
"form submit"は今のところ役に立ちません。有効にするには、もう一つのAngularディレクティブであるNgSubmit
を使って<form>
を更新し、イベントバインディングを使ってHeroFormComponent.submit()
メソッドと紐づけます。
<form *ngIf="active" (ngSubmit)="onSubmit()" #heroForm="ngForm">
最後のところに余分なものが入りました!#heroForm
というテンプレート参照変数を定義し、"ngForm"という値を使って初期値を設定しています。
heroForm
という変数は、フォーム全体を管理するNgForm
ディレクティブを参照します。
NgFormディレクティブ
NgForm
とは何でしょうか?結局、私たちはNgForm ディレクティブは追加しませんでした。
ただし、AngularはNgFormを追加しています。Angularは
NgForm
ディレクティブを作成し、<form>
タグに対して自動的に付加します。
NgForm
ディレクティブはform
要素を追加機能で補います。ngModel
ディレクティブやname
属性を伴う要素のコントロールをもち、その正当性も含めて、プロパティの監視を行います。すべてのコントロールが正当であると判断されたときのみ、trueであるvalid
プロパティを自身が持つようになります。
テンプレートの後半で、heroForm
変数を経由してボタンのdisabled
プロパティとフォーム全体の正当性を紐づけています。ここで少しマークアップしてみます。
<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button>
アプリケーションを再起動してください。フォームが正当な状態で開かれると、ボタンも使用可能になります。
では、Name
を削除してください。以前と同様、エラーメッセージの中に"name required"ルールがちゃんと表示されますが、それを無視します。そのとき、Submitボタンもまたdisabledになっています。
すごくないですか?ちょっと考えてみてください。Angularの助けなくフォームの正当性をボタンのenable/disableに割り当てようとすると、どれだけのことをしないといけなくなるでしょうか。
ここで行った作業は単純です。
- (拡張した)フォーム要素に、テンプレート参照変数を定義する
- 50行程度離れたボタンにある変数を参照する
2つのフォームの範囲を切り替える(特別クレジット)
フォーム送信は、もうそれほど劇的なものではありません。
デモを見ていても驚きはありません。正直なところ、フォームに関してちょっと色をつけたところで、新しく学ぶものは何もないのです。しかしこれは新しく、成功するためのバインディングスキルを身に着ける絶好の機会といえます。もし興味がなければ、この章の総論は飛ばして、うまくやってください。
さあ、もう少し視覚的なことをやっていきましょう。データ入力エリアを隠し、ほかの何かを表示させてみます。
<div>
でフォームを入れ子にしてから開始し、hidden
プロパティとHeroFormComponent.submitted
プロパティを紐づけます。
<div [hidden]="submitted">
<h1>Hero Form</h1>
<form *ngIf="active" (ngSubmit)="onSubmit()" #heroForm="ngForm">
<!-- ... all of the form ... -->
</form>
</div>
submitted
プロパティはフォームが送信されるまではfalse なので、メインフォームは最初から視覚化されています。このHeroFormComponent
からのフラグは覚えておきましょう。
submitted = false;
onSubmit() { this.submitted = true; }
Submitボタンをクリックすると、submitted
フラグがtrueになり、formが予定通り非表示になります。
フォームが送信済の間、何かを表示しておく必要があるでしょう。今作成した<div>
ラッパーの下に次のHTMLブロックを追加してください。
<div [hidden]="!submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9 pull-left">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9 pull-left">{{ model.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9 pull-left">{{ model.power }}</div>
</div>
<br>
<button class="btn btn-default" (click)="submitted=false">Edit</button>
</div>
私たちのヒーローが、インターポレーションバインディングによって閲覧用に再表示されました。
このHTMLのコードはコンポーネントが送信済のときのみ表示されます。
Editボタンを加え、そのクリックイベントとsubmitted
フラグを削除する動作を紐づけます。
それをクリックすると、このブロックは非表示になり、編集可能なフォームが再表示されます。
これでだいぶ気持ちが昂ぶってきたんじゃないでしょうか。
総論
この章で論じられたAngularフォームのテクニックは、次のようなフレームワークの機能で有効です。データ修正やバリーデーションなどのサポートを提供してくれます。
- AngularのHTMLフォームテンプレート
-
Component
デコレータを伴うフォームコンポーネントクラス - フォーム送信を扱う
ngSubmit
ディレクティブ -
#heroForm
、#name
、#power
のようなテンプレート参照変数 - 双方向データバインディング、バリデーション、変更監視で使う
[(ngModel)]
シンタックスとname
属性 - コントロールが正当であり、エラーメッセージの表示なのか、非表示なのかをチェックする、インプットコントロール上にある参照変数の
valid
プロパティ -
NgForm
の正当性とバインディングして、submitボタンが利用可能かどうかを管理する - ユーザーに不正なコントロールに関する視覚的フィードバックを行うカスタムCSSクラス
最終的なプロジェクトのフォルダ構成は次のようになりました。
angular-forms
├ app
│ ├ app.component.ts
│ ├ app.module.ts
│ ├ hero.ts
│ ├ hero-form.component.html
│ ├ hero-form.component.ts
│ └ main.ts
├ node_modules ...
├ index.html
├ package.json
└ tsconfig.json
ソースの最終版は次の通りです。
import { Component } from '@angular/core';
import { Hero } from './hero';
@Component({
moduleId: module.id,
selector: 'hero-form',
templateUrl: 'hero-form.component.html'
})
export class HeroFormComponent {
powers = ['Really Smart', 'Super Flexible',
'Super Hot', 'Weather Changer'];
model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');
submitted = false;
onSubmit() { this.submitted = true; }
// Reset the form with a new hero AND restore 'pristine' class state
// by toggling 'active' flag which causes the form
// to be removed/re-added in a tick via NgIf
// TODO: Workaround until NgForm has a reset method (#6822)
active = true;
newHero() {
this.model = new Hero(42, '', '');
this.active = false;
setTimeout(() => this.active = true, 0);
}
}
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form</h1>
<form *ngIf="active" (ngSubmit)="onSubmit()" #heroForm="ngForm">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#name="ngModel" >
<div [hidden]="name.valid || name.pristine"
class="alert alert-danger">
Name is required
</div>
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo"
[(ngModel)]="model.alterEgo" name="alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" id="power"
required
[(ngModel)]="model.power" name="power"
#power="ngModel" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div [hidden]="power.valid || power.pristine" class="alert alert-danger">
Power is required
</div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button>
<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>
</form>
</div>
<div [hidden]="!submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9 pull-left">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9 pull-left">{{ model.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9 pull-left">{{ model.power }}</div>
</div>
<br>
<button class="btn btn-default" (click)="submitted=false">Edit</button>
</div>
</div>
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroFormComponent } from './hero-form.component';
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
AppComponent,
HeroFormComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: '<hero-form></hero-form>'
})
export class AppComponent { }
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
// Compiles the module (asynchronously) with the runtime compiler
// which generates a compiled module factory in memory.
// Then bootstraps with that factory, targeting the browser.
platformBrowserDynamic().bootstrapModule(AppModule);
<html>
<head>
<title>Hero Form</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
href="node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="forms.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
Next Step
DEPENDENCY INJECTION - 依存性注入 -