LoginSignup
17
18

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-27

前置き

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

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

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

完成品

FireShot Capture 070 - vue_calender_component - localhost.png
んで、todoがある日付を選択すると、
FireShot Capture 072 - vue_calender_component - localhost.png
todoリストが出る仕組み。

まずはじめに

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

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;

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

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にて、まずは今日の日付と、今月の値を取得。

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

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


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

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

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の状態を監視。

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の更新を行なっております。

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

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

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

<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に初めて投稿しましたが、
わかりやすく書くのってめちゃくちゃむずい。

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

17
18
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
17
18