Ionic + Firebaseで記録するカレンダーアプリを作る　その4 dayページからイベントを追加

#完成イメージ
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">こんな感じ。 <a href="https://t.co/CvOFjFq2th">pic.twitter.com/CvOFjFq2th</a></p>&mdash; げん げんと (@gento34165638) <a href="https://twitter.com/gento34165638/status/1190431795877007362?ref_src=twsrc%5Etfw">November 2, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#はじめに
環境は以下の通り
・ionic4
・Nativeとの橋渡しには[capacitor](https://capacitor.ionicframework.com)を使用
・カレンダーについては[ionic2-calendar](https://github.com/twinssbc/Ionic2-Calendar#readme)というライブラリーを使用

前回はFirebaseにイベントを追加し、取得までした。
:point_right:前回の記事は[こちら](https://qiita.com/kokogento/items/ba59a9fb93294f6e3f4c)

今回はRxJSを使うので、聞いたこともない人は[こちらの記事](https://qiita.com/agajo/items/7942743a0130f7a0f30b)を見てみるといいかも。
**正直な所、私もRxJSを大して理解していない！**「何か色々と監視してくれてるんだな〜」程度しか分かっていない。

よって本格的な解説は期待しないで欲しい。。:pray:

#概要
**完成イメージ**の動画を見ると分かりやすいが、このカレンダーアプリは大きく分けると2つのページから成り立っている。
・１つはメインとなるカレンダーのページ（`home.page.html`）
・もう１つが日付ごとの詳細ページ（`day.page.html`）

この`day.page.html`からイベントを追加したり、変更したりする仕様にしたい。
今回はこの**dayページから、イベントを追加**できるようにする。

**それを実現するにはカレンダーで選択した日の日付を、dayページに渡す必要がある。**


`home.page.ts`の`onTimeSelected`で日付を取得できているのがわかる。

#serviceを用意
まずは`home.service.ts`にイベントデータ関連の処理をまとめて書く。その方が`home.page.ts`などのファイルがすっきりして見やすくなる。

`home.service.ts`を作成

```terminal
ionic g service home/home
```

また[前回](https://qiita.com/kokogento/items/ba59a9fb93294f6e3f4c)は`home.page.ts`に`addNewEvent`を書いたが、これを消しておこう。

##home.service.tsを作る

```typescript:home.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { map, take } from 'rxjs/operators';
import { AngularFireAuth } from '@angular/fire/auth';

// DayEventというイベントの型
export interface DayEvent {
  id?: string;
  title: string;
  endTime: Date;
  startTime: Date;
  memo: string;
  allDay: boolean;
  birthControls: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class HomeService {
  // 取得したコレクションを格納
  private dayCollection: AngularFirestoreCollection<DayEvent>;
  // コレクションのストリームを格納
  private dayEvents: Observable<DayEvent[]>;

  eventSource = [];
  selectedDate = new Date();
  eventTitle = '';
  eventMemo = '';
  birthControl = null;

  constructor(
    private afs: AngularFirestore,
  ) {
   // コレクションを取得してdayCollectionに格納
    this.dayCollection = this.afs.collection<DayEvent>(`events`);
   // 1つのドキュメントをdayEventsに格納
    this.dayEvents = this.dayCollection.snapshotChanges().pipe(
          map(actions => {
            return actions.map(a => {
              const event = a.payload.doc.data();
              const id = a.payload.doc.id;
              return { id, ...event };
            });
          })
        );
  }

  getEvents(): Observable<DayEvent[]> {
    return this.dayEvents;
}

  getEvent(id: string) {
    return this.dayCollection.doc<DayEvent>(id).valueChanges().pipe(
      take(1),
      map(dayEvent => {
        dayEvent.id = id;
        return dayEvent;
      })
    );
  }

  addNewEvent() {
    const start = this.selectedDate;
    const end = this.selectedDate;
    const eventTitle = this.eventTitle;
    const eventMemo = this.eventMemo;
    const birthControl = this.birthControl;
    end.setHours(end.getHours() + 1);
    const event = {
      title: eventTitle,
       startTime: start,
       endTime: end,
       memo: eventMemo,
       birthControls: birthControl,
       allDay: false
    };
    return this.afs.collection(`events`).add(event);
  }

  updateEvent(dayEvent: DayEvent): Promise<void> {
    return this.dayCollection.doc(dayEvent.id)
      .update({
        title: dayEvent.title,
        memo: dayEvent.memo,
        birthControls: dayEvent.birthControls
      });
  }

  deleteEvent(id: string): Promise<void> {
    return this.afs.collection(`events`).doc(id).delete();
  }
}
```
訳わからなくてもとりあえず先へ進もう。

これでイベント1つ1つに対してのCRUDは全てできた。

>CRUD（クラッド）とは、ほとんど全てのコンピュータソフトウェアが持つ永続性[1]の4つの基>本機能のイニシャルを並べた用語。その4つとは、Create（生成）、Read（読み取り）、>Update（更新）、Delete（削除）である。

あとはこれをどう使うかだ。

#home.page.tsをserviceにデータを渡せるようにする

##home.page.ts
```typescript:home.page.ts
import { AngularFirestore } from '@angular/fire/firestore';
import { HomeService, DayEvent } from './home.service';
import { Component, OnInit } from '@angular/core';
import { ToastController, LoadingController, AlertController } from '@ionic/angular';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})

export class HomePage implements OnInit {

eventSource = [];
  viewTitle;
  selectedDate = new Date();
  calendar = {
    mode: 'month',
    currentDate: new Date(),
    locale: 'ja-JP'
  };
  birthControl = null;

  dayEvent: DayEvent = {
    title: '',
    startTime: this.selectedDate,
    endTime: this.selectedDate,
    memo: '',
    allDay: true,
    birthControls: this.birthControl
  };

  // ここでのプロパティがhtmlで使われる
  private dayEvents: Observable<DayEvent[]>;

constructor(
    private router: Router,
    private homeService: HomeService,
    private db: AngularFirestore,
    private authService: AuthService,
    private toastCtrl: ToastController,
  ) {
// Firebaseからイベント取得
        this.db
          .collection(`events`).snapshotChanges()
          .subscribe(colSnap => {
            this.eventSource = [];
            colSnap.forEach(snap => {
              const event: any = snap.payload.doc.data();
              // ここでのevent.idがhome.page.htmlで使われる
              event.id = snap.payload.doc.id;
              event.startTime = event.startTime.toDate();
              event.endTime = event.endTime.toDate();
              this.eventSource.push(event);
            });
          });
}

async ngOnInit() {
    this.dayEvents = this.homeService.getEvents();
  }

onViewTitleChanged(title) {
    this.viewTitle = title;
  }

  // 選択した日付を取得&serviceのaddNewEventを設定
  onTimeSelected(ev) {
    const selected = new Date(ev.selectedTime);
    // home.page.htmlに選択した日付を表示するためのプロパティを作成
    this.dayEvent.startTime = selected;
    this.dayEvent.endTime = selected;
    // home.serviceのaddNewEvent()の日付
    this.homeService.selectedDate = selected;
  }

onCurrentDateChanged(event: Date) {}

  today() {
    this.calendar.currentDate = new Date();
  }

async onEventSelected() {
    console.log('onEventSelected');
  }
}
```

##home.page.html
`ng-template`でカレンダー以外の画面を作る（日本語が分かりづらくてすいません）
[こちら](https://qiita.com/kokogento/items/75054646876c474e925c)を参考にしていただきたい！

```html:home.page.html
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      {{ viewTitle }}
    </ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="today()">Today</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <ion-grid>
    <ion-row>
      <ion-col size="12" size-sm="8" offset-sm="2">
        <calendar [eventSource]="eventSource" [calendarMode]="calendar.mode" [currentDate]="calendar.currentDate"
          (onCurrentDateChanged)="onCurrentDateChanged($event)" (onEventSelected)="onEventSelected(dayEvent)"
          (onTitleChanged)="onViewTitleChanged($event)" (onTimeSelected)="onTimeSelected($event)" [locale]="calendar.locale"
          [monthviewEventDetailTemplate]="template">
        </calendar>
      </ion-col>
    </ion-row>
  </ion-grid>

  <ng-template #template let-showEventDetail="showEventDetail" let-selectedDate="selectedDate"
    let-noEventsLabel="noEventsLabel">
    <ion-grid class="event-detail-container" has-bouncing="false" *ngIf="showEventDetail" overflow-scroll="false">
      <ion-row>
        <!-- もしイベントがある日なら -->
      <ion-col *ngFor="let event of selectedDate?.events" (click)="eventSelected(event)" class="ion-text-center">
        <ion-card *ngIf="dayEvent" class="monthview-eventdetail-timecolumn">
          <ion-card-header>
            <ion-card-title>{{ dayEvent.startTime | date:"yyyy/MM/dd (EEE)" }}</ion-card-title>
          </ion-card-header>
          <ion-card-content>
            <div class="event-detail">
              <p *ngIf="event.title">
                <ion-text color="dark">タイトル : </ion-text> {{ event.title }}
              </p>
              <p *ngIf="event.memo">
                <ion-text color="dark">メモ : </ion-text> {{ event.memo }}
              </p>
            <p *ngIf="event.birthControls">
              避妊あり
            </p>
            <p *ngIf="!event.birthControls">
              <ion-text color="dark">避妊なし</ion-text>
            </p>
            </div>
            <!-- idをurlにつける -->
            <ion-button class="ion-margin-top" [routerLink]="['/day', event.id]">
              記録ページへ
            </ion-button>
          </ion-card-content>
        </ion-card>
      </ion-col>
        <!-- もしイベントが無い日なら -->
        <ion-col *ngIf="selectedDate?.events.length==0" class="ion-text-center">
          <ion-card *ngIf="dayEvent" class="monthview-eventdetail-timecolumn">
            <ion-card-header>
              <ion-card-title>{{ dayEvent.startTime | date:"yyyy/MM/dd (EEE)" }}</ion-card-title>
            </ion-card-header>
            <ion-card-content>
              <p>この日はしていないよ。</p>
              <ion-button class="ion-margin-top" [routerLink]="['/day']">
                記録ページへ
              </ion-button>
            </ion-card-content>
          </ion-card>
        </ion-col>
      </ion-row>
    </ion-grid>
  </ng-template>
</ion-content>
```
次は遷移先のdayページを作る。

#dayページを用意
```terminal
ionic g page day
```

<img width="263" alt="スクリーンショット 2019-11-09 19.01.33.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/ea0f8a8d-ce86-543d-19ff-772467eed0a9.jpeg">



##`app-routing.module.ts`でdayページのroutingを設定する
まずdayページは以下の様に**２種類存在する。**
・イベントがない日のdayページ
・イベントがある日のdayページ

**イベントがある日のdayページ**についてだが、そのイベントがどのイベントかを判別するのに`id`が必要となる。

この`id`をURLで渡すと
`http://localhost:8100/day/kMItE6tYjUocvcjTjuJI`
こんな感じになる。

**`kMItE6tYjUocvcjTjuJI`の部分が`id`で**、Firebaseのデータベースに追加したら勝手に生成されるものだ。

これを使うためにはroutingを少し変える必要がある。

```typescript:app-routing.module.ts
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  {
    path: 'home',
    loadChildren: './home/home.module#HomePageModule',
  },
// idがある場合とない場合を用意
  { path: 'day', loadChildren: './day/day.module#DayPageModule' },
  { path: 'day/:id', loadChildren: './day/day.module#DayPageModule' },
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```

##day.page.htmlを作る
```html:day.page.html
<ion-header>
  <ion-toolbar color="secondary">
      <ion-buttons slot="start">
          <ion-back-button defaultHref="/home"></ion-back-button>
        </ion-buttons>
    <ion-title>
      {{ selectedDate | date:"yyyy/MM/dd (EEE)" }}
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <h2 text-center>記録しよう</h2>
  <form #f="ngForm">
    <ion-grid>
      <ion-row>
        <ion-col size="12" size-sm="8" offset-sm="2">
          <ion-item>
            <ion-label position="floating">タイトル</ion-label>
            <ion-input type="text" placeholder="もしタイトルが欲しければ❤️" [(ngModel)]="dayEvent.title" name="title">
            </ion-input>
          </ion-item>
          <ion-item>
            <ion-label position="floating">メモ</ion-label>
            <ion-textarea rows="6" [(ngModel)]="dayEvent.memo" placeholder="もしメモしたい事があれば❣️" name="memo">
            </ion-textarea>
          </ion-item>
          <ion-item>
            <ion-label>避妊あり</ion-label>
            <ion-checkbox [(ngModel)]="dayEvent.birthControls" name="birthControls">
            </ion-checkbox>
          </ion-item>
        </ion-col>
      </ion-row>
    </ion-grid>
  </form>
</ion-content>

<ion-footer *ngIf="!dayEvent.id">
  <ion-button expand="block" fill="solid" color="tertiary" (click)="addNewEvent()">
    <ion-icon name="checkmark" slot="start"></ion-icon>
    記録する
  </ion-button>
</ion-footer>

<ion-footer *ngIf="dayEvent.id">
  <ion-row no-padding text-center>
    <ion-col size="6">
      <ion-button expand="block" fill="solid" color="danger" (click)="deleteEvent()">
        <ion-icon name="trash" slot="start"></ion-icon>
        削除
      </ion-button>
    </ion-col>
    <ion-col size="6">
      <ion-button expand="block" fill="solid" color="tertiary" (click)="updateEvent()">
        <ion-icon name="save" slot="start"></ion-icon>
        更新
      </ion-button>
    </ion-col>
  </ion-row>
</ion-footer>
```

##day.page.tsを作る

```typescript:day.page.ts
import { HomeService, DayEvent } from './../home/home.service';
import { Component, OnInit, ViewChild } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { ToastController } from '@ionic/angular';
import { NgForm } from '@angular/forms';


@Component({
  selector: 'app-day',
  templateUrl: './day.page.html',
  styleUrls: ['./day.page.scss'],
})

export class DayPage implements OnInit {
  @ViewChild('f', { static: true }) form: NgForm;
  eventSource = [];
  viewTitle;
  selectedDate = new Date();
  birthControl = true;
  eventTitle = '';
  eventMemo = '';

  dayEvent: DayEvent = {
    title: this.eventTitle,
    startTime: this.selectedDate,
    endTime: this.selectedDate,
    memo: this.eventMemo,
    allDay: true,
    birthControls: this.birthControl
  };
  id = null;

  constructor(
    private homeService: HomeService,
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private toastCtrl: ToastController,
  ) {
  }

  ngOnInit() {
    // activatedRouteでidを取得
    this.id = this.activatedRoute.snapshot.paramMap.get('id');
    // homeからserviceを経由して、dayに日付データを渡す
    this.selectedDate = this.homeService.selectedDate;
    this.birthControl = this.dayEvent.birthControls;
  }

  ionViewWillEnter() {
    if (this.id) {
     this.homeService.getEvent(this.id).subscribe(dayEvent => {
        this.dayEvent = dayEvent;
      });
    }
  }

  addNewEvent() {
// dayページで入力された値を、addNewEvent()の為にserviceへ渡す
    this.homeService.eventTitle = this.dayEvent.title;
    this.homeService.eventMemo = this.dayEvent.memo;
    this.homeService.birthControl = this.dayEvent.birthControls;
    this.homeService.addNewEvent().then(() => {
      this.showToast('記録しました');
      this.router.navigateByUrl('/');
    });
  }

  updateEvent() {
    this.homeService.updateEvent(this.dayEvent).then(() => {
      this.router.navigateByUrl('/');
      this.showToast('更新しました');
    },
      err => {
        this.showToast('問題が発生しました。更新できなかったです。');
      });
  }

  deleteEvent() {
    this.homeService.deleteEvent(this.dayEvent.id).then(() => {
      this.router.navigateByUrl('/');
      this.showToast('削除しました');
    },
      err => {
        this.showToast('問題が発生しました。削除できなかったです。');
      });
  }

  showToast(msg) {
    this.toastCtrl.create({
      message: msg,
      duration: 2000
    }).then(toast => toast.present());
  }
}
```

これでdayページからイベントを追加することができた。
根幹部分はひとまず実装できたっぽかな。



#最後に
続き:point_down:
[その5 ユーザー作成と認証](https://qiita.com/kokogento/items/90ae35c2c587c69c58de)


RxJSはまじでよくわかっていません。。。
ほとんど[こちら](https://devdactic.com/ionic-4-firebase-angularfire-2/)の記事を真似しただけです。

各ストアで配信されてます。
興味があれば使ってみてください。:relieved:

<a href="https://apps.apple.com/jp/app/%E6%80%A7%E6%B4%BB%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC/id1484151444?mt=8">Apple Store</a>

[Google Play](https://play.google.com/store/apps/details?id=com.hCalendar.app)