水無瀬のプログラミング日記

プログラムの勉強した時のメモ

Angularのリアクティブフォームまとめ

はじめに

ReactiveFormこそAngularの花的な話をどこかで見た気がしたから軽くまとめる。

リアクティブフォームとは

リアクティブフォーム は、時間とともに入力値が変わるフォームを扱うためのモデル駆動なアプローチを提供します。
このガイドでは、シンプルなフォームコントロールの作成と更新から、グループ内の複数コントロールの使用、フォームのバリデーション、高度なフォームの実装の方法を説明します。

Angular日本語ドキュメントによると上記の通りらしい。
使った感じ、入力フォームとか動的にNG出したいときとかに使えそう。
Angular日本語ドキュメントをなぞりながら簡単にまとめる。

前提条件

Angular使えてCLIも使える前提

準備

とりあえずCLIでプロジェクト生成する。

$ ng new reactive-form-test

生成されたapp.module.tsに下記を追加する。
やることはReactiveFoms用にmoduleをインポートするだけ。

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    // other imports ...
    ReactiveFormsModule
  ],
})
export class AppModule { }

使ってみる

生成されてるapp.component.htmlapp.component.tsを下記の通りに変える。

import { Component } from '@angular/core';
import { FormControl,Validators } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  public control = new FormControl('', [
    Validators.required
  ]);
}
入力欄:<input type="text" [formControl]="control" required>
<div *ngIf="control.invalid && (control.dirty || control.touched)">
  <span *ngIf="control.hasError('required')">必須です。</span>
</div>

やってること説明

とりあえず上記サンプルはここから動きを確認できる。
やっていることはフォームの必須チェック。
一旦テキストボックスにカーソル入れた後、フォーカスアウトをすると必須のメッセージが表示される。
htmlでdivを入れているのは、フォーカスが当たるまで、メッセージを表示しないようにするため。

<div *ngIf="control.invalid && (control.dirty || control.touched)">

これが無いと、初期表示時未入力 = バリデーションNGということでいきなりメッセージが出てくる。
大抵の場合、それは本意じゃ無いと思うから上を入れておくのが無難だと思う。

おまけ:ngModelで代用得してみる。

public requiredNg = false;
public text = '';

public valid(target: string) {
  this.requiredNg = target.length === 0;
}
入力欄:<input type="text" [(ngModel)]="text" (blur)="valid($event.target.value)"><br>
<span *ngIf="requiredNg">必須です。</span>

やっていることとしては、テキストボックスの内容を双方向でバインドして、フォーカスアウト時に長さをチェックするということ。
上の様にすればできなくは無いけど、リアクティブフォームを使ったときに比べるとわかりにくいし冗長な気がする。

ちょっと使いやすくする

リアクティブフォームは使いやすい気がするけど、個人的に使いやすいようにちょっと変える。
理由としてはチェックしたいフォームが複数になるとhtmlが見にくくなっていくから。
おまけとして、formControlをグループにまとめる。
とりあえずエラーメッセージをコンポーネントを作るところから始める。
コンポーネント作成はng g component ng-messageでOK。

<div>
  ユーザID:<input type="text" formControlName="userID" required>
  <div *ngIf="userID.invalid && (userID.dirty || userID.touched)">
    <div *ngIf="userID.hasError('required')">
      必須です。
    </div>
    <div *ngIf="userID.hasError('maxlength')">
      最大5文字です。
    </div>
  </div>
</div>
<div>
  パスワード:<input type="text" formControlName="password" required>
    <div *ngIf="password.invalid && (password.dirty || password.touched)">
    <div *ngIf="password.hasError('required')">
      必須です。
    </div>
    <div *ngIf="password.hasError('minlength')">
      最低8文字です。
    </div>
  </div>
</div>
<div>
  e-mail:<input type="text" formControlName="email" required>
    <div *ngIf="email.invalid && (email.dirty || email.touched)">
    <div *ngIf="email.hasError('email')">
      e-mailアドレスのフォーマットにしてください。(xxx@hogehoge.com)
    </div>
  </div>
</div>

個人的に見やすくした結果

結果から書くと下記の様にした。

app.component.html

<form [formGroup]="formGroup">
  <div>
    <div>
      ユーザID:<input type="text" formControlName="userID" required>
      <app-ng-message [form]="userID"></app-ng-message>
    </div>
    <div>
      パスワード:<input type="text" formControlName="password" required>
      <app-ng-message [form]="userID"></app-ng-message>
    </div>
    <div>
      e-mail:<input type="text" formControlName="email" required>
      <app-ng-message [form]="email"></app-ng-message>
    </div>
  </div>
</form>

app.component.ts

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

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
  public formGroup = new FormGroup({
    userID: new FormControl('', [
      Validators.required, //必須
      Validators.maxLength(5) // 最大5文字
    ]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(8) // 最低8文字
    ]),
    email: new FormControl('', [
      Validators.email, // e-mailフォーマットチェック
      this.duplicateEmailValidator() // 任意のバリデーション(今回は重複チェック)
    ])
  });

  constructor() { }

  ngOnInit() {
  }

  private duplicateEmailValidator(): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} | null => {
      const ngList = [
        'hoge@example.com',
        'huga@test.co.jp'
      ];
      const duplicate = ngList.includes(control.value);
      return duplicate ? {'duplicateEmail': {value: control.value}} : null;
    };
  }

  public get userID() {
    return this.formGroup.get('userID');
  }
  public get password() {
    return this.formGroup.get('password');
  }
  public get email() {
    return this.formGroup.get('email');
  }
}

ng-message.component.html

<div *ngIf="form.invalid && (form.dirty || form.touched)">
  <span *ngIf="form.hasError('required')">
    必須です。
  </span>
  <span *ngIf="form.hasError('maxlength')">
    最大5文字です。
  </span>
  <span *ngIf="form.hasError('minlength')">
    最低8文字です。
  </span>
  <span *ngIf="form.hasError('email')">
    e-mailアドレスのフォーマットにしてください。(xxx@hogehoge.com)
  </span>
  <span *ngIf="form.hasError('duplicateEmail')">
    入力頂いたアドレスは重複しています。
  </span>
</div>

ng-message.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-ng-message',
  templateUrl: './ng-message.component.html',
  styleUrls: ['./ng-message.component.css']
})
export class NgMessageComponent implements OnInit {
  @Input() form: FormControl;

  constructor() { }

  ngOnInit() {
  }
}

app.component.tsFormGroupFormCotntrolをまとめている。
まとめたControlは個別にバリデーションを設定できるので、一応全部バラバラにしてみた。
emailのところでは自作のバリデーションを追加している。
Angular日本語ドキュメントによると(英語のドキュメントも同じだけど)Heroの名前にbobが使えないいじめ仕様だから変えてみた。
+そもそも公式の例だとValidators.pattern(/bob/i)って書けば良いはずなので、もうちょっと有り得そうなパターンにしたかったから。
ちなみにapp.co,ponent.tsにgetterを作っているのは、
FormGroupから特定のデータを取るにはFormGroup.get('フォーム名')としなきゃ行けなくて、html他で参照するときに使いづらいから。
(+公式に書いてあるから。)

エラーが有った場合には作成したng-message.componentにFormControlをそのまま投げている。
ng-message.componentで受け取った値に応じたメッセージを表示するという仕組み。
これでメッセージ追加する時はここに足せばいいし、呼び出し側でも長い条件書かなくていいから楽になったのではないかと。

まとめ

リアクティブフォームについて簡単にまとめて簡単に使ってみた。
今までは双方向バインドでええやんって思ってたけど、これは便利かもしれない。
今後は積極的に使っていきたい所存。

参考サイト