Angular フォーム (2)：バリデーション

# バリデーション
フォーム・バリデーションを使う設定は次の通りです。

##### 1. app.module.ts で、FormsModule をインポートする
* [フォームモジュールを使うには](https://qiita.com/thankkingdom/items/f8a670be6302471a198e#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E4%BD%BF%E3%81%86%E3%81%AB%E3%81%AF)

##### 2. コンポーネントTSファイルの Validators の importを追加

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

##### 3. コンポーネントTSファイルの ngOnInit() にてフォームコントロール毎にバリデーション・ルールを設定する。

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

@Component({ 省略 })
export class SampleComponent implements OnInit {

  title!:string;
  message!:string;
  sampleForm!: FormGroup;

  constructor(
    private formBuilder:FormBuilder
  ) { }

  ngOnInit(): void {
    this.title = 'Hello-app';
    this.message = 'FormControlを使う';

    this.sampleForm = this.formBuilder.group({
      name: ['', [Validators.required, Validators.minLength(1)]],
      email: ['', [Validators.required, Validators.email]],
      age: [0, [Validators.min(1), Validators.max(150)]]
    });
  }

  get name() { return this.sampleForm.get('name'); }
  get email() { return this.sampleForm.get('email'); }
  get age() { return this.sampleForm.get('age'); }
}
```

##### 4. コンポネントHTML

```html
<div id="body">
  <h1>{{title}}</h1>
  <p>メッセージ：{{message}}</p>
  <form [formGroup]="sampleForm">
    <table>
      <tr>
        <th>名前：</th>
        <td>
          <input type="text" formControlName="name">
          <ng-container *ngIf="name?.invalid && (name?.dirty || name?.touched)">
            <div *ngIf="name?.errors?.['required'] === true" class="error">名前は必須入力です。</div>
          </ng-container>
        </td>
      </tr>
      <tr>
        <th>Eメール：</th>
        <td>
          <input type="text" formControlName="email">
          <ng-container *ngIf="email?.invalid && (email?.dirty || email?.touched)">
            <div *ngIf="email?.errors?.required" class="error">Eメールは必須入力です。</div>
            <div *ngIf="email?.errors?.email" class="error">Eメールのフォーマット不正です。</div>
          </ng-container>
        </td>
      </tr>
      <tr>
        <th>年齢：</th>
        <td>
          <input type="number" formControlName="age">
          <ng-container *ngIf="age?.invalid && (age?.dirty || age?.touched)">
            <div *ngIf="age?.errors?.min" class="error">年齢は1才以上でなければなりません。</div>
            <div *ngIf="age?.errors?.max" class="error">年齢は150才以下でなければなりません。</div>
          </ng-container>
        </td>
      </tr>
    </table>
    <input type="button" value="クリック" [disabled]="sampleForm.invalid">
  </form>
</div>
```

* クリックボタンの ```[disabled]="sampleForm.invalid"``` は、エラーがなければ、使用できるようにするための設定です。


###### 注意点

サンプル通りに記述しているはずなのに、チェック結果を確認する ngIfでコンパイルエラーが発生しました。解決までに時間がかかったのでメモを残しておく。

```typescript
Error: src/app/sample/sample.component.html:10:59 - error TS2531: Object is possibly 'null'.

10           <div *ngIf="name.invalid && (name.dirty || name.touched)">
                                                             ~~~~~~~
```

「name.touch」を「name?.touch」という記述に変えるとエラーが解消されました。この記述だとnullの場合にエラーを回避してくれるようです。

プロパティ宣言で、初期化しないとエディタでエラーになっていました。VS Code に出てくるQuick Fixを使うとエラーが消えました。

* 「Add 'undefined' type to property 'sampleForm'」を選択した場合

> sampleForm: FormGroup | undefined;

プロパティ宣言ではエラーは出ないが、参照箇所で undefined の可能性があるのでエラーとなりました。

* 「Add definite assignment assertion to property 'sampleForm:FormGroup'」を選択した場合

> sampleForm!:FormGroup

エラーがなくなったので、この定義がベストのようです。

サンプルや書籍とは違い実際はエラーとなる現象は、使っているAngularかNode.jsのバージョンが新しために、TypeScriptのチェックが厳しくなったからなのかも知れないです。

```shell
$ ng version
Your global Angular CLI version (13.1.0) is greater than your local version (12.1.4). The local Angular CLI version is used.

To disable this warning use "ng config -g cli.warnings.versionMismatch false".

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/


Angular CLI: 12.1.4
Node: 16.13.1 (Unsupported)
Package Manager: npm 8.1.2
OS: win32 x64

Angular: 12.1.5
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1201.4
@angular-devkit/build-angular   12.1.4
@angular-devkit/core            12.1.4
@angular-devkit/schematics      12.1.4
@angular/cli                    12.1.4
@schematics/angular             12.1.4
rxjs                            6.6.7
typescript                      4.3.5

Warning: The current version of Node (16.13.1) is not supported by Angular.

```

## 独自バリデーション

```typescript
メソッド名 (c: FormControl) {

  //問題がない場合
  return null;

  // 問題がある場合
  //return { 名前: { valid: boolean値, ... } };
  return { 名前: boolean値 };
}
```

サンプルとして、name に日本語チェックを追加しました。以下は抜粋です。

##### コンポーネントTSファイル
```typescript
export class SampleComponent implements OnInit {

  ngOnInit(): void {

    this.sampleForm = this.formBuilder.group({
      name: ['', [Validators.required, this.isJapanese]],
      email: ['', [Validators.required, Validators.email]],
      age: [0, [Validators.min(1), Validators.max(150)]]
    });
  }

  isJapanese(c: FormControl) {
    console.log(JSON.stringify(c.value));
    if (c.value == null || c.value == "") {
      return null;
    }
    if (c.value.match(/^[\u30a0-\u30ff\u3040-\u309f\u3005-\u3006\u30e0-\u9fcf]+$/)) {
      return null;
    }
    return { isJapanese: false };
  }

}
```

#### コンポーネントHTMLファイル
```html
      <tr>
        <th>名前：</th>
        <td>
          <input type="text" formControlName="name">
          <ng-container *ngIf="name?.invalid && (name?.dirty || name?.touched)">
            <div *ngIf="name?.errors?.['required'] === true" class="error">名前は必須入力です。</div>
            <div *ngIf="name?.errors?.isJapanese === false" class="error">日本語で入力して下さい。</div>
          </ng-container>
        </td>
      </tr>
```

# 関連記事
* [Angular はじめに覚えること](https://qiita.com/thankkingdom/items/ca8acd2fd75c7778443d)
* [Angular フォーム (1)：基礎](https://qiita.com/thankkingdom/items/f8a670be6302471a198e)
* [Angular 次に覚えること](https://qiita.com/thankkingdom/items/13eb62a8d35a65c819dd)


# 参考資料
* 『Angular超入門』 著者：掌田 津邪乃
* https://angular.jp/start
* https://angular.jp/guide/form-validation
* https://hepokon365.hatenablog.com/entry/2020/08/23/220835
* [https://itblog.verdy-it.xyz/ypescript型宣言字のクエスチョンマークとビックリマ/](https://itblog.verdy-it.xyz/typescript%E5%9E%8B%E5%AE%A3%E8%A8%80%E5%AD%97%E3%81%AE%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%81%E3%83%A7%E3%83%B3%E3%83%9E%E3%83%BC%E3%82%AF%E3%81%A8%E3%83%93%E3%83%83%E3%82%AF%E3%83%AA%E3%83%9E/)
* [Angularの便利タグng-container, ng-content, ng-template](https://qiita.com/shibukawa/items/c8c7fd22c1054348db3a)
* [javascript 日本語チェックです](https://qiita.com/graminume/items/2ac8dd9c32277fa9da64)
* [JavaScriptでひらがな・カタカタ・漢字をチェックする方法まとめ](https://pisuke-code.com/js-check-hira-kana-kanzi/)