Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順（2）

## 概要
Angular7 で俺式 MEAN スタックを作るための備忘録。
今回は「クライアント側のクラサバ通信対応」を行う。

### 前提
2019年1月1日時点の情報です。また、以下の環境になっている前提です。

- Angular CLI： 7.0.6
- Node.js： 10.15.0
- npm： 6.4.1

また、「[Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順（1）](https://qiita.com/tomy0610/items/83c2e2c3d5163942db55)」が完了していること。

## クラサバ共用部の作成

クライアントとサーバでデータ形式を一致させるために、インターフェース統一用の定義ファイルを格納する領域を作る。ついでに共用する定数などがあれば、それもこちらに用意することにする。

### ディレクトリを作成

以下の通りに、新規ディレクトリを src/app 配下に作成しておく

```bash
［ルートディレクトリ］
  :
  ├─ common # 新規作成
  │   ├─ apis
  │   │  └─ sample 
  │   │      └─ sample.api.ts
  │   ├─ entities
  │   │  └─ sample 
  :   │      └─ sample.entity.ts
  :   └─ tsconfig.json
```
### tsconfig ファイル作成
以下のとおりに中身を作成する

```json-doc:common/tsconfig.json
{
  "extends": "../tsconfig"
}

```

### インターフェースを作成

独自クラスを以下の通りに作成

```ts:common/entities/sample/sample.entity.ts
/** サンプル クラス */
export class Sample {
  /** ID */
  id: number;
  /** 氏名 */
  name: string;
  /** 年齢 */
  age: number;
}

```

API インターフェースを以下の通りに作成

```ts:common/apis/sample/sample.api.ts
import { Sample } from '../../entities/sample/sample.entity';

export const SAMPLE_API_PATH = '/api/sample';

/** サンプル パスパラメータ */
export interface ISamplePathParams {
  /** ID */
  id: number;
}

/** サンプル リクエスト */
export interface ISampleRequest {
  /** 氏名（サンプル） */
  name: string;
  /** 年齢（サンプル） */
  age: number;
}

/** サンプル レスポンス */
export interface ISampleResponse {
  users: Sample[];
}

```

## クライアント側を作成

サーバ API を呼び出すサービスを作っていく。

### ディレクトリを作成

以下の通りに、新規ディレクトリを src/app 配下に作成しておく

```bash
［ルートディレクトリ］
  ├─ browser
  │   └─ app # 既存
  :      :
  :      └─ services # 新規作成： サービス用
             └─ sample # 新規作成： サンプル個別サービス用
```

### 既存の tsconfig ファイル修正

browser/tsconfig.app.json の中身を以下の通りにする。

common ディレクトリの各ファイルを import 時に、`../` の多用を防ぐためにエイリアスを設定しておく。

```json-doc:browser/tsconfig.app.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./", // 追加
    "outDir": "../out-tsc/app",
    "types": [],
    // 追加
    "paths": {
      "common/*": ["../common/*"]
    },
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ]
}

```

### サービスクラスのファイルを生成

```bash
# 移動
cd browser/app/services

# HTTP 基底サービス生成
ng generate s http

# さらに移動
cd sample

# 画面用の専用サービス生成
ng generate s sample
```

終わったら、 `cd ../../../..` で元のディレクトリ位置に戻っておく。

### HTTP 基底サービス作成

HTTP 通信を実際に担う部分を、以下のとおりに作成。

```ts:browser/app/services/http.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams, HttpResponse, HttpErrorResponse } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class HttpService {

  constructor(protected http: HttpClient) { }

  public async get<Req, Res>(apiPath: string, request?: Req): Promise<HttpResponse<Res>> {
    return await this.send<Req, Res>('GET', apiPath, request);
  }

  public async post<Req, Res>(apiPath: string, request?: Req): Promise<HttpResponse<Res>> {
    return await this.send<Req, Res>('POST', apiPath, request);
  }

  public async put<Req, Res>(apiPath: string, request?: Req): Promise<HttpResponse<Res>> {
    return await this.send<Req, Res>('PUT', apiPath, request);
  }

  public async delete<Req, Res>(apiPath: string, request?: Req): Promise<HttpResponse<Res>> {
    return await this.send<Req, Res>('DELETE', apiPath, request);
  }

  /** HTTP通信 実行 */
  private async send<Req, Res>(method: 'GET'|'POST'|'PUT'|'DELETE', path: string, request?: Req): Promise<HttpResponse<Res>> {
    try {
      const url = window.location.origin + path;
      console.log('[HttpService - url] ', url);

      // GET, PUT の場合 クエリパラメータ生成 (exppress の query にセットされる)
      let httpParams: HttpParams = new HttpParams();
      if ((method === 'GET' || method === 'DELETE') && request) {
        for (const requestKey of Object.keys(request)) {
          if (request[requestKey]) {
            httpParams = httpParams.append(requestKey, request[requestKey]);
          }
        }
      }

      const response = await this.http.request<Res>(method, url, {
        headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
        responseType: 'json',
        params: httpParams,
        body: request
      }).toPromise();
      console.log('[HttpService - response] ', response);

      return { ok: true, status: 200, body: response } as HttpResponse<Res>;
    } catch (error) {
      console.log('[HttpService - error] ', error);

      if (error instanceof HttpErrorResponse) {
        return { ok: false, status: error.status, body: undefined } as HttpResponse<Res>;
      } else {
        return { ok: false, body: undefined } as HttpResponse<Res>;
      }
    }

  }

}

```

### サンプルサービス作成

```ts:browser/app/services/sample/sample.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { SAMPLE_API_PATH, ISampleRequest, ISampleResponse } from 'common/apis/sample/sample.api';
import { Sample } from 'common/entities/sample/sample.entity';
import { HttpService } from '../http.service';

@Injectable({
  providedIn: 'root'
})
export class SampleService extends HttpService {

  constructor(protected http: HttpClient) { super(http); }

  public async testGet(request: ISampleRequest, id?: number): Promise<{result: boolean, users: Sample[]}> {
    let apiPath = SAMPLE_API_PATH;
    if (id) { apiPath += `/${id}`; }

    const response = await this.get<ISampleRequest, ISampleResponse>(apiPath, request);

    return { result: response.ok, users: response.body.users };
  }

  public async testPost(request: ISampleRequest): Promise<{result: boolean}> {
    const apiPath = SAMPLE_API_PATH;

    const response = await this.post<ISampleRequest, ISampleResponse>(apiPath, request);

    return { result: response.ok };
  }

  public async testPut(request: ISampleRequest, id: number): Promise<{result: boolean}> {
    const apiPath = SAMPLE_API_PATH + `/${id}`;

    const response = await this.put<ISampleRequest, ISampleResponse>(apiPath, request);

    return { result: response.ok };
  }

  public async testDelete(id: number): Promise<{result: boolean}> {
    const apiPath = SAMPLE_API_PATH + `/${id}`;

    const response = await this.delete<any, ISampleResponse>(apiPath, undefined);

    return { result: response.ok };
  }
}

```

### モジュールに HTTP 通信用モジュールを追加

HTTP 通信を行うために必要なモジュールをインポート。
これをしないとロジック作ってもエラーで動かない。

```ts:src/app/app.module.ts
:
import { HttpClientModule } from '@angular/common/http'; // 追加
:

@NgModule({
  :
  imports: [
    :
    HttpClientModule, // 追加
  ],
:
})
export class AppModule { }

```

### コンポーネント修正

TS ファイルを以下のとおりに追加。
（サービス呼び出し処理追加、および `sampleList` の型を修正）

```ts:browser/app/components/home/home.component.ts
import { Component, OnInit } from '@angular/core';

import { Sample } from 'common/entities/sample/sample.entity';
import { SampleService } from '../../services/sample/sample.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

  id: number;
  name: string;
  age: number;

  // エラーメッセージ
  message: string;

  sampleList: Sample[] = [];

  constructor(private service: SampleService) { }

  ngOnInit() {
  }

  test() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 表示処理
    if (this.id && this.name && this.age) {
      this.sampleList.push({ id: this.id, name: this.name, age: this.age });
    } else {
      this.message = '未入力の項目があります。必ず全て入力してください';
    }
  }

  // 表示リストをリセット
  resetList() {
    this.sampleList = [];
  }

  async searchUsers() {
    // エラーメッセージ初期化
    this.message = '';

    // 通信実施
    const response = await this.service.testGet({ name: this.name, age: this.age }, this.id);

    if (response.result) {
      // 通信が成功した場合
      this.sampleList = response.users;
    } else {
      // 処理が失敗した場合
      this.message = 'エラーが発生しました';
    }
  }

  async createUser() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 作成処理
    if (this.name && this.age) {
      // 通信実施
      const response = await this.service.testPost({ name: this.name, age: this.age });

      if (response.result) {
        // 通信が成功した場合、一覧再取得
        await this.searchUsers();
      } else {
        // 処理が失敗した場合
        this.message = 'エラーが発生しました';
      }
    } else {
      this.message = '「ID」以外の項目は、全て入力してください';
    }
  }

  async updateUser() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 更新処理
    if (this.id) {
      // 通信実施
      const response = await this.service.testPut({ name: this.name, age: this.age }, this.id);

      if (response.result) {
        // 通信が成功した場合、一覧再取得
        await this.searchUsers();
      } else {
        // 処理が失敗した場合
        this.message = 'エラーが発生しました';
      }
    } else {
      this.message = '「ID」の項目は、必ず入力してください';
    }
  }

  async deleteUser() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 更新処理
    if (this.id) {
      // 通信実施
      const response = await this.service.testDelete(this.id);

      if (response.result) {
        // 通信が成功した場合、一覧再取得
        await this.searchUsers();
      } else {
        // 処理が失敗した場合
        this.message = 'エラーが発生しました';
      }
    } else {
      this.message = '削除する「ID」を入力してください';
    }
  }

}

```

HTML を以下のとおりに修正（検索ボタン追加）

```html:browser/app/components/home/home.component.html
<div class="home">
  <h2>ホーム画面</h2>

  <!-- <a [routerLink]="'/home2'">go to home2</a> -->

  <!-- エラーメッセージ -->
  <p *ngIf="message" class="error-message">{{ message }}</p>

  <!-- 入力エリア -->
  <div class="condition">
    <label>ユーザID</label>
    <input type="number" [(ngModel)]="id">

    <label>名前</label>
    <input type="text" [(ngModel)]="name">

    <label>年齢</label>
    <input type="number" [(ngModel)]="age">

    <button (click)="test()">表示追加</button>
    <button (click)="searchUsers()">検索</button>
    <button (click)="createUser()">登録</button>
    <button (click)="updateUser()">更新</button>
    <button (click)="deleteUser()">削除</button>
    <button (click)="resetList()">リセット</button>
  </div>

  <!-- 一覧表示エリア -->
  <table>
    <thead>
      <tr>
        <th>ID</th><th>名前</th><th>年齢</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let element of sampleList">
        <td>{{ element.id }}</td><td>{{ element.name }}</td><td>{{ element.age }}歳</td>
      </tr>
    </tbody>
  </table>
</div>

```

### この後の手順

サーバサイド作らないと動作確認できないので、サーバサイドを作る。

👉 [次の開発手順はこちら](https://qiita.com/tomy0610/items/1bf3c347df6a5b7d6806)