Angular(とAngular Material)を使った際にはまった事

# 概要
Angular7でWebアプリケーションを開発した際に色々とはまったことが数多く出てきたので~~戒めの~~今後のために
記事にまとめておくことにします。
今回の開発ではAngular Materialも使用しましたのでそちらで発生した問題についてもまとめて記載しておきたいと思います。

# はまった事と解決方法一覧

## ネットワークの外部に出られない

### 1. 詳細
今回の開発で一番苦労したと思うところがこれでした。
`angular-in-memory-web-api`を使用している場合、そのままだと外部のネットワークに出ることができないということを知らずに数日エラーと戦っていました。
[AngularでHTTPアクセスしようとしたら「statusText: Not Found」](https://www.virtual-surfer.com/entry/2018/06/06/190000)

下記のようにURLアクセスと共存させる方法もありますが、基本的には開発中に使用するのであれば、jsonファイルを作成してそのファイルに対してHttpClientでアクセスした方が内部のテスト環境と外部の環境の切り替えがしやすいと思います。
[Angular2 angular-in-memory-web-api で モック と urlアクセスを共存させる方法](https://qiita.com/endk/items/c53b4651d81dcebf1bf1)

内部環境では一旦下記のようにやっていました。本来は環境の切り替えのみで内部と外部に対する接続を切り替えられるようにしなければならないのですが、調査時間がなかったためできていません。
調べ次第ここに追記します。

```typescript:
  constructor(
    private http: HttpClient,
  ) { }

// ... 省略

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type': 'json',
  })
};
return this.http.get<ResponseClass>('jsonfile.json', httpOptions);
```

# ページ遷移の場合でページを読み込んだ後に白飛びしないようにする
AppRoutingModuleを使用している場合に`href=xxx`ではなく`navigateByUrl('/xxx_link')`を使用する
AppRoutingModuleを使用する場合で同一のサイト内を移動する際は`navigateByUrl`を使用した方が良いと思われる

## 日付をフォーマットして表示したい
こちらの記事がわかりやすかったので参考にさせていただきました。
[Angular 日付のフォーマット](https://qiita.com/shirokuman/items/13d5bd132ae3b35d7ca5)

DatePipeについての説明とフォーマットオプション一覧
[Angular:DatePipe](https://angular.jp/api/common/DatePipe)

### 1. htmlでフォーマットを指定する

htmlでフォーマットする場合は`app.module.ts`等に追加する必要はなく、`Date`型の変数をhtmlで参照して
下記のようにフォーマットを指定すれば良い。

```html:module.component.html
<div>{{testDate|date:"yyyy年MM月dd日 HH時mm分"}}</div>
```

### 2. TypeScriptでフォーマットを指定する
typeScriptで指定する場合は二つの方法がある

### 2.1. `app.module.ts`の`providers`にDatePipeを追加後コンポーネントでimportして使用する
こちらの場合は`app.module.ts`と使用するコンポーネント二つ修正する必要がある

```typescript:app.module.ts
import { DatePipe } from '@angular/common';

@NgModule({
  // ... 省略
  providers: [DatePipe],
  // ... 省略
})
```

```typescript:module.component.ts
import { DatePipe } from '@angular/common';

@Component({
  selector: 'module-component',
  templateUrl: './module-component.html',
  styleUrls: ['./module-component.scss']
})
export class ModuleComponent implements OnInit {
  date1 = new Date();
  date2: string;

  constructor(
    private datepipe: DatePipe,
  ) { }

  ngOnInit(): void {
    this.date2 = this.datepipe.transform(this.date1, 'yyyy年M月d日 HH時mm分ss秒');
  }
}

```

### 2.2. 各コンポーネントの`@Component`に`providers`を追加してDatePipeを含める
こちらの場合は使用するコンポーネントのみ修正する必要がある

```typescript:module.component.ts
import { DatePipe } from '@angular/common';

@Component({
  selector: 'module-component',
  templateUrl: './module-component.html',
  styleUrls: ['./module-component.scss'],
  providers: [DatePipe]
})
export class ModuleComponent implements OnInit {
  date1 = new Date();
  date2: string;

  constructor(
    private datepipe: DatePipe,
  ) { }

  ngOnInit(): void {
    this.date2 = this.datepipe.transform(this.date1, 'yyyy年M月d日 HH時mm分ss秒');
  }
}

```

### 2.3 結局どちらにDatePipeのプロバイダーを設定すれば良い？

頻繁にロードされるコンポーネントの場合は`app.module.ts`(ルートモジュール)に追加した方が良いが、多くなるとロードに時間がかかる
遅延ロードが有効な場面かどうかを確認して、ルートモジュールもしくは各コンポーネントに追加するかを選択する

[Angular:プロバイダー](https://angular.jp/guide/providers)

## Angularで作成したWebサイトをGAEに乗せたい
こちら参考にさせていただきました。
[Angular 製のアプリケーションを Google App Engine にデプロイする](https://qiita.com/nirasan/items/68ccb9808f3739a9c999)

今回作成したWebアプリケーションはビルド時に`ng build --output-path=./server/static`を指定し、かつNode.jsを使用したため、`app.yaml`は以下のようになりました。

```yaml:./server/app.yaml
runtime: nodejs8

handlers:
- url: /(.*\.(css|gif|png|jpg|ico|js|html|json))
  static_files: static/\1
  upload: static/(.*\.(css|gif|png|jpg|ico|js|html|json))
- url: /(.*)
  static_files: static/index.html
  upload: static/index.html

```

## Mat-XXに対してngModelが設定できない

`app.module.ts`に対して`FormsModule`をインポートする

```typescript:app.module.ts
// インポートに追加
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    // ... 省略
    FormsModule,
    // ... 省略
  ],
})
export class AppModule {
  // ... 省略
}
```

[Angular 4 Error: Can’t Bind to ngModel since it isn’t a known property of select](https://www.encodedna.com/angular/angular-error-cant-bind-to-ngmodel-since-it-isn-t-a-known-property-of-select.htm)

## mat-XXでアニメーションが発生しない
`app.module.ts`に対して`BrowserAnimationsModule`もしくは`NoopAnimationsModule`をインポートする
[Angular:Angularアニメーション・イントロダクション](https://angular.jp/guide/animations)
[Angular:Angular Material Getting started](https://material.angular.io/guide/getting-started)

```typescript:app.module.ts
// インポートに追加
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
// アニメーションを無効化したい場合はこちらをインポート
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  imports: [
    // どちらか一方をimportsに含める
    BrowserAnimationsModule,
    NoopAnimationsModule,
    // ... 省略
  ],
})
export class AppModule {
  // ... 省略
}
```

[Ripple effect is not displaying in button in angular app - Angular-Material](https://stackoverflow.com/questions/53712068/ripple-effect-is-not-displaying-in-button-in-angular-app-angular-material)

## mat-selectに初期値未定の項目を作りたい

`[value]`未定の`<mat-option>`を作る

```html:module.component.html
<mat-form-field floatLabel="never">
  <mat-label>内容を選択</mat-label>
  <mat-select name="selectItem" [(ngModel)]="selectItem">
    <mat-option>未選択</mat-option>
    <mat-option *ngFor="let slt of selectModels" [value]="slt.id">
      {{slt.description}}
    </mat-option>
  </mat-select>
</mat-form-field>
```

上のように設定した場合、`mat-select`を選択すると「未選択」項目が表示される

|![mat-select_default_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340824/12e1ff1b-64bb-4549-f85d-bd677703368c.png)|
|:-:|

「未選択」項目を選択した場合、項目が表示されずに「内容を選択」に戻る

|![mat-select_default_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340824/349d5b8d-714b-d90c-8296-893ba1e046d2.png)|
|:-:|

## mat-XXの項目を当てた時のplaceholderが上に移動するのを止めたい
`mat-label`を使用している場合は`mat-form-field`タグ内に`floatLabel="never"`を追加する
もしくは`mat-label`を使用せずに`placeholder`を使用する

```html:module.component.html
<!-- mat-selectの場合はfloatLabel="never" -->
<mat-form-field floatLabel="never">
  <mat-label>内容を選択</mat-label>
  <mat-select name="selectItem" [(ngModel)]="selectItem">
    <mat-option>未選択</mat-option>
    <mat-option *ngFor="let slt of selectModels" [value]="slt.id">
      {{slt.description}}
    </mat-option>
  </mat-select>
</mat-form-field>

<!-- inputタグはこちらも使用可能 -->
<div><input matInput placeholder="内容を入力"></div>
```

## DatePickerの値をDateもしくはStringで取りたい・入れたい

こちら参考にさせていただきました。
[JavaScript の Date は罠が多すぎる](https://qiita.com/labocho/items/5fbaa0491b67221419b4)

```typescript:app.module.ts
// インポートに追加
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatInputModule } from '@angular/material/input';
import { MatNativeDateModule } from '@angular/material/core';

@NgModule({
  imports: [
    // ... 省略
    MatDatepickerModule,
    MatInputModule,
    MatNativeDateModule,
    // ... 省略
  ],
})
export class AppModule {
  // ... 省略
}
```

```html:module.component.html
<div>{{selectDate2}}</div>
<mat-form-field floatLabel="never">
  <input matInput name="picker1" [matDatepicker]="picker1" placeholder="日付を選択" [(ngModel)]="selectDate"
    (dateChange)="onDatePickerChange($event)">
  <mat-datepicker-toggle matSuffix [for]="picker1"></mat-datepicker-toggle>
  <mat-datepicker #picker1></mat-datepicker>
</mat-form-field>
```

```typescript:module.component.ts
  ngOnInit(): void {
    // 文字列を日付に変換
    const date = '20181231';
    const dateStr = date.substr(0, 4) + '/' + date.substr(4, 2) + '/' + date.substr(6, 2);
    const time = ' 00:00:00';
    this.selectDate = new Date(dateStr + time);
  }

  // 設定された日付を取得して文字列に変換
  public onDatePickerChange(event: any): void {
    // 取得したプロパティをそのまま使用する場合、未設定時にnullになるため一度インスタンスを生成する
    const dateStr = new Date(this.selectDate);
    const yyyy = ('0000' + dateStr.getFullYear()).slice(-4);
    // 月は0〜11で取得されるので+1 足す
    const mm = ('00' + (dateStr.getMonth() + 1)).slice(-2);
    const dd = ('00' + dateStr.getDate()).slice(-2);

    this.selectDate2 = yyyy + mm + dd;
  }
```

## DatePickerに対して日付ピッカーを開いた後にValidationを入れたい

製造時、DatePickerを使用した際に下記のような動作を入れたくなった

* 日付の直接入力は不可
* アイコンクリックして日付入力(または未入力でキャンセル)時にValidationを実行

|![DatePicker_Validation_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340824/b569d052-6708-1da7-6dae-80f9b3772063.png)|
|:-:|

### 回避方法
pickerが開く時はopened、閉じる時はclosedイベントが発生するのでそちらのイベントから手動でフラグを作成した
picker1に対して追加でmarkAsTouchedを発行しているのは黄枠の部分もエラー表示にしたいため
![DatePicker_Validation_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340824/e90b4202-1f10-ff61-a9f0-62585545ea15.png)


```html:module.component.html
<form #testForm="ngForm" appIdentityRevealed>
// ... 省略
  <mat-form-field floatLabel="never" hideRequiredMarker>
    <input matInput name="picker1" [matDatepicker]="picker1" placeholder="日付を選択" [(ngModel)]="selectDate"
      (dateChange)="onDatePickerChange($event)" readonly required #selectDate1="ngModel">
    <mat-datepicker-toggle matSuffix [for]="picker1"></mat-datepicker-toggle>
    <mat-datepicker #picker1 name="date1" (opened)="streamOpened()" (closed)="streamClosed(testForm)"></mat-datepicker>
    <mat-hint class="alert alert-danger" *ngIf="pickerClosed && selectDate1?.errors?.required" class="icon-warn">
      日付は必須入力です。
    </mat-hint>
  </mat-form-field>
// ... 省略
</form>
```

```typescript:module.component.ts
import { NgForm } from '@angular/forms';
// ... 省略
export class ModuleComponent implements OnInit {
  pickerClosed = false;
// ... 省略
  public streamOpened() {
    this.pickerClosed = false;
  }

  public streamClosed(form: NgForm) {
    this.pickerClosed = true;
    form.controls.picker1.markAsTouched({ onlySelf: true });
  }
}
```

## Enterキーを押した時にFormの送信を防ぐ

テキストエリアのみEnterキーを有効(改行可)にして他のFormではEnterキーでのForm送信を無効化したい
[Angular 2 prevent enter from submitting in template-driven form](https://stackoverflow.com/questions/40653758/angular-2-prevent-enter-from-submitting-in-template-driven-form)

```html:module.component.html
<form #testForm="ngForm" appIdentityRevealed (keydown.enter)="handleEnterKeyPress($event)">
// ... 省略
</form>
```

```typescript:module.component.ts
  handleEnterKeyPress(event) {
    const tagName = event.target.tagName.toLowerCase();
    if (tagName !== 'textarea') {
      return false;
    }
  }
```

## DatePickerのロケーションを変更したい

普通にDatePickerを作るとロケーションが(確か)`en-US`なので`ja-JP`に変更したい

### パッケージのインストール

下記2つのパッケージをインストール

```shell:
npm install moment
npm install @angular/material-moment-adapter
```

### 1. `app.module.ts`に追加しない場合(CustomDateAdapterを作成しない場合)

```typescript:module.component.ts
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MAT_MOMENT_DATE_FORMATS, MomentDateAdapter } from '@angular/material-moment-adapter';

@Component({
  // ... 省略
  providers: [
    { provide: MAT_DATE_LOCALE, useValue: 'ja-JP' },
    { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
    { provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
  ],
})
```

DatePickerを使用している各自コンポーネントに追加した場合は下記のような表示になる
![DatePicker_change_location_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/340824/f6013497-1845-21b6-5603-36f601126dcd.png)

### 2. `app.module.ts`に追加する場合(CustomDateAdapterを作成する場合)

上の方法だとDatePickerを開いた場合の年月表記が逆になっていたりするので、余裕があるならCustomDateAdapterを作成した方が良い
[date inputの救世主!? Angular Materialのdatepickerを使ってみた](https://qiita.com/pittanko_pta/items/4766a1dfb9a682b76181)

## ファイル選択のボタンにデフォルトのボタンではなくAngular Materialのボタンを使用したい
下記の記事を参考にさせていただきました。

[Angular Materialでファイル選択ボタン](https://qiita.com/daikiojm/items/d744d63300915b2d8c4b)

## Mat-Chipの縦の高さを変更可能にしたい

mat-chip内に自動生成される`mat-standard-chip`で高さが指定されている模様。
mat-chipに対して以下のcssを指定することで高さが変動できるようになる。

```scss:style.scss
mat-chip {
  // mat-standard-chipの高さ固定を上書き
  height: auto;
}
```

# 最後に

実装に迷ったところが出てきたら随時追記します。

# 参考

[AngularでHTTPアクセスしようとしたら「statusText: Not Found」](https://www.virtual-surfer.com/entry/2018/06/06/190000)
[Angular2 angular-in-memory-web-api で モック と urlアクセスを共存させる方法](https://qiita.com/endk/items/c53b4651d81dcebf1bf1)
[Angular 日付のフォーマット](https://qiita.com/shirokuman/items/13d5bd132ae3b35d7ca5)
[Angular:DatePipe](https://angular.jp/api/common/DatePipe)
[Angular:プロバイダー](https://angular.jp/guide/providers)
[Angular 製のアプリケーションを Google App Engine にデプロイする](https://qiita.com/nirasan/items/68ccb9808f3739a9c999)
[Angular 4 Error: Can’t Bind to ngModel since it isn’t a known property of select](https://www.encodedna.com/angular/angular-error-cant-bind-to-ngmodel-since-it-isn-t-a-known-property-of-select.htm)
[Angular:Angularアニメーション・イントロダクション](https://angular.jp/guide/animations)
[Angular:Angular Material Getting started](https://material.angular.io/guide/getting-started)
[Ripple effect is not displaying in button in angular app - Angular-Material](https://stackoverflow.com/questions/53712068/ripple-effect-is-not-displaying-in-button-in-angular-app-angular-material)
[JavaScript の Date は罠が多すぎる](https://qiita.com/labocho/items/5fbaa0491b67221419b4)
[Angular 2 prevent enter from submitting in template-driven form](https://stackoverflow.com/questions/40653758/angular-2-prevent-enter-from-submitting-in-template-driven-form)
[date inputの救世主!? Angular Materialのdatepickerを使ってみた](https://qiita.com/pittanko_pta/items/4766a1dfb9a682b76181)
[Angular Materialでファイル選択ボタン](https://qiita.com/daikiojm/items/d744d63300915b2d8c4b)