1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-11-10

#完成イメージ

#はじめに
環境は以下の通り
・ionic4
・Nativeとの橋渡しにはcapacitorを使用
・カレンダーについてはionic2-calendarというライブラリーを使用

前回はFirebaseにイベントを追加し、取得までした。
:point_right:前回の記事はこちら

今回はRxJSを使うので、聞いたこともない人はこちらの記事を見てみるといいかも。
正直な所、私もRxJSを大して理解していない!「何か色々と監視してくれてるんだな〜」程度しか分かっていない。

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

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

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

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

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

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

home.service.tsを作成

ionic g service home/home

また前回home.page.tsaddNewEventを書いたが、これを消しておこう。

##home.service.tsを作る

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

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でカレンダー以外の画面を作る(日本語が分かりづらくてすいません)
こちらを参考にしていただきたい!

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ページを用意

ionic g page day
スクリーンショット 2019-11-09 19.01.33.jpg

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

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

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

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

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

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を作る

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を作る

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 ユーザー作成と認証

RxJSはまじでよくわかっていません。。。
ほとんどこちらの記事を真似しただけです。

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

Apple Store

Google Play

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?