#完成イメージ
こんな感じ。 pic.twitter.com/CvOFjFq2th
— げん げんと (@gento34165638) November 2, 2019
#はじめに
環境は以下の通り
・ionic4
・Nativeとの橋渡しにはcapacitorを使用
・カレンダーについてはionic2-calendarというライブラリーを使用
前回はFirebaseにイベントを追加し、取得までした。
前回の記事はこちら
今回はRxJSを使うので、聞いたこともない人はこちらの記事を見てみるといいかも。
正直な所、私もRxJSを大して理解していない!「何か色々と監視してくれてるんだな〜」程度しか分かっていない。
よって本格的な解説は期待しないで欲しい。。
#概要
完成イメージの動画を見ると分かりやすいが、このカレンダーアプリは大きく分けると2つのページから成り立っている。
・1つはメインとなるカレンダーのページ(home.page.html
)
・もう1つが日付ごとの詳細ページ(day.page.html
)
このday.page.html
からイベントを追加したり、変更したりする仕様にしたい。
今回はこのdayページから、イベントを追加できるようにする。
それを実現するにはカレンダーで選択した日の日付を、dayページに渡す必要がある。
home.page.ts
のonTimeSelected
で日付を取得できているのがわかる。
#serviceを用意
まずはhome.service.ts
にイベントデータ関連の処理をまとめて書く。その方がhome.page.ts
などのファイルがすっきりして見やすくなる。
home.service.ts
を作成
ionic g service home/home
また前回はhome.page.ts
にaddNewEvent
を書いたが、これを消しておこう。
##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
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
でカレンダー以外の画面を作る(日本語が分かりづらくてすいません)
こちらを参考にしていただきたい!
<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

##app-routing.module.ts
でdayページのroutingを設定する
まずdayページは以下の様に2種類存在する。
・イベントがない日のdayページ
・イベントがある日のdayページ
イベントがある日のdayページについてだが、そのイベントがどのイベントかを判別するのにid
が必要となる。
このid
をURLで渡すと
http://localhost:8100/day/kMItE6tYjUocvcjTjuJI
こんな感じになる。
kMItE6tYjUocvcjTjuJI
の部分がid
で、Firebaseのデータベースに追加したら勝手に生成されるものだ。
これを使うためにはroutingを少し変える必要がある。
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を作る
<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を作る
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ページからイベントを追加することができた。
根幹部分はひとまず実装できたっぽかな。
#最後に
続き
その5 ユーザー作成と認証
RxJSはまじでよくわかっていません。。。
ほとんどこちらの記事を真似しただけです。
各ストアで配信されてます。
興味があれば使ってみてください。