AngularのNgModuleを使って、アプリの構成を管理する

> この記事は[Angular+Firebaseでチャットアプリを作る](http://qiita.com/Yamamoto0525/items/a76ea4b3924eeb82b0f9)のエントリーです。
> 前記事：[Angular+FirebaseRTDBでCRUD(CREATE, READ, UPDATE, DELETE)を実装する](http://qiita.com/Yamamoto0525/items/c1ec1b7ce2350b294aeb)
> 次記事：[Angularのルーティング設定（基礎編）](http://qiita.com/Yamamoto0525/items/e870713d9d05d2d36a80)

# この記事で行うこと

ここまでの記事では、ルーティング（ページ遷移）のない単一ページのWEBアプリを構築してきました。
ですが、これから認証機能を実装していくにあたって、ログインページなど他ページへの遷移や、ヘッダーなどサイト全体に共有するテンプレートが必要になってきます。
本稿ではそうした中規模以上のWEBアプリ構築をするため、モジュール構成の整理とヘッダーのテンプレート作成を行います。

- [モジュールを整理する](http://qiita.com/Yamamoto0525/items/c647f03a5ab56883e09e#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E6%95%B4%E7%90%86%E3%81%99%E3%82%8B)
- [ヘッダーテンプレートを作る](http://qiita.com/Yamamoto0525/items/c647f03a5ab56883e09e#%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B)

### Angularのモジュラリティシステム

[以前の記事](http://qiita.com/Yamamoto0525/items/437a2884c0c51f5a3af8)でも少し触れましたが、AngularはNgModuleというモジュラリティシステムを導入しています。
モジュールは、モジュール内部で扱うコンポーネント、サービス、外部ライブラリなどを定義しており、独立して動作するものになっています。これらのモジュールはLazy Loading（遅延読み込み）の対象とすることができ、一度に読み込ませる範囲の指定にもなります。

> #### Lazy Loadingの例
> 検索にかかる機能を「サーチモジュール」とした場合、それにかかるページに遷移、ないしはその機能を使用しない限り読み込みが実行されない→ローディング時間が短くなる。

[Angularのガイド](https://angular.jp/guide/ngmodules)ではモジュールの種類を、最初に読み込まれるルートモジュール、~~アプリ全体に適用させるコアモジュール~~、機能単位で切り出された機能モジュール、共通部品を格納する共有モジュールの４つに分類しています。
下記では~~コアモジュール~~ルートモジュールと共有モジュールの実装方法を紹介します。

> **コアモジュールの扱いについて**
> Angular7の公式ドキュメントから、コアモジュールについての記述が削除されました。
> これはサービスがシングルトンに対応することになったことに伴い、ルートモジュールとコアモジュールの機能をあえてわける必要がなくなったためのようです。（参考：https://github.com/angular/angular/issues/29848)
> コアモジュールが使えなくなったわけではないので、ここでの記述も残そうかと思いましたが、チュートリアルという性質上修正するのが妥当と思い、以下の記述を修正しています。

**************
（2018/1追記）記述を現時点で最新のものに差し替えました。
（2018/9追記）記述を現時点で最新のものに差し替えました。
（2020/6追記）記述を現時点で最新のものに差し替えました。
**************

# 実装内容

## モジュールを整理する

ここまでで作成した単一ページのAngularプロジェクト（`/src/app`以下）は次のようになっています。

```
 app
  ├── class
  │    ├── chat.spec.ts
  │    └── chat.ts
  ├── pipe
  │    ├── chat-date.pipe.spec.ts
  │    └── chat-date.pipe.ts
  ├── app.component.css
  ├── app.component.html
  ├── app.component.spec.ts
  ├── app.component.ts
  ├── app.module.ts
  └── app-routing.module.ts
```
この構成にAngular CLIを使ってモジュールとコンポーネントを追加します。

```bash
ng g module shared
ng g component chat
```

`app.module.ts`を**ルートモジュール**、新しく作成された`shared.module.ts`を**共有モジュール**として扱っていきます。

まずはルートモジュールと共有モジュールの内容を見ていきます。

### 子コンポーネントを表示する

ユーザーがアプリを起動後、最初に実行されるのがルートモジュールです。
ルートモジュールである`app.module.ts`を見てみると、`@NgModule`というデコレータの`bootstrap`というプロパティに`AppComponent`が指定されています。
これは最初に読み込むコンポーネントを`AppComponent`に指定したという意味になります。

指定された`AppComponent`は`app.component.ts`に書かれているコンポーネントで、`app.component.html`をhtmlテンプレートとして指定しています。今回はこのテンプレートに上記で作成した`ChatComponent`を表示させます。

```ts:chat.component.ts
@Component({
  selector: 'app-chat',
  templateUrl: './chat.component.html',
  styleUrls: ['./chat.component.css']
})
```

`chat.component.ts`を開くと、`@Component`デコレータの中に`selector: 'app-chat'`という記述があります。これがこのコンポーネントを指定するカスタムタグ名となります。

では、`app-chat`をカスタムタグとして`app.component.html`に挿入します。

```html:app.component.html
    <div class="card-header">
      NgChat　<app-chat></app-chat>
    </div>
```

#### 実行結果

![NgChat (8).png](https://qiita-image-store.s3.amazonaws.com/0/112566/a41a4550-6d42-26d6-bdf4-18f12f656681.png)

`ChatComponent`のテンプレート内容が`app.component.html`に表示されました。このとき、`ChatComponent`は`AppComponent`の「子コンポーネント」である、ということができます。
ルートのコンポーネントである`AppComponent`には全ページに共通するものを残していくので、`AppComponent`の内容を`ChatComponent`に移していきます。

```ts:app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-chat></app-chat>`,　// templateに変更
  // styleUrlsを削除
})

export class AppComponent {

  constructor() {
  }

}
```
```html:app.component.html
<!--内容をchat.component.htmlに移動して削除-->
```
```css:app.component.css
/* 内容をchat.component.cssに移動 */
```
```ts:chat.component.ts
import { Component, OnInit } from '@angular/core';
import { Comment, User } from '../class/chat';
import { AngularFirestore } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

const CURRENT_USER: User = new User(1, 'Tanaka Jiro');
const ANOTHER_USER: User = new User(2, 'Suzuki Taro');

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

  public content = '';
  public comments: Observable<Comment[]>;
  public currentUser = CURRENT_USER;

  // DI（依存性注入する機能を指定）
  constructor(private db: AngularFirestore) {
  }

  ngOnInit(): void {
    this.comments = this.db
      .collection<Comment>('comments', ref => {
        return ref.orderBy('date', 'asc');
      })
      .snapshotChanges()
      .pipe(
        map(actions => actions.map(action => {
          // 日付をセットしたコメントを返す
          const data = action.payload.doc.data() as Comment;
          const key = action.payload.doc.id;
          const commentData = new Comment(data.user, data.content);
          commentData.setData(data.date, key);
          return commentData;
        })));
  }

  // 新しいコメントを追加
  addComment(e: Event, comment: string) {
    if (comment) {
      this.db
        .collection('comments')
        .add(new Comment(this.currentUser, comment).deserialize());
      this.content = '';
    }
  }

  // 編集フィールドの切り替え
  toggleEditComment(comment: Comment) {
    comment.editFlag = (!comment.editFlag);
  }

  // コメントを更新する
  saveEditComment(comment: Comment) {
    this.db
      .collection('comments')
      .doc(comment.key)
      .update({
        content: comment.content,
        date: comment.date
      })
      .then(() => {
        alert('コメントを更新しました');
        comment.editFlag = false;
      });
  }

  // コメントをリセットする
  resetEditComment(comment: Comment) {
    comment.content = '';
  }

  // コメントを削除する
  deleteComment(key: string) {
    this.db
      .collection('comments')
      .doc(key)
      .delete()
      .then(() => {
        alert('コメントを削除しました');
      });
  }
}
```

これで`AppComponent`にかかる記述がかなりすっきりしました。内容を移すにあたり、変更を加えた点を列記しておきます。

#### `template`に変更

`app.component.ts`ではこれまで外部ファイルをhtmlテンプレートに指定する`templateUrl`というプロパティを使っていましたが、これを`template`に変更しています。
`template`はデコレータ内に直接テンプレートを書き込むためのプロパティです。テンプレートの記述内容が少ない時などに使用します。

#### `constructor()`の記述を`ngOnInit()`に移す

`ngOnInit()`はコンポーネントが読み込まれる際に最初に実行される関数です。役割的には`constructor()`と同じなのですが、データのライフサイクルをわかりやすくしておくため、最初に読み込まれるものは極力`ngOnInit()`に記述するようにしていきます。

> [Angularのライフサイクル](https://angular.io/guide/lifecycle-hooks)。機会があったら取り上げてみます。

### 共有モジュールを読み込む

さて、次は共有モジュールを作成していきます。上記でも触れましたが、共有モジュールはアプリ内で使用する共通部品を管理するモジュールです。これまでに作成したものだと`ChatDatePipe`が共通部品として使えそうです。
ngIFやngForを扱う`CommonModule`、ngModelなどを扱う`FormsModule`と合わせて共有モジュールに登録します。

まず`/src/app/shared`配下に`/src/app/pipe`を移動し、`SharedModule`に`ChatDatePipe``CommonModule``FormsModule`を追加します。

```ts:src/app/shared/shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from "@angular/forms"; // 追加

import { ChatDatePipe } from "./pipe/chat-date.pipe"; // 追加

@NgModule({
  imports: [
    CommonModule,
    FormsModule, // 追加
  ],
  exports: [  // 追加
    CommonModule,
    FormsModule,
    ChatDatePipe
  ],
  declarations: [ // 追加
    ChatDatePipe
  ]
})
export class SharedModule { }
```

注意すべき点として、`SharedModule`の外部で使う部品については`exports`にも登録する必要があります。これを忘れるとDIをする時に「宣言されていません」というエラーが出るので、必ず登録しておくようにします。

次に`AppModule`を更新します。先ほど`SharedModule`に追加したものを削除し、代わりに`SharedModule`をインポートします。
ここでは`AppModule`に登録を行っていますが、`SharedModule`は必ずしもルートモジュールに登録する必要はありません。
`SharedModule`を必要とするモジュールがあればその都度登録を行い、そのモジュールが読み込まれる際に`SharedModule`も合わせて読み込まれます。

```ts:src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
// FormsModuleを削除
import { environment } from '../environments/environment';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireAuthModule } from '@angular/fire/auth';

import { SharedModule } from './shared/shared.module'; // 追加
import { AppComponent } from './app.component';
import { ChatComponent } from './chat/chat.component'; // 追加
// ChatDatePipeを削除


@NgModule({
  declarations: [
    AppComponent,
    ChatComponent
  ],
  imports: [
    NgbModule.forRoot(),
    // FormsModuleを削除
    BrowserModule,
    SharedModule, // 追加
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule,
    AngularFireAuthModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```
これで共有モジュールの読み込みができるようになりました。
この時点で`/src/app`配下は次のようになっています。

```
 app
  ├── chart
  │    ├── chat.component.css
  │    ├── chat.component.html
  │    ├── chat.component.spec.ts
  │    └── chat.component.ts
  ├── class
  │    ├── chat.spec.ts
  │    └── chat.ts
  ├── shared
  │    ├──　pipe
  │    │　　　├── chat-date.pipe.spec.ts
  │    │　　　└── chat-date.pipe.ts
  │    └── shared.module.ts
  ├── app.component.spec.ts
  ├── app.component.ts
  ├── app.module.ts
  └── app-routing.module.ts
```

## ヘッダーテンプレートを作る

次はルートモジュールにヘッダーを作っていきます。
Angular CLIでルートモジュール内にヘッダーコンポーネントを作成します。

```bash
ng g component header
```
上記コマンドを叩くと自動的にコンポーネントが作成されますが、合わせてルートモジュールも更新がかかります。Angular CLIはディレクトリの階層構造をみて、該当するモジュールに自動でコンポーネントを登録する機能を備えています。慣れるとかなり楽チンです。

これでヘッダーコンポーネントのルートモジュールへの登録が完了しました。
ルートコンポーネントでヘッダーのカスタムタグが使えるようになったので、テンプレートに追加します。

```ts:src/app/app.component.ts
@Component({
  selector: 'app-root',
  template: ` 
  		<app-header></app-header> <!--追加-->
  		<app-chat></app-chat>
  `, 
  styleUrls: ['./app.component.css']
})
```
あとはヘッダーのテンプレートをモリモリ更新するだけです。
ヘッダー固定のレイアウトにするので、`chat.component.css`の調整もしておきます。

```html:src/app/header/header.component.html
<nav class="navbar fixed-top navbar-dark bg-primary">
	<a class="navbar-brand" href="#">NgChat</a>
	<span class="navbar-text">
		<a href="#">Login</a>
    </span>
</nav>
```

```css:src/app/chat/chat.component.css
.page section {
  margin: 65px 10px 30px; /*margin-topを65pxに*/
}
```

#### 実行結果
![localhost_4200_ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112566/b10bb533-7674-2a0d-7f30-97ef2e5c4c3c.png)



これで中規模アプリを構築していく準備が整いました。
次はログインページへのルーティングを扱います。

## ソースコード
この時点での[ソースコード](https://github.com/Yamamoto0525/NgChat/tree/ad883517f0785225df08e2ac78ab60f002e9bfd8)
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。