NCMBとMonaca、Framework7を使ってカレンダーアプリを作る（その3：予定の表示と登録）

NCMBとMonacaを使ってカレンダーアプリを作ります。予定を登録したり、Framework7のカレンダーコンポーネントを使って予定を表示できるというアプリです。

[前回の記事では認証を実装しました](https://qiita.com/goofmint/items/83a3024b2c0a3a9e78c5)ので、今回はカレンダーへのデータ登録と表示などを実装します。

## コードについて

今回のコードは[NCMBMania/monaca-calendar-app](https://github.com/NCMBMania/monaca-calendar-app)にアップロードしてあります。実装時の参考にしてください。

## カレンダーの予定一覧について

### ストアの利用

カレンダーの予定は、Framework7のストアを使って管理します。こうすることで、画面を遷移した後でもデータを一元管理でき、アクセスも行えます。

予定一覧と一緒に、選択した日付もストアに入れます。

```js
// js/store.js に記述
const createStore = Framework7.createStore;
const store = createStore({
  state: {
    schedules: [], // スケジュール一覧
    selectedDate: null, // 選択された日付
  },
  getters: {
    schedules({ state }) {
      return state.schedules;
    },
    selectedDate({ state }) {
      return state.selectedDate;
    },
  },
  actions: {
    // スケジュール一覧をセット
    addSchedules({ state }, schedules) {
      state.schedules = schedules;
    },
    // 選択された日付をセット
    setDate({ state }, date) {
      state.selectedDate = date;
    },
  },
});
```

### 画面について

![0409 localhost - 1120115748.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/31b53e7f-d111-0d19-6c44-d6fa029c8908.jpeg)


カレンダー画面は以下のようになります。カレンダーの表示自体はFramework7のコンポーネントを使うので `#calendar` だけで済みます。

`selectedDate.value` のように `value` が付いているのはストアを利用しているからです。

```html
<div id="calendar"></div>
<div class="block-title">
	${ selectedDate.value ? moment(selectedDate.value).format('YYYY年MM月DD日') : "日付を選択してください"}
</div>
<div class="list media-list chevron-center">
	${ selectedDate.value === null || schedules.value.length === 0 ?
		$h`<div class="block">予定はありません</div>` :
		$h`<ul>
			${ schedules.value
				.filter(schedule => moment(schedule.startDate.iso).format('YYYY-MM-DD') === moment(selectedDate.value).format('YYYY-MM-DD'))
				.map(schedule => $h`
				<li>
					<a href="/form/${schedule.objectId}/" class="item-link item-content">
						<div class="item-inner">
							<div class="item-title-row">
								<div class="item-title">${schedule.title}</div>
								<div class="item-after">
									${moment(schedule.startDate.iso).format('HH:mm')}〜${moment(schedule.endDate.iso).format('HH:mm')}
								</div>
							</div>
							<div class="item-text">
								${schedule.body}
							</div>
						</div>
					</a>
				</li>
			`)}
		</ul>`
	}
</div>
```

### 予定の取得とストアへの設定

画面を表示したタイミングでNCMBのデータストアから予定一覧を取得しています。その結果は、ストアへセットします。

まずストアからデータを取得しておきます。

```js
// ストアからデータを取得
const { schedules, selectedDate } = $store.getters;
```

そして画面が表示される際のイベントで、予定を取得する関数を実行します。

```js
// 画面が表示される際に実行される処理
$on('page:beforein', async () => {
	// NCMBから予定を取得
	await getSchedule(new Date);
});
```

`getSchedule` の内容は以下のようになります。デフォルトでは表示月の月初から月末までの予定をすべて取得し、それをストアに保存します。

```js
// 選択された日付からデータストアを検索、予定の一覧を取得する処理
const getSchedule = async (date) => {
	// データストアの準備
	const Schedule = ncmb.DataStore('Schedule');
	// データストアを検索
	const results = await Schedule
		.greaterThanOrEqualTo('startDate', moment(date).startOf('month').toDate()) // 開始日は入力値以上
		.lessThan('endDate', moment(date).endOf('month').toDate()) // 終了日は明日未満
		.limit(1000)
		.fetchAll();
	// 検索結果をストアに適用
	$store.dispatch('addSchedules', results);
	// 画面に表示
	applySchedules();
};
```

### カレンダー表示

カレンダーはFramework7のコンポーネントを使います。以下のコードを参照してください。

```js
// 初期表示時の処理
$on('page:init', async () => {
	// Framework7のカレンダーを初期化
	calendar = $f7.calendar.create({
		containerEl: '#calendar',
		value: [new Date()],
		events: [],
		weekHeader: false,
		renderToolbar: function () {
			return `
			<div class="toolbar calendar-custom-toolbar no-shadow">
				<div class="toolbar-inner">
					<div class="left">
						<a href="#" class="link icon-only">
							<i class="icon icon-back"></i>
						</a>
					</div>
					<div class="center"></div>
					<div class="right">
						<a href="#" class="link icon-only">
							<i class="icon icon-forward"></i>
						</a>
					</div>
				</div>
			</div>
			`;
		},
		on: {
			init: (c) => {
				$('.calendar-custom-toolbar .center').text(`${c.currentYear}年${c.currentMonth + 1}月`);
				$('.calendar-custom-toolbar .left .link').on('click', () => {
					c.prevMonth();
				});
				$('.calendar-custom-toolbar .right .link').on('click', () => {
					c.nextMonth();
				});
			},
			// 年月を変更した際のイベント
			monthYearChangeStart: async (c) => {
				// 表示日付をリセット
				$store.dispatch('setDate', null);
				// 該当月のスケジュールを取得
				await getSchedule(new Date(c.currentYear, c.currentMonth, 1));
				// タイトルを修正
				$('.calendar-custom-toolbar .center').text(`${c.currentYear}年${c.currentMonth + 1}月`);
			},
			// 日付を選択した際の処理
			dayClick: (calendar, dayEl, year, month, day) => {
				// 選択日付を設定
				$store.dispatch('setDate', new Date(year, month, day));
			}
		}
	});
});
```

カレンダーコンポーネントの `events` に予定のデータを追加することで、カレンダー上にドット表示できるようになります。そのための関数が `applySchedules` になります。

```js
// 取得したスケジュールをカレンダーに反映する処理
const applySchedules = () => {
	// Framework7用のイベント配列に変換
	const events = schedules.value.map(schedule => ({
			date: new Date(moment.parseZone(schedule.startDate.iso).format('YYYY/MM/DD')),
			color: getColor(),
	}));
	// カレンダーに追加
	calendar.params.events = events;
	// 表示を更新
	calendar.update();
}
```

`getColor` 関数はランダムな色を返すだけの関数です。

```js
// ランダムな色を返す関数
const getColor = () => {
	return `#${Math.floor(Math.random() * 16777215).toString(16)}`;
};
```

## 予定を追加・編集する

予定を追加したり、編集する際には `form.html` へ移動します。これは `calendar.html` のヘッダーメニューにあるプラスアイコンより遷移します。

```html
<div class="navbar">
	<div class="navbar-bg"></div>
	<div class="navbar-inner sliding">
		<div class="title">カレンダー</div>
		<div class="right">
			<a href="/form/new/" class="link">
				<i class="f7-icons">plus</i>
			</a>
		</div>
	</div>
</div>
```

### フォーム画面について

![0409 localhost - 1120120122.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/82b8ba38-53bc-830d-1f37-9b049a950361.jpeg)


フォーム画面は予定のタイトルと詳細、予定開始日時、終了日時などを入力して登録を行います。

```html
<div class="list no-hairlines-md">
	<form id="schedule">
		<ul>
			<li class="item-content item-input">
				<div class="item-inner">
					<div class="item-input-wrap">
						<input type="text" id="startDate" name="startDate" @change=${setEndDate} placeholder="日時" value="${targetDate}" />
						<span class="input-clear-button"></span>
					</div>
				</div>
			</li>
			<li class="item-content item-input">
				<div class="item-inner">
					<div class="item-input-wrap">
						<input type="text" id="endDate" name="endDate" placeholder="終了日時" />
						<span class="input-clear-button"></span>
					</div>
				</div>
			</li>
			<li class="item-content item-input">
				<div class="item-inner">
					<div class="item-input-wrap">
						<input type="text" name="title" placeholder="タイトル" value="${schedule.title}" />
						<span class="input-clear-button"></span>
					</div>
				</div>
			</li>
			<li class="item-content item-input">
				<div class="item-inner">
					<div class="item-input-wrap">
						<textarea class="resizable" name="body">${schedule.body}</textarea>
						<span class="input-clear-button"></span>
					</div>
				</div>
			</li>
		</ul>
		<div class="block block-strong">
			<p class="row">
				<a href="#" @click=${saveSchedule} class="col button">保存</a>
			</p>
			${ schedule.objectId ?
				$h`<p class="row">
						<a href="#" @click=${deleteSchedule} class="col button">削除</a>
					</p>`
				: ''
			}
		</div>
</form>
</div>
```

### データの準備

まず新規データか既存データかの判別を行います。これは画面遷移時のIDによって判別できます。

```js
// ストアから選択された日付とスケジュール一覧を取得
const { schedules, selectedDate } = $store.getters;
// データストアのクラスを準備
const Schedule = ncmb.DataStore('Schedule');
// 既存データの修正か、新規データの追加かを判定
const schedule = props.id === 'new' ? new Schedule() : schedules.value.find(s => s.objectId === props.id);
```

既存データの場合は `/form/new/` ですが、既存データの場合は `/form/:objectId` になっています。この `objectId` はデータによって変わる、データのユニークキーになります。

以下は `calendar.html` の予定一覧表示部分です。

```html
${ schedules.value
	.filter(schedule => moment(schedule.startDate.iso).format('YYYY-MM-DD') === moment(selectedDate.value).format('YYYY-MM-DD'))
	.map(schedule => $h`
	<li>
		<a href="/form/${schedule.objectId}/" class="item-link item-content">
			<div class="item-inner">
				<div class="item-title-row">
					<div class="item-title">${schedule.title}</div>
					<div class="item-after">
						${moment(schedule.startDate.iso).format('HH:mm')}〜${moment(schedule.endDate.iso).format('HH:mm')}
					</div>
				</div>
				<div class="item-text">
					${schedule.body}
				</div>
			</div>
		</a>
	</li>
`)}
```

そしてデータに対してACLを設定します。ACLはデータをセキュアに扱うために必要です。今回はデータを作成した本人のみ、読み書きできるようにします。

```js
// ユーザーだけしかアクセスできないようにする
const user = ncmb.User.getCurrentUser();
const acl = new ncmb.Acl();
acl.setUserReadAccess(user, true);
acl.setUserWriteAccess(user, true);
schedule.set('acl', acl);
```

### データの保存処理

データの保存は `saveSchedule` 関数で行っています。入力値の中でも開始日時・終了日時はDateオブジェクトに変換してセットします。

そして既存データ（objectIdがある）は `update` メソッド、新規データは `save` メソッドでデータを保存します。

```js
// 予定の保存処理
const saveSchedule = async () => {
	// 入力値の取得
	const params = app.form.convertToData($('form#schedule'));
	// 入力値をデータストアのインスタンスにセット
	for (const key in params) {
		// 日付は日付オブジェクトに変換する（それ以外は文字列のまま）
		const value = ['startDate', 'endDate'].includes(key) ? new Date(params[key]) : params[key];
		// データストアのインスタンスにセット
		schedule.set(key, value);
	}
	// データストアへの保存
	await (schedule.objectId ? schedule.update() : schedule.save());
	// カレンダー画面に戻る
	$f7router.back();
};
```

## 予定の削除

予定を削除するのは `delete` メソッドになります。確認ダイアログを出して、データを削除します。

```js
// 予定の削除処理
const deleteSchedule = () => {
	// 確認ダイアログを表示
	app.dialog.confirm('削除しますか？', () => {
		// 予定の削除
		schedule.delete();
		// カレンダー画面に戻る
		$f7router.back();
	});
};
```

## まとめ

ここまでの流れでカレンダーアプリの完成です。NCMBのデータストアでは日付や文字列（他にも数字や配列、真偽値、オブジェクト、位置情報）など様々な形式でデータの保存・取得ができます。ぜひ柔軟に皆さんのアプリに活かしてください。

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)