読者です 読者をやめる 読者になる 読者になる

ITの隊長のブログ

ITの隊長のブログです。CakePHPとPlayFrameworkを使って仕事しています。最近はAngular2をさわりはじめたお(^ω^ = ^ω^)

jQueryしか使ったことがない人がAngularのFormで試したことを書く

Angular2 JavaScript

スポンサードリンク

この記事はAngular Advent Calendar 2016 17日目の記事です。(遅刻しました)

この記事を書いている人

  • jQueryを使って、基本的な使い方でホームページのナビゲーション開閉やタブのアニメーションを書ける人
  • jQueryを使って、動的なFormを作ったり、サーバへリクエスト飛ばしたりする人
  • jQueryを使って、5000行ぐらいのシミュレーターみたいなアプリケーションを書いたことがある人

仕事で使っているというだけで、jQueryJavaScriptを深くは理解していません。大好き!ってわけでもありません。「javascript やりたいこと」でググって解決するレベルです。Googleがないと仕事できない。

そんな人がAngular2を試したら

  • 「AngularJS1.x触ったことあるしへーきへーき」 -> (°ω° オレノシッテイルアンギュラハドコ?
  • 「Typescript?ES6?なにそれ?」
  • 「webpack?gulpとかnpmじゃだめなの?」

↑の状態で挑んだら簡単に死亡いたしました。本当にありg(ry

で、そんな人がググり続けたとしても、古い情報なのか新しい情報なのかわからるはずもなく、地雷を踏みづづけるだけで、一向に闇から抜け出せません。マジで挫折一歩手前。

近くに簡単に質問できる人もコミュニティもないため、洋書ですが本を購入することにしました。

  • ng-book2

www.ng-book.com

記事の内容のほとんどが↑の本から学習したことなので、この記事読まなくて↑の本読んだ方が早かったりするかもです(^^ゞ ※英語の本です。 ※私は英語はほとんど理解してませんが、Google翻訳を使って読むとびっくりするほど内容が理解できます。ありがとうGoogle

この本購入してからAngular楽しいお(^ω^ = ^ω^)

本記事は ng-book2 を読んで、Angular Formで学習したこと、Angular Formで試したことのログをまとめた記事です。

Form in Angular2

この記事で試したソースコードgithubに上がっています。実際にコードを動かしたほうが理解すると思いますー。

github.com

はじめの環境構築

必要なアプリケーションのインストール

この記事は、Typescriptとangular-cliを使います。また、OSはOSXです。windowsの方はすみません(´・ω・`)

自分の環境は以下。

$ sw_vers 
ProductName:  Mac OS X
ProductVersion: 10.11.6
BuildVersion: 15G1108

$ node -v
v6.6.0

$ npm -v
3.10.9

それでは環境構築します。

$ npm install -g typescript
$ npm install -g angular-cli@1.0.0-beta.18
# angular-cliの監視オプションに使うツールらしい
# linuxの人はどうすればいいの? 
# -> https://ember-cli.com/user-guide/#watchman
# windowsの人はどうすればいいの?
# -> なんか必要ないんだって。nodea.jsに実装されているwatcherを使うんだって
$ brew install watchman

無事インストールできたら、確認。

$ ng version
angular-cli: 1.0.0-beta.18
node: 6.6.0
os: darwin x64

おk

プロジェクトを作りましょう

$ ng new angular-hello-form-application

これだけ。That's it. 感動。

動作確認してみましょう

angular-cliApacheとかwebアプリケーション的なものを用意しなくても、すぐに動作を確認できるオプションが用意されています。

$ cd angular-hello-form-application/
$ ng serve
** NG Live Development Server is running on http://localhost:4200. **
...

ng serve実行後、すぐにURLが表示されるので、それをコピってブラウザで確認してみてください。

f:id:aipacommander:20161226192107p:plain

この画面がでてきたら、環境構築完了です!

それでは本題に進みます。

FormControlとFormGroup

Angular2でFormを使いたいなら、この2つのモジュールを利用します。

FormControlは、1つのinputフィールドを意味します。

let firstName = new FormControl("Aipa");
console.log(firstName.value);  // "Aipa"
console.log(firstName.errors); // 型はStringMap<string, any> of errors
console.log(firstName.dirty);  // 値は変わったか検知。boolean
console.log(firstName.valid);  // validateの結果が登録されます。boolean

templateで使いたい場合はこう書きます。

<!-- formの中で書く -->
<input type="text" [formControl]="name">

んで、これらをまとめてくれるのが、FormGroupです。

let yourName = new FormGroup({
  firstName: new FormControl('Aipa'),
  lastName: new FormControl('Commander')
});

console.log(yourName.value); // -> {
//  firstName: "Aipa",
//  lastName: "Commander"
// }

console.log(yourName.errors); // StringMap<string, any> of errors
console.log(yourName.dirty);  // false
console.log(yourName.valid);  // true

というふうに、まとめることができます。

FormControlは1つのフィールド、FormGroupはフィールド全体を見てくれます。

例えば、firstNameのFormControlのバリデーションがfalseだった場合、yourNameのバリデーションもfalseになります。部分的にチェックしてエラーを表示したり、全体のバリデーションがうまくいかないとsubmitさせないなどができると思います。

yourName.formControl['firstName'].valid; // false
yourName.formControl['lasttName'].valid; // true
yourName.valid; // false

簡易フォームの実装

それでは、実際に実装してみましょー。

と、その前に、ちゃんと説明しようと書いていましたが、説明の仕方に悩んだときにこんな記事を見つけました。

;°ω°)モウコレデイイジャン

もっと早く会いたかったです。

まぁ自分は自分なりに説明書いてログにします。

本題に戻ります。先ほど作ったプロジェクトのディレクトリで、componentをgenerateします。

$ ng generate component first_form

まずは、generateしたcomponentに実装していきます。

おっと、その前にFormControlやFormGroupを使えるようにするために、~/angular-hello-form-application/src/app/app.module.tsで、FormsModuleとReactiveFormsModuleをimportします。

~/angular-hello-form-application/src/app/app.module.ts

import { FormsModule, ReactiveFormsModule } from '@angular/forms'; // <- add

@NgModule({
  declarations: [
    AppComponent,
    FirstFormComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
    FormsModule,        // <- add
    ReactiveFormsModule // <- add

これでおkです。

ちなみに、FormsModuleのみimportすると

  • ngModel
  • NgForm

を組み合わせて、Formを扱うことができます。ReactiveFormsModuleも一緒にimportすると

  • formControl
  • ngFormGroup

を含め、色々使えるようになります。

ふーん。Reactive リアクティブってなに?

ぐぐったらこの記事がでてきた。ふむ。

Webフロントエンドでリアクティブプログラミング

いまいちピンときていません。分かる人いたら教えて下さい。

まぁいまはなんだっていいでしょう。実装に話を戻します。

~/angular-hello-form-application/src/app/app.component.htmlにタグを追加します。先ほどgenerateしたcomponentをrenderできるように追記します。

~/angular-hello-form-application/src/app/app.component.html

<app-first-form></app-first-form>

formの実装に入ります。

~/angular-hello-form/application/src/app/first-form/first-form.componet.htmlに、formを追加します。

<form #f="ngForm" (ngSubmit)="onSubmit(f)">
  <div class="form-group">
    <label>お名前は?</label>
    <input type="text" placeholder="名前を入力してください" name="yourName" ngModel>
  </div>
  <div class="form-group">
    <button type="submit">送信</button>
  </div>
</form>

↑のように追加すればおkです。

突然あらわれる#f=ngFormngFormにびっくりしましたが、これはFormsModuleをimportするだけで、NgForm(<form #f="ngForm" ...>)がviewで使えるようになり、また自動的に<form>にアタッチされます。

それでは、component.tsへメソッドを追加します。

~/angular-hello-form/application/src/app/first-form/first-form.componet.ts

export class FirstFormComponent implements OnInit {
  // ...
  onSubmit(form: any): void {
    console.log(form.value);
  }
}

追加したフォームに"アイパー隊長"と入力し、送信ボタンをクリックすると、consoleから、Object {yourName: "アイパー隊長"} がでてきました。これでよし。

#f=ngForm#fはテンプレートで使用できるようするローカル変数です。つまりこの箇所はローカル変数を宣言している&値をセットしているのです。

#fがフォームの値となります。「テンプレートで使える変数」という理解でおkだと思います。

また、NgFormの型はFormGroupです。なので、変数fは、FormGroupとして扱うことができます。

最初のFormGroupとFormControlの関係を説明した通り、FormControlはFormGroupに追加されて扱えるようになります。

このフォームではngModelがセットされているinputタグの値は、自動的にFormControlを作成して、属性であるname=***で関連付けられます。そして、FormGroupである変数fにセットされています。

このフォームでsubmitボタンをクリックすると、onSubmitのイベントが発生します。その時実行されるのが、<form>の属性にある(ngSubmit)="onSubmit(f.value)"です。

Angularで()は、イベントをセットするときに使う属性と理解しています。んで、実行されるのが右側のコードです。onSubmit(f.value)は、Componentに実装してあげます。

上ですでに動きは確認しました。もちろんconsole.log()だけではなく、フォームにセットされた値を確認したり、バリデーションしたり、サーバへ送信したりなど、やりたいことを実装してあげれば良いです。

よっしゃ!理解したぞ!(多分)

ここまでが、単純にフォームを使うだけのチュートリアルです。

FormBuilderでFormのカスタマイズ

ngForm、ngModelを使って、暗黙的にFormControl、FormGroupを使ってみました。

ただ、このままだとカスタマイズができません。

実践では、もっと複雑に、詳細にカスタマイズするはずですので、今度はそれができるFormBuilderを使ってみましょう。

今、自分もこのFormBuilderを実践で使っています。

ng-book2をFormBuilderはどういうふうに使えばいいの? 語るよりはまずは実際に実装してみました。

コンポーネントをジェネレートします。

$ ng generate component use-form-builder

~/angular-hello-form/application/src/app/app.component.html

<h1>
  {{title}}
</h1>
<app-first-form></app-first-form>

<app-use-form-builder></app-use-form-builder> <!-- これを追加 -->

んじゃ、使用するComponentで、FormBuilderをimportします。

~/angular-hello-form/application/src/app/use-form-builder/use-form-builder.component.ts

import { FormBuilder, FormGroup } from '@angular/forms';

importしただけじゃ使えないので、DIしましょう。 ※"DI (dependency injection)"って何?の人。自分もよくわかっていないので、近くのエロい人たちに聞いてください。

export class UseFormBuilderComponent {
  myForm: FormGroup;

  constructor(formBuilder: FormBuilder) { // <- これがDI(らしい)
    this.myForm = formBuilder.group({
      'myName': ['Aipa']
    });
  }

  onSubmit(value: string): void {
    console.log('わたしの名前は' + value);
  }
}

これで、FormBuilderが使えるようになりました。constructor()の引数にセットすることで、このComponentで利用できるようになりました。

FormBuilderでよく使うメソッドは2つ

  • control - FormControlを新規で作成します
  • group - FormGroupを新規で作成します

この2つです。

formBuilder.group({})は、FormGroupを返します。

また、formBuilder.group({})の中に、KeyValueにセットされているmyForm: ['Aipa']はFormConotrolです。また、['Aipa']は初期値です。

ちなみに、arrayってなんてのもありますが、この記事の最後あたりで試したログがあります。

説明がだるくなってきたので、まずは動かしてみてみましょう。templateを用意して、ビルドしてみてください。

~/angular-hello-form/application/src/app/use-form-builder/use-form-builder.component.html

<h2>ふぉーむびるだーずふぁいたーずです。</h2>
<form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm)">
  <div class="form-group">
    <label>お名前は?</label>
    <input type="text" placeholder="名前を入力してください" [formControl]="myForm.controls['myName']">
  </div>
  <div class="form-group">
    <button type="submit">送信</button>
  </div>
</form>

実行するとこんな画面が出力されるはず。初期値が入っていますね。

f:id:aipacommander:20161226192217p:plain

また、submitしてみると、consoleに"わたしの名前はAipa"とでるはず。これでおkです。

少し説明します。

<form>タグの属性に、[formGroup]="myForm"とあります。この[formGroup]は、ディレクティブとして動作しており、myFormをこのフォームのFormGroupとして利用することを宣言しています。

次に、<input>タグの属性に[formControl]="myForm.controls['myName']"とあります。これは既存のFormControlにFormBuilderで作成したformControlをバインドしています。これでComponent側で細かく設定する情報を付与することができます。

Validation

次はValidationを試します。

~/angular-hello-form/application/src/app/use-form/builder/use-form-builder.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-use-form-builder',
  templateUrl: './use-form-builder.component.html',
  styleUrls: ['./use-form-builder.component.css']
})

export class UseFormBuilderComponent implements OnInit {
  myForm: FormGroup;

  constructor(formBuilder: FormBuilder) { // <- これがDI(らしい)
    this.myForm = formBuilder.group({
      'myName': ['Aipa', Validators.required]
    });
  }

  ngOnInit() {
  }

  onSubmit(form: any): void {
    console.log(form);
    console.log('わたしの名前は' + form.controls['myName'].value);
  }
}

~/angular-hello-form/application/src/app/use-form-builder/use-form-builder.component.html

<h2>ふぉーむびるだーずふぁいたーずです。</h2>
<form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm)">
  <div class="form-group" [class.error]="!myForm.controls['myName'].valid && myForm.controls['myName'].touched">
    <label>お名前は?</label>
    <input type="text" placeholder="名前を入力してください" [formControl]="myForm.controls['myName']">
    <div *ngIf="!myForm.controls['myName'].valid">myName is invalid</div>
    <div *ngIf="myForm.controls['myName'].hasError('required')">myName is required</div>
  </div>
  <div class="form-group" *ngIf="!myForm.valid">Form is invalid</div>
  <div class="form-group">
    <button type="submit">送信</button>
  </div>
</form>

ValidationはValidatorsをimportして利用します。

先ほど、key valueでformControlの設定をFormBuilder.groupへ渡しました、valueの値は配列であり、要素の0番目が初期値でした、Validationは要素の1番目にセットします。今回はValidators.required(空文字NG)をセットしました。

templateには、エラーを表示するようにngIfを用意しています。最初のほうで説明したformControlやformGroupにある、validを使っています。また、セットした特定のValidationのエラーを表示したい場合は、 myForm.controls['myName'].hasError('required')を使って出力します。

次は複数のValidationをセットします。4文字以上入力しないとエラーを出力するようにしてみます。Validators.minLenghtを使います。

「複数あるから・・・」って考えたら、要素の2番めに配列でValidatorsを渡したくなると思います。それでも動きました。が、本ではそんな説明がありませんでした。(ネットにはちらほらありますが)

配列で渡すのは正規な方法ではないのかな?

この記事ではValidators.compose([])を使います。

~/angular-hello-form/application/src/app/use-form/builder/use-form-builder.component.ts

  constructor(formBuilder: FormBuilder) {
    this.myForm = formBuilder.group({
      'myName': ['Aipa', Validators.compose([ // <- これを追加
          Validators.required,
          Validators.minLength(4)
        ])
      ]
    });
  }

~/angular-hello-form/application/src/app/use-form-builder/use-form-builder.component.html

<h2>ふぉーむびるだーずふぁいたーずです。</h2>
<form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm)">
  <div class="form-group" [class.error]="!myForm.controls['myName'].alid && myForm.controls['myName'].touched">
    <label>お名前は?</label>
    <input type="text" placeholder="名前を入力してください" [formControl]="myForm.controls['myName']">
    <div *ngIf="!myForm.controls['myName'].valid">myName is invalid</div>
    <div *ngIf="myForm.controls['myName'].hasError('required')">myName is required</div>
    <div *ngIf="myForm.controls['myName'].hasError('minlength')">myName must be at least 4 characters long.</div> <!-- ここを追加 -->
  </div>
  <div class="form-group" *ngIf="!myForm.valid">Form is invalid</div>
  <div class="form-group">
    <button type="submit">送信</button>
  </div>
  <pre>{{myForm.controls['myName'].errors | json}}</pre>
</form>

全然関係ないですが、このValidators.minLength(4)、templateでエラーを出力する属性を用意したら何故か出力されませんでした。

最初書いていたコードはこちら。

<div *ngIf="myForm.controls['myName'].hasError('minLength')">myName must be at least 4 characters long.</div> <!-- ここを追加 -->
hasError('minLength') // "minLength" => ダメ、"minlenght" => おk

(°ω° キャメルケースじゃダメなのね。。。

<form>の閉じタグ1行前で、<pre>{{myForm.controls['myName'].errors | json}}</pre>で確認したら発覚しました。わかりづらい。

ちょっとハマったが、Validationを複数扱うときは、Validators.compose()を使えばおk.

Custom Validation

Validationの続きですが、自前でValidationも用意することができます。

返り値はStringMap<string, boolean>で返せば良いです。クラスがあるとわかりにくいのでようは

{ [s: string]: boolean }

これを返せばよい。

~/angular-hello-form/application/src/app/use-form/builder/use-form-builder.component.ts

function fullMatchStringAipa(control: FormControl): { [s: string]: boolean } {
  // 初期値がない場合はエラーになったので
  // nullチェックもする
  if (control.value) {
    if (!control.value.match(/^Aipa/)) {
      return {invalidAipa: true};
    }
  }
}

// ... 省略

  constructor(formBuilder: FormBuilder) {
    this.myForm = formBuilder.group({
      'myName': ['Aipa', Validators.compose([
          Validators.required,
          Validators.minLength(4),
          fullMatchStringAipa // <- これを追加
        ])
      ]
    });
  }

~/angular-hello-form/application/src/app/use-form-builder/use-form-builder.component.html

<div *ngIf="myForm.controls['myName'].hasError('invalidAipa')">myName is string "Aipa"</div>

hasError()で渡す値は、自前で用意したValidationの返り値{ [s: string]: boolean }s: stringの値を渡してください。

Watching for changes

formControlの値の変更を検知することができます。AngularJS1.xではすごく苦労した記憶があります(物覚え悪いからだと思いますが)。今回はどうでしょう。

~/angular-hello-form/application/src/app/use-form/builder/use-form-builder.component.ts

  constructor(formBuilder: FormBuilder) {
    this.myForm = formBuilder.group({
      'myName': ['Aipa', Validators.compose([
          Validators.required,
          Validators.minLength(4),
          fullMatchStringAipa
        ])
      ]
    });

    // 追加した記述
    this.myForm.controls['myName'].valueChanges.subscribe(
      (value: string) => {
        console.log('aipa value change: ', value);
      }
    );
  }

個人的にはすごくわかりやすかったです! こんなすぐ使えるのねー。

次は試したかったこと。です。

Form in Angular2 で、試したこと

Enter keyの無効化

フォームで入力中にEnter keyを2度押してしまって、ページが遷移するってことありません? わたしはよくあります。

あれが嫌で、作成するフォームはだいたい<form onsubmit="return false;">をしています。

これが良いか悪いかの判断はできませんが(できる方、教えてくださいm(_ _ )m )、とりあえずAngular2でもやりたかったので、試してみました。

<form [formGroup]="myForm"
      (ngSubmit)="onSubmit(myForm.value)"
      (keydown.enter)="keyDown($event)" <!-- 追加 -->
      class="ui form">
</form>

keydown.enterで、エンターキーが押されたときのイベントが発火したときに、メソッドをコールすることができる。Component側に下記を実装。

keyDown(event: any): void {
  console.log('You just clicked entry.');
  return event.preventDefault(); // enterを無効化
}

これでできました。

Input form bind date picker

日付入力のフォームには、date pickerをよく使いますよね。jQueryだったらすぐ使えます。

が、Angular2ではどう扱ったらいいのはわからず、ググり方もわからなかったので、時間を多く浪費しました。

pluginも色々試しましたが、何故かうまくいかなかったんですよねー。

色々学んできて、やっとできたので、これもメモしておきます。

3rd Party Library Installation

まず、必要なライブラリをインストールします。

$ npm install pikaday moment --save-dev 

今回はpikadayを使います。また、moment.jsも依存しているため、これもインストール。

んで、ビルドツールである、angular clijsonファイルに追記します。

~/angular-hello-form-application/angular-cli.json

"app": [{ 
      "styles": [
        "styles.css",
        "../node_modules/pikaday/scss/pikaday.scss"
      ],
      "scripts": [
        "../node_modules/pikaday/pikaday.js",
        "../node_modules/moment/moment.js"
      ]
}]

次に使いやすいようにdirectiveをを用意します。

$ ng generate directive date-picker
installing directive
  create src/app/date-picker.directive.spec.ts
  create src/app/date-picker.directive.ts
import { Directive, ElementRef, Input } from '@angular/core';

// 宣言?
declare let Pikaday: any;

@Directive({
  selector: '[appDatePicker]'
})
export class DatePickerDirective {

  // この変数は呼び元コンポーネントから値をもらうので
  // @Input()の値をselectorと同じにする
  @Input('appDatePicker') datePickerField: any;

  constructor(
    // pikadayのクラスのbindにはフィールドの情報が必要なため
    // ElementRefを使って自分のフィールドを渡す
    private elementRef: ElementRef
  ) {}

  ngOnInit() {
    var picker = new Pikaday({
      field: this.elementRef.nativeElement,
      format: 'YYYY/MM/DD',
      i18n: {
        previousMonth : '先月',
        nextMonth     : '来月',
        months        : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
        weekdays      : ['日曜日','月曜日','火曜日','水曜日','木曜日','金曜日','土曜日'],
        weekdaysShort : ['日','月','火','水','木','金','土']
      },
      onSelect: (date) => {
        // 何故かpikadayをbindしただけじゃ、formControlの値を変更することができない
        // onSelect時に、直接formControlへ選択した文字列を渡すようにする
        this.datePickerField.setValue(picker.toString());
      }
    });
  }

}

ちなみに、悩みとしては、angular cliでビルドしていますが、pikadayのcssなどが、htmlへinlineで追加されてしまい、他のcssに影響がでています。

上書きしてもいいですが、あとあと絶対面倒になるので、どうにか必要なページだけpikaday、pikadayのcssをロードできないか方法を探し中です。知っている人いましたら教えてください(><

追記 2016/12/26

必要なページだけ読み出す方法がわかりました。

まず、angular-cliに追加した情報を削除します。 ~/angular-hello-form-application/angular-cli.json

"app": [{ 
      "styles": [
        "styles.css",
        "../node_modules/pikaday/scss/pikaday.scss" // 削除
      ],
      "scripts": [
        "../node_modules/pikaday/pikaday.js", // 削除
        "../node_modules/moment/moment.js" // 削除
      ]
}]

んで、directiveをcomponentへ変更して、デコレータのパラメータを変更します。というか、Componentとして作り直したほうがいいかもしれません

~/src/app/date-picker/date-picker.component.ts

import { Component, ElementRef, Input, ViewEncapsulation } from '@angular/core';

// 宣言?
const Pikaday = require('../../../node_modules/pikaday/pikaday');
const PikadayStyle = require('../../../node_modules/pikaday/scss/pikaday.scss');

@Component({
  selector: '[appDatePicker]',
  template: '',
  styleUrls: [PikadayStyle],
  encapsulation: ViewEncapsulation.None
})
export class DatePickerComponent {

個別のページだけモジュールを使うって方法がわからなかったので、汚い英語ですが、stackoverflowに投稿して回答を待ちましたところ、「requireを使えばいけるよ」的な回答を頂いたので、試してみたらまじでうまくいきました(°ω°

stackoverflow.com

自分の実装はDirectiveで試していましたが、styleをあてたかったのと、Directiveのデコレータでstyleを指定する方法がわからなかったので、Componentへ変更しました。

また、Componentが吐き出すstyleは、cssセレクターに動的の属性が当てられます。しかし、pikadayのdomはComponentの属性が付与されないため、単純にComponentを用意するだけではstyleはあたりません。

なので、encapsulationの値に、ViewEncapsulation.Noneを渡してあげると、(よろしくないとは思いますが)属性を付与しなくなるので、pikadayのdomにstyleがあたるようになります。これでおkです。

これで必要なComponentだけstyleとmoduleをロードすることができるようになりました。githubの方も修正しています。

動的なフォーム

これが一番やりたかった。

ng-book2 にはのっていなかったので、できるか心配でしたが、色々ググって試してみたらできたのでメモしておきます。

$ ng generate component dynamic-form
installing component
  create src/app/dynamic-form/dynamic-form.component.css
  create src/app/dynamic-form/dynamic-form.component.html
  create src/app/dynamic-form/dynamic-form.component.spec.ts
  create src/app/dynamic-form/dynamic-form.component.ts

~/angular/hello-form/application/src/app/dynamic-form/dynamic-form.component.html

<h2>動的フォーム</h2>
<form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm)">
  <div class="form-group">
    <label>お名前は?</label>
    <input type="text" [formControl]="myForm.controls['yourName']">
  </div>
  <!-- この下のタグを渡せないと動作しなかった。group -> arrayにも名前を用意しないといけないらしい -->
  <div formArrayName="whatAnimationDoYouLike">
    <div *ngFor="let testDatetime of myForm.controls.whatAnimationDoYouLike.controls; let i=index">
      <div [formGroupName]="i">
        <div class="delete-button">
          <span>Address {{i + 1}}</span>
          <button type="button" *ngIf="myForm.controls.whatAnimationDoYouLike.controls.length > 1" (click)="removeGroup(i)">フォームから削除</button>
        </div>
        <div class="form-group">
          <label>アニメの名前は?(必須)</label>
          <input type="text"
            [formControl]="myForm.controls['whatAnimationDoYouLike'].controls[i].controls['answer']">
        </div>
        <div class="form-group">
          <label>好きな理由は?(任意)</label>
          <input type="text"
            [formControl]="myForm.controls['whatAnimationDoYouLike'].controls[i].controls['reason']">
        </div>
      </div>
    </div>
  </div>

  <div class="margin-20">
    <button type="button" (click)="addGroup()">フォームを追加</button>
  </div>
  <pre>{{myForm.value | json}}</pre>
  <div class="form-group">
    <button type="submit">送信</button>
  </div>
</form>

属性formArrayName="whatAnimationDoYouLike"の箇所。これも必須です。Cannot find control with unspecified name attributeのエラーが発生します。group -> arrayにも名前を用意しないといけないらしい。

下はComponentです。動的なフォームを組みたい場合、FormArrayが必要になりますので、importしてください。

~/angular/hello-form/application/src/app/dynamic-form/dynamic-form.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';

@Component({
  selector: 'app-dynamic-form',
  templateUrl: './dynamic-form.component.html',
  styleUrls: ['./dynamic-form.component.css']
})
export class DynamicFormComponent implements OnInit {
  myForm: FormGroup;

  constructor(
    private formBuilder: FormBuilder
  ) { }

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      'yourName': ['', Validators.required],
      'whatAnimationDoYouLike': this.formBuilder.array([
        this.initGroup()
      ])
    });
  }

  initGroup() {
    return this.formBuilder.group({
      'answer': ['', Validators.required],
      'reason': ['']
    });
  }

  addGroup() {
    const arrayControls = this.getFormArrayControls();
    arrayControls.push(this.initGroup());
  }

  removeGroup(i: number) {
    const arrayControls = this.getFormArrayControls();
    arrayControls.removeAt(i);
  }

  getFormArrayControls() {
    return <FormArray>this.myForm.controls['whatAnimationDoYouLike'];
  }

  onSubmit(form: any) {
    console.log(form);
  }
}

initGroup()で、複数のformControlをgroupingして値を返します。

addGroup()は、今現在のFormArrayを取得して、initGroup()から追加データをpush()して追加しています。

removeGroupはその逆です。現在のFormArrayを取得して、指定された要素番号でFormArrayを、removeAt(i)しています。i: numberはテンプレートからもらう情報です。

これでおkです。もしFormArrayの値をwatchしたいとかあれば、this.myForm.controls['whatAnimationDoYouLike'].controls[i].controls['answer']とかでvalueChangesを実行すればおkです。ながいねー。

まとめ(?)

1ヶ月前は「なんでこのフレームワークを選んだんだ(´;ω;`)ブワッ」と泣きそうになりましたが、理解してくると楽しくなってきますねー。

近くに教えてくれる人がいると手っ取り早いですが、そうじゃない場合はやっぱり本がいいのではと思います。

引き続き、Angular2を学んで、使えるようになったらまた色々と記事を上げていきたいです。