Vue.jsでカレンダーコンポーネント作ってみた

###前置き
カレンダーと一言で言っても、機能も様々なので、
ひとまず機能もシンプルで、あまりごちゃごちゃさせてないものを紹介します。
**vue**と**moment**のみで作っています。

下記が主な機能：
**1.<>ボタンで月単位での移動ができる**
**2.選択状態の概念があること（表示時は現在日付）**
**3.日付ごとにその日のTodoを表示できる**

結構スタイルに依存するので、簡単なcssも記述します。

###完成品
![FireShot Capture 070 - vue_calender_component - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469924/57164ec7-ad74-c1b3-a7b0-23bde92e2ec3.png)
んで、todoがある日付を選択すると、
![FireShot Capture 072 - vue_calender_component - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469924/18bcbcae-be9d-a937-2596-a04073eba5f7.png)
todoリストが出る仕組み。

###まずはじめに
todoリストを別ファイルで作ってみます。
実際こういうコンポーネントを使うアプリを作る場合は、
だいたいDBから取得するパターンが多いと思います。
今回は簡易的に定数ファイル的なノリで作成。

```javascript:todoList.js
const TODO_LIST = [
  {
    id: 1,
    title: "起床",
    description: "きっとねむい",
    date: "2019-07-11",
    time: "09:00",
  },
  {
    id: 2,
    title: "出勤",
    description: "まだねむい",
    date: "2019-07-11",
    time: "10:00",
  },
  {
    id: 3,
    title: "打ち合わせ",
    description: "",
    date: "2019-07-12",
    time: "11:00",
  },
  {
    id: 4,
    title: "作業",
    description: "なにやろうかななにやろうかな",
    date: "2019-07-12",
    time: "15:00",
  },
  {
    id: 5,
    title: "お風呂",
    description: "温度は43度",
    date: "2019-07-12",
    time: "19:00",
  },
  {
    id: 6,
    title: "カレンダー作り",
    description: "つくるぞつくるぞ",
    date: "2019-07-13",
    time: "14:00",
  },
];

export default TODO_LIST;
```

続いて、カレンダーのカレンダーの部分。

```vue:Calender.vue
<template>
  <div class="calender">
    <div class="calender-component">
      <div class="calender-header">
        <div class="arrow-back" v-on:click="changeMonth(0)"><</div>
        <div class="current-date">{{ dateLabel }}</div>
        <div class="arrow-next" v-on:click="changeMonth(1)">></div>
      </div>
      <div class="calender-body">
        <ul class="calender-panel-list">
          <li class="calender-panel_space" v-for="space in spaces"></li>
          <li
            class="calender-panel"
            v-on:click="selectDate"
            v-for="date in dates"
            :id="date.date"
            v-bind:class="selectedDate === date.date ? 'selected' : ''"
          >
            <div class="calender-date">{{ date.dateNumber }}</div>
            <div
              class="calender-todo"
              v-bind:class="date.todoNumber !== '-' ? 'number' : ''"
            >{{ date.todoNumber }}</div>
          </li>
        </ul>
      </div>
      <div class="calender-footer">
        <div
          class="calender-footer_todo"
          v-for="todo in todoList"
          v-show="todo.date === selectedDate"
        >
          <div class="calender-footer_todo-time">{{ todo.time }}</div>
          <div class="calender-footer_todo-title">{{ todo.title }}</div>
          <div class="calender-footer_todo-description">{{ todo.description }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import moment from "moment";
import TODO_LIST from "../data/todoList";

export default {
  mixins: [TODO_LIST],
  data() {
    return {
      todoList: TODO_LIST, //todoリスト
      dates: [], //カレンダーの日付
      spaces: [], //その月の最初日が始まる場所
      dateLabel: "", //フォーマット="2019年7月"
      selectedMonth: null, //今選択している月
      selectedDate: null //今選択している日付
    };
  },
  methods: {
    selectDate(event) {
      //日付を選択する
      this.selectedDate = event.currentTarget.id;
    },
    changeMonth(num) {
      //月を変更
      if (num === 0) {
        this.selectedMonth = moment(this.selectedMonth).subtract(1, "months");
      } else {
        this.selectedMonth = moment(this.selectedMonth).add(1, "months");
      }
    }
  },
  created() {
    //画面表示時に今日の日付と月を設定。
    this.selectedDate = moment().format("YYYY-MM-DD");
    this.selectedMonth = moment();
  },
  watch: {
    selectedMonth: function() {
      //選択している月の変更時の処理、画面表示時も動きます
      this.dateLabel = moment(this.selectedMonth).format("YYYY年MM月"); //月ラベルを更新
      this.spaces = []; //スペースを初期化
      for (
        let i = 0;
        i <
        moment(this.selectedMonth)
          .startOf("month")
          .day();
        i++
      ) {
        //スペースを更新
        this.spaces[i] = i;
      }

      this.dates = []; //カレンダーパネルを初期化
      for (let i = 0; i < moment(this.selectedMonth).daysInMonth(); i++) {
        //カレンダーパネルを更新
        let todoNumber = "-";
        for (let k of Object.keys(this.todoList)) {
          //todoListの情報をカレンダーパネルに追加
          if (this.dates[i]) {
            if (this.todoList[k].date === this.dates[i].date) {
              todoNumber++;
            }
          }
        }
        this.dates[i] = {
          date: moment(this.selectedMonth)
            .startOf("month")
            .add(i, "day")
            .format("YYYY-MM-DD"),
          dateNumber: i + 1,
          todoNumber: todoNumber
        };
      }
    }
  }
};
</script>
```

ざっくりとした解説をしていきます。
createdにて、まずは今日の日付と、今月の値を取得。

```javascript
this.selectedDate = moment().format('YYYY-MM-DD');
this.selectedMonth = moment();
```

methodsは二つ。
**1.選択している日付を変更。**

```javascript

selectDate(event) { //日付を選択する
  this.selectedDate = event.currentTarget.id
},
```

**2.選択している月を変更。**

```javascript
changeMonth(num) { //月を変更
  if(num === 0) {
    this.selectedMonth = moment(this.selectedMonth).subtract(1, 'months');
  } else {
    this.selectedMonth = moment(this.selectedMonth).add(1, 'months');
}
```
"<"には0、">"では1を返しております。

watchではselectedMonthの状態を監視。

```javascript
selectedMonth: function() { //選択している月の変更時の処理、画面表示時も動きます
  this.dateLabel = moment(this.selectedMonth).format('YYYY年MM月'); //月ラベルを更新

  this.spaces = []; //スペースを初期化
  for(let i = 0; i < moment(this.selectedMonth).startOf('month').day(); i++) { //スペースを更新
    this.spaces[i] = i;
  }
      
  this.dates = []; //カレンダーパネルを初期化
  for(let i = 0; i < moment(this.selectedMonth).daysInMonth(); i++) { //カレンダーパネルを更新
    let todoNumber = '-';
    for(let k of Object.keys(this.todoList)) { //todoListの情報をカレンダーパネルに追加
      if(this.todoList[k].date === this.dates[i].date) {
        todoNumber++;
      }
    }
    this.dates[i] = {
      date: moment(this.selectedMonth).startOf('month').add(i, 'day').format('YYYY-MM-DD'),
      dateNumber: i + 1,
      todoNumber: todoNumber
    }
  }
}
```

ここで、選択されてる月(selectedMonth)の日数と、最初の日が何曜日かの計算と、todoListの情報も各日付に入れてます。
spacesとdatesの更新を行なっております。

```javascript
this.spaces = [0]; //1日が火曜日の場合
this.spaces = [0, 1, 2, 3]; //1日が金曜日の場合
```

selectedMonthが変更されるたびに、watchの処理が走るので、
それに合わせてカレンダーも更新されます。

スタイルも乗せときます。

```html
<style scoped>

.calender {
  width: 336px;
}

.calender-component {
  min-height: 320px;
  border: solid 1px gray;
  padding: 24px;
  box-sizing: border-box;
}

.calender-header {
  display: flex;
  justify-content: space-between;
}

.arrow {
  font-size: 16px;
  color: gray;
  user-select: none;
  cursor: pointer;
  text-align: center;
  width: 24px;
  height: 24px;
}

.arrow:hover {
  background-color: silver;
  border-radius: 4px;
}

.current-date {
  color: gray;
  user-select: none;
}

.calender-body {
  margin-top: 24px;
}

.calender-panel-list {
  display: flex;
  flex-wrap: wrap;
  padding: 0;
  justify-content: left;
}

.calender-panel {
  padding: 8px 0px;
  width: 40px;
  text-align: center;
  color: gray;
  font-size: 14px;
  user-select: none;
  list-style: none;
}

.calender-panel:hover {
  background-color: silver;
  border-radius: 4px;
}

.calender-date {
  cursor: pointer;
}

.selected {
  background-color: silver;
  border-radius: 4px;
}

.calender-todo {
  cursor: pointer;
  text-align:center;
  margin: 0px 8px;
  line-height: 25px;
}

.calender-panel_space {
  width: 40px;
  list-style: none;
}

.calender-footer_todo {
  border-top: 1px gray solid;
  padding-top: 12px;
  margin-top: 8px;
}

.calender-footer_todo-time {
  font-size: 18px;
  color: gray;
  word-break: break-word;
  text-decoration: underline;
}

.calender-footer_todo-title {
  font-size: 20px;
  color: gray;
  word-break: break-word;
}

.calender-footer_todo-description {
  font-size: 14px;
  color: gray;
  word-break: break-word;
}

.number {
  text-decoration: underline;
}

</style>
```

一行に日付が7日(曜日分)表示させるのは、スタイルで制御しちゃってます。

###まとめと振り返り
実際にこれを使用したプロジェクトでは、1時間ぐらいで作ったので、
ところどころ直して、自分の備忘録として書いてみました。

ほどよくライフサイクルとディレクティブを使ってたりするので、
Vue初心者の学習にちょうどいいかもしれないです。

input type="date"コンポーネントの自作とかも、
これをベースに作れそうですよね。

ただ、スタイル依存の部分が多いという点で、
汎用性に怪しさを感じました。

あとqiitaに初めて投稿しましたが、
わかりやすく書くのってめちゃくちゃむずい。

気づいたところがあれば、都度更新していきたいです。