この記事はAngular+Firebaseでチャットアプリを作るのエントリーです。
前記事:Angular+FirebaseRTDBでCRUD(CREATE, READ, UPDATE, DELETE)を実装する
次記事:Angularのルーティング設定(基礎編)
この記事で行うこと
ここまでの記事では、ルーティング(ページ遷移)のない単一ページのWEBアプリを構築してきました。
ですが、これから認証機能を実装していくにあたって、ログインページなど他ページへの遷移や、ヘッダーなどサイト全体に共有するテンプレートが必要になってきます。
本稿ではそうした中規模以上のWEBアプリ構築をするため、モジュール構成の整理とヘッダーのテンプレート作成を行います。
Angularのモジュラリティシステム
以前の記事でも少し触れましたが、AngularはNgModuleというモジュラリティシステムを導入しています。
モジュールは、モジュール内部で扱うコンポーネント、サービス、外部ライブラリなどを定義しており、独立して動作するものになっています。これらのモジュールはLazy Loading(遅延読み込み)の対象とすることができ、一度に読み込ませる範囲の指定にもなります。
Lazy Loadingの例
検索にかかる機能を「サーチモジュール」とした場合、それにかかるページに遷移、ないしはその機能を使用しない限り読み込みが実行されない→ローディング時間が短くなる。
Angularのガイドではモジュールの種類を、最初に読み込まれるルートモジュール、アプリ全体に適用させるコアモジュール、機能単位で切り出された機能モジュール、共通部品を格納する共有モジュールの4つに分類しています。
下記ではコアモジュールルートモジュールと共有モジュールの実装方法を紹介します。
コアモジュールの扱いについて
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を使ってモジュールとコンポーネントを追加します。
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
を表示させます。
@Component({
selector: 'app-chat',
templateUrl: './chat.component.html',
styleUrls: ['./chat.component.css']
})
chat.component.ts
を開くと、@Component
デコレータの中にselector: 'app-chat'
という記述があります。これがこのコンポーネントを指定するカスタムタグ名となります。
では、app-chat
をカスタムタグとしてapp.component.html
に挿入します。
<div class="card-header">
NgChat <app-chat></app-chat>
</div>
実行結果
ChatComponent
のテンプレート内容がapp.component.html
に表示されました。このとき、ChatComponent
はAppComponent
の「子コンポーネント」である、ということができます。
ルートのコンポーネントであるAppComponent
には全ページに共通するものを残していくので、AppComponent
の内容をChatComponent
に移していきます。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<app-chat></app-chat>`, // templateに変更
// styleUrlsを削除
})
export class AppComponent {
constructor() {
}
}
<!--内容をchat.component.htmlに移動して削除-->
/* 内容をchat.component.cssに移動 */
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のライフサイクル。機会があったら取り上げてみます。
共有モジュールを読み込む
さて、次は共有モジュールを作成していきます。上記でも触れましたが、共有モジュールはアプリ内で使用する共通部品を管理するモジュールです。これまでに作成したものだとChatDatePipe
が共通部品として使えそうです。
ngIFやngForを扱うCommonModule
、ngModelなどを扱うFormsModule
と合わせて共有モジュールに登録します。
まず/src/app/shared
配下に/src/app/pipe
を移動し、SharedModule
にChatDatePipe``CommonModule``FormsModule
を追加します。
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
も合わせて読み込まれます。
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でルートモジュール内にヘッダーコンポーネントを作成します。
ng g component header
上記コマンドを叩くと自動的にコンポーネントが作成されますが、合わせてルートモジュールも更新がかかります。Angular CLIはディレクトリの階層構造をみて、該当するモジュールに自動でコンポーネントを登録する機能を備えています。慣れるとかなり楽チンです。
これでヘッダーコンポーネントのルートモジュールへの登録が完了しました。
ルートコンポーネントでヘッダーのカスタムタグが使えるようになったので、テンプレートに追加します。
@Component({
selector: 'app-root',
template: `
<app-header></app-header> <!--追加-->
<app-chat></app-chat>
`,
styleUrls: ['./app.component.css']
})
あとはヘッダーのテンプレートをモリモリ更新するだけです。
ヘッダー固定のレイアウトにするので、chat.component.css
の調整もしておきます。
<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>
.page section {
margin: 65px 10px 30px; /*margin-topを65pxに*/
}
実行結果
これで中規模アプリを構築していく準備が整いました。
次はログインページへのルーティングを扱います。
ソースコード
この時点でのソースコード
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。