Vuetifyで簡単にカレンダーを表示する

CSSやJavascriptでカレンダーを作るの、難しくないですか？
Vuetifyのカレンダーコンポーネントを使うと簡単にカレンダーを作ることができたので、作り方を紹介したいと思います

## ゴール
↓のようなカレンダーを作ります
https://vuetify-calendar-sample.netlify.app/
![スクリーンショット 2020-08-15 21.27.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145075/4a81de45-e24e-925b-a25d-5ab2789a3aa9.png)

[ソースコード](https://github.com/youichiro/vuetify-calendar-sample)

## Vuetify
[Vuetify](https://vuetifyjs.com/ja/)はVueで使えるUIライブラリの一つです
自分でCSSを記述することなく美しいUIを作ることができます
今回は[Vue CLI](https://cli.vuejs.org/)を使って簡単なカレンダーのデモを作成していきます


## 環境
- Node.js v14.8.0
- VueCLI 4.5.3

### nodeのインストール
nodenvを使ってnodeをインストールしました。nodenvのインストールは[こちら](https://github.com/nodenv/nodenv)を参考にしてください

```bash
$ nodenv install 14.8.0
$ nodenv global 14.8.0
$ nodenv local 14.8.0
$ node -v
v14.8.0
```

### VueCLIのインストール
[公式ガイド](https://cli.vuejs.org/guide/installation.html)を参考にインストールします

```bash
$ npm install -g @vue/cli@4.5.3
$ vue --version
@vue/cli 4.5.3
```

## プロジェクトの作成

VueCLIコマンドでプロジェクトを作成します

```bash
$ vue creare calendar-app
```

プリセットはDefault(Vue2)を選択しました (Vue3で試してみましたがVuetifyのインストール時にエラーが出ました)
![スクリーンショット 2020-08-15 17.32.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145075/6f2419af-b3dc-4e62-1862-76d837e9bcf5.png)


## Vuetifyのインストール

プロジェクトディレクトリに移動してVuetifyをインストールします

```bash
$ cd calendar-app
$ vue add vuetify
```

プリセットはDefaultを選択しました
![スクリーンショット 2020-08-15 17.33.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145075/26fb60c1-2c67-3468-7748-3be93e6da0c6.png)
インストールが終わると`src/main.js`にVuetifyの設定が自動で記述されます

```diff
 import Vue from 'vue'
 import App from './App.vue'
+import vuetify from './plugins/vuetify';

 Vue.config.productionTip = false

 new Vue({
-  render: h => h(App),
+  vuetify,
+  render: h => h(App)
 }).$mount('#app')
```


ここまでで表示してみると下の画像のようにVuetifyのウェルカム画面が表示されます

```bash
npm run serve
# http://localhost:8080/ を開く
```

![スクリーンショット 2020-08-16 2.19.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145075/ca884e8d-0c51-373f-f7e3-260f083c2c89.png)

## カレンダーを表示する

まず、`src/App.vue`を次のように書き換えます

```vue:src/App.vue
<template>
  <v-app>
    <v-main>
      <Calendar />
    </v-main>
  </v-app>
</template>

<script>
import Calendar from './components/Calendar';

export default {
  name: 'App',
  components: {
    Calendar,
  },
};
</script>
```

`v-app`コンポーネントで囲うことを忘れてしまうとVuetifyのスタイルが適用されないので注意です [参考](https://vuetifyjs.com/ja/components/application/)

次に、`src/components/Calendar.vue`を作成します
[公式ドキュメントのサンプル](https://vuetifyjs.com/ja/components/calendars/#usage)を見つつ、最低限表示できるところまでコードを削ぎ落としました

```vue:src/components/Calendar.vue
<template>
  <div>
    <v-sheet tile height="6vh" color="grey lighten-3" class="d-flex align-center">
      <v-btn icon @click="$refs.calendar.prev()">
        <v-icon>mdi-chevron-left</v-icon>
      </v-btn>
      <v-btn icon @click="$refs.calendar.next()">
        <v-icon>mdi-chevron-right</v-icon>
      </v-btn>
    </v-sheet>
    <v-sheet height="94vh">
      <v-calendar
        ref="calendar"
        :events="events"
        :event-color="getEventColor"
        @change="getEvents"
      ></v-calendar>
    </v-sheet>
  </div>
</template>

<script>
export default {
  data: () => ({
    events: [],
  }),
  methods: {
    getEvents() {
      const events = [
        {
          name: '会議',
          start: new Date('2020-08-03T01:00:00'), // 開始時刻
          end: new Date('2020-08-03T02:00:00'), // 終了時刻
          color: 'blue',
          timed: true, // 終日ならfalse
        },
      ];
      this.events = events;
    },
    getEventColor(event) {
      return event.color;
    },
  },
};
</script>
```

今回はデモ用なので直接イベントデータを記述していますが、APIからデータを取得する処理に書き換えることもできます
このようなカレンダーが表示されます

![スクリーンショット 2020-08-16 2.46.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145075/2b17ee56-d8cc-f2f1-8d9c-5acec3c595e6.png)

```vue
<v-calendar
  ref="calendar"
  :events="events"
  :event-color="getEventColor"
  @change="getEvents"
></v-calendar>
```

`:events="events"`でカレンダーに表示するイベントデータを渡しています
`events`変数は最初は空配列ですが、`@change="getEvents"`で値を代入しています

```js
const events = [
  {
    name: '会議',
    start: new Date('2020-08-03T01:00:00'),
    end: new Date('2020-08-03T02:00:00'),
    color: 'blue',
    timed: true,
  },
];
```

これがカレンダーに表示されるイベントの中身です
`start`, `end`は開始時刻と終了時刻であり、Dateオブジェクトを指定します
`timed`には時刻を表示する時は`true`、表示しない(終日)の時は`false`を指定します
配列にイベントオブジェクトを追加していくことでカレンダーに表示されるイベントが増えていきます

ここから機能を追加していきます


## 年月を表示する
表示しているカレンダーの年月を表示するようにします
[Moment.js](https://momentjs.com/)を使うので先にインストールしておきます

```bash
$ npm install moment --save
```

```diff:src/components/Calendar.vue
<template>
  <div>
    <v-sheet tile height="6vh" color="grey lighten-3" class="d-flex align-center">
      <v-btn icon @click="$refs.calendar.prev()">
        <v-icon>mdi-chevron-left</v-icon>
      </v-btn>
      <v-btn icon @click="$refs.calendar.next()">
        <v-icon>mdi-chevron-right</v-icon>
      </v-btn>
+     <v-toolbar-title>{{ title }}</v-toolbar-title>
    </v-sheet>
  </div>
</template>

<script>
+ import moment from 'moment';

export default {
  data: () => ({
    events: [],
+   value: moment().format('yyyy-MM-DD'),  // 現在日時
  }),
+ computed: {
+   title() {
+     return moment(this.value).format('yyyy年 M月');  // 表示用文字列を返す
+   }
+ },
  methods: {
    ...
  },
};
</script>

```

`value`変数には`2020-08-01`のような形式の日付文字列が入ります
初期値は現在の日付です
`title()`で表示形式を変換して表示しています

## 月を戻す/進める

`v-calendar`に`v-model="value"`を設定します
これだけで、ボタンを押すと戻る/進むできるようになります

```diff:src/components/Calendar.vue
<v-calendar
  ref="calendar"
+ v-model="value"
  :events="events"
  :event-color="getEventColor"
  @change="getEvents"
></v-calendar>
```

例えば戻るボタンを押すと`$refs.calendar.prev()`が実行されます
これが実行されると`value`に先月の末日が代入されます
元が`2020-08-15`だったら`2020-07-31`が入ります
一方で`$refs.calendar.next()`は次月の初日である`2020-08-01`が入ります
従って`value`に表示させたい月の日付を代入することでカレンダーを切り替えることができます


## 今日ボタンを追加

```diff:src/components/Calendar.vue
<template>
  <div>
    <v-sheet tile height="6vh" color="grey lighten-3" class="d-flex align-center">
+     <v-btn outlined small class="ma-4" @click="setToday">
+       今日
+     </v-btn>
      <v-btn icon @click="$refs.calendar.prev()">
        <v-icon>mdi-chevron-left</v-icon>
      </v-btn>
      ...
</template>

<script>
  ...
  methods: {
+   setToday () {
+       this.value = moment().format('yyyy-MM-DD')
+   },
    ...
</script>
```

今月のカレンダーに戻りたい時は`value`に今日の日付を代入すればできます👍


## 日本語表記
曜日の表示がSUNやMONのように英語表記になっているので、日本語表記に変えます

```diff:src/components/Calendar.vue
<v-calendar
  ref="calendar"
  v-model="value"
  :events="events"
  :event-color="getEventColor"
+ locale="ja-jp"
+ :day-format="(timestamp) => new Date(timestamp.date).getDate()"
+ :month-format="(timestamp) => (new Date(timestamp.date).getMonth() + 1) + ' /'"
  @change="getEvents"
></v-calendar>
```

これだけで日本語の曜日表記になります
![スクリーンショット 2020-08-16 5.01.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145075/6a1f74f6-9d92-707f-1142-8528ada4ed00.png)


## イベントのクリック処理、日付のクリック処理
イベントをクリックした時、日付をクリックした時の処理の書き方です
ここでは簡単のため`alert`を実行しています

```diff:src/components/Calendar.vue
<template>
  ...
      <v-calendar
        ref="calendar"
        v-model="value"
        :events="events"
        :event-color="getEventColor"
        locale="ja-jp"
        :day-format="(timestamp) => new Date(timestamp.date).getDate()"
        :month-format="(timestamp) => new Date(timestamp.date).getMonth() + 1 + ' /'"
        @change="getEvents"
+       @click:event="showEvent"
+       @click:date="viewDay"
      ></v-calendar>
  ...
</template>

<script>
  ...
  methods: {
    setToday() {
      this.value = moment().format('yyyy-MM-DD');
    },
+   showEvent({ event }) {
+     alert(`clicked ${event.name}`);
+   },
+   viewDay({ date }) {
+     alert(`date: ${date}`);
+   },
    ...
</script>
```

`@click:event`, `@click:date`にそれぞれ実行させるメソッドを指定します
メソッドの引数に色々なデータを持つオブジェクトが渡されるので、それを使ってイベントの詳細やデイリーカレンダーの表示などを実行できそうです


## 最終的なコード

```vue:src/components/Calendar.vue
<template>
  <div>
    <v-sheet tile height="6vh" color="grey lighten-3" class="d-flex align-center">
      <v-btn outlined small class="ma-4" @click="setToday">
        今日
      </v-btn>
      <v-btn icon @click="$refs.calendar.prev()">
        <v-icon>mdi-chevron-left</v-icon>
      </v-btn>
      <v-btn icon @click="$refs.calendar.next()">
        <v-icon>mdi-chevron-right</v-icon>
      </v-btn>
      <v-toolbar-title>{{ title }}</v-toolbar-title>
    </v-sheet>
    <v-sheet height="94vh">
      <v-calendar
        ref="calendar"
        v-model="value"
        :events="events"
        :event-color="getEventColor"
        locale="ja-jp"
        :day-format="(timestamp) => new Date(timestamp.date).getDate()"
        :month-format="(timestamp) => new Date(timestamp.date).getMonth() + 1 + ' /'"
        @change="getEvents"
        @click:event="showEvent"
        @click:date="viewDay"
      ></v-calendar>
    </v-sheet>
  </div>
</template>

<script>
import moment from 'moment';

export default {
  data: () => ({
    events: [],
    value: moment().format('yyyy-MM-DD'),
  }),
  computed: {
    title() {
      return moment(this.value).format('yyyy年 M月');
    },
  },
  methods: {
    setToday() {
      this.value = moment().format('yyyy-MM-DD');
    },
    showEvent({ event }) {
      alert(`clicked ${event.name}`);
    },
    viewDay({ date }) {
      alert(`date: ${date}`);
    },
    getEvents() {
      const events = [
        // new Dateからmomentに変更
        {
          name: '会議',
          start: moment('2020-08-03 10:00:00').toDate(),
          end: moment('2020-08-03 11:00:00').toDate(),
          color: 'blue',
          timed: true,
        },
        // イベントを追加
        {
          name: '休暇',
          start: moment('2020-08-04').toDate(),
          end: moment('2020-08-04').toDate(),
          color: 'green',
          timed: false,
        },
        {
          name: '出張',
          start: moment('2020-08-05').toDate(),
          end: moment('2020-08-07').toDate(),
          color: 'cyan',
          timed: false,
        },
        {
          name: '飲み会',
          start: moment('2020-08-06').toDate(),
          end: moment('2020-08-06').toDate(),
          color: 'orange',
          timed: false,
        },
        {
          name: '打ち合わせ',
          start: moment('2020-08-07 10:00').toDate(),
          end: moment('2020-08-07 11:00').toDate(),
          color: 'cyan',
          timed: true,
        },
        {
          name: '振り返り',
          start: moment('2020-08-07 11:00:00').toDate(),
          end: moment('2020-08-07 12:00').toDate(),
          color: 'cyan',
          timed: true,
        },
        {
          name: '休暇',
          start: moment('2020-09-07').toDate(),
          end: moment('2020-09-11').toDate(),
          color: 'green',
          timed: false,
        },
      ];
      this.events = events;
    },
    getEventColor(event) {
      return event.color;
    },
  },
};
</script>
```

## まとめ
Vuetifyを使ってカレンダーを表示する方法を紹介しました
自分でCSSやJavascriptを書くよりもずっと少ないコードで実装することができました！
もっとたくさんイベントを追加してみるとわかるのですが、連日のイベントだと横に伸びたり、イベントが多いとmore表示になったりして気が利いています。便利
![スクリーンショット 2020-08-16 5.15.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145075/5faee3b1-4eba-c161-73a0-966248df47ea.png)


## 参考
- [Vuetifyを使ったカレンダーに挑戦](https://qiita.com/marumaruchan/items/6c7dd5afb760fdd3a2ca)
- [VuetifyのCalendarsを日本語化してみる](https://qiita.com/yamanakata1/items/440defea50f87fb18254)