JavaScriptでカレンダーを利用したい場合はライブラリを利用するかGoogle Calendarを利用するしかないって決め付けていることはないですか?オリジナルのカレンダーの作成方法がわかればライブラリにはない自分だけの機能をもったカレンダーアプリを作成できるかもしれません。

本文書ではカレンダーを作成する際にvue.jsを利用していますが、vue.jsは必須ではありません。本文書でカレンダー作成の基本がわかればJavaScriptのみでも作成可能です。カレンダー作成後にイベントなどをカレンダーに追加させるためにvue.jsを利用する予定です。

カレンダーの作成

カレンダーの作成には日付ライブラリであるmoment.jsを利用します。それ以外についてはライブラリを利用せずJavaScriptとVue.jsを利用しています。カレンダーの作成の基本部分についてはJavaScriptのみで作成可能です。

vue.jsの環境

vue-cliコマンドを利用して、カレンダー動作確認用のプロジェクトを作成し、moment.jsを利用するためmomentライブラリのインストールも行っています。


 % vue create vue-calendar
 % vue npm install moment 

カレンダーの表示

Step by Stepでカレンダーを作成していきます。

カレンダー作成のルールには以下の2つがあります。

  • カレンダーの左上の一番上は月の最初の日ではなく月の最初の日が含まれている週の日曜日から表示
  • カレンダーの最後の日はその月の最終日ではなく最終日を含む週の土曜日を表示

下記の図であればカレンダーの最初の日はDecemberの30の日曜日、最後の日はFebruaryの2の土曜日です。

vuefityのカレンダーを参考
vuefityのカレンダーを参考
Decemberである12月のカレンダーです。

カレンダーの最初の日と最後の日

本文書を書いている2020年の9月のカレンダーを作成するためにカレンダーに表示させる最初の日と最後の日を取得するコードを記述します。

カレンダーの最初の日を出すために曜日を利用します。

momentを使って9月1日の曜日を出します。曜日は数字で表示され日曜日の0が基準になります。9月1日は2と表示され、火曜日ということがわかります。startOfメソッドの引数に”month”を入れるとその月の最初の日になります。dayメソッドで曜日を出すことができます。


import moment from "moment";
//略
let date = moment().startOf("month");
const youbiNum = date.day();
// 2 2020年9月1日は火曜日です。

9月1日の週の日曜日は9月1日が火曜日なので、9月1日から2日引くと8月30日になることがわかります。日曜日の日付を出すために先ほど計算した曜日の数字を利用します。momentのsubstractメソッドを利用し第一引数に先ほど出した曜日の数字(2)、第二引数は日付の引き算を行うのでdaysを設定します。

第2引数にdaysを入れれば日での引き算、”month”を入れれば月の引き算になります。”month”を使ったsubstractメソッドはカレンダーを別の月に変更する際に利用します。

その月の最初の日の曜日を利用してカレンダーに表示させる最初の日を見つけることができました。


date.subtract(youbiNum, "days");
//Sun Aug 30 2020 00:00:00 GMT+0900 

一連の流れをgetStartDateという関数にしました。データプロパティでカレンダーにアクセスしている日のcurrentDateを追加し利用しています。


import moment from "moment";

export default {
  data() {
    return {
      currentDate: moment(),
    };
  },
  methods: {
    getStartDate() {
      let date = moment(this.currentDate);
      date.startOf("month");
      const youbiNum = date.day();
      return date.subtract(youbiNum, "days");
    },
//略
moment(this.currentDate)を実施することでthis.currentDateのコピーを行っています。this.currntDate.clone()でも同様の操作を行うことができます。

つぎにカレンダーの最終日を出します。こちらも曜日を出すまでは同じですが、最終日を出す場合は日付を引くのではなくその月の最終日からその週の土曜日の日付を出すために6から曜日の数字を引いた数を足します。これでカレンダーの最終日を出すことができます。


getEndDate() {
    let date = moment(this.currentDate);
    date.endOf("month");
    const youbiNum = date.day();
    return date.add(6 - youbiNum, "days");
},

カレンダーの縦の週の数を計算する

カレンダーの横の数は必ず曜日の7つと決まっていますが、何曜日からその月が始まるかによってカレンダーの高さは変動します。例えば2月の1日が日曜日の場合は2月28日は土曜日になり、高さは4(4週)となり画面上には4週分のカレンダーが表示されます。

下記の図の場合は、5の高さ(5週)ということになります。この場合は5週分が表示されます。

vuefityのカレンダーを参考
vuefityのカレンダーを参考

計算は先ほど出したカレンダーの最初の日と最後の日の差を取り7で割ることで高さを決めます。momentのdiffメソッドを使って差の日数を出し、出した数字をMath.ceilで切り上げを行っています。これでカレンダーの高さを求めることができます。上の図の例であれば最初の日が12月30日、最後の日が2月2日でその差の日数を出して7で割ります。


let startDate = this.getStartDate();
const endDate = this.getEndDate();
const weekNumber = Math.ceil(endDate.diff(startDate, "days") / 7);

カレンダー表示用の配列作成

カレンダーを表示させるための準備が整ったのでカレンダーの日付を保存するgetCalendarメソッドを作成します。先ほど計算したカレンダーの高さweekNumber(週の数)でループを行い、その中でさらに1週間分(7)ループをまわします。内側のループ毎に日付を1ずつ追加しています。配列の中にはdateとして日付を登録しています。momentではgetメソッドにdateを指定すると日のみ取得することができます。


getCalendar() {
    let startDate = this.getStartDate();
    const endDate = this.getEndDate();
    const weekNumber = Math.ceil(endDate.diff(startDate, "days") / 7);

    let calendars = [];
    for (let week = 0; week > weekNumber; week++) {
    let weekRow = [];
    for (let day = 0; day > 7; day++) {
        weekRow.push({
        date: startDate.get("date"),
        });
        startDate.add(1, "days");
    }
    calendars.push(weekRow);
    }
    return calendars;
},

実行すると下記のように1つ目の配列には第1週分の7日分のデータ、2つ目の配列に第2週分の7日分のデータが入っています。weekNumberの数と一致する5週分のデータまで入っています。

カレンダーの配列を作成
カレンダーの配列を作成

これを利用してブラウザにカレンダーを表示させます。

ブラウザへのカレンダー表示

カレンダーの表示にはcomputedプロパティを利用します。


  computed: {
    calendars() {
      return this.getCalendar();
    },
  },

先ほど作成したカレンダーの配列を2つのループを利用して展開していきます。ただ展開しただけだと日付が縦に表示されるので週毎に横並びにするためにdisplay:flexを設定します。


<template>
  <div>
    <h2>カレンダー{{ currentDate }}</h2>
    <div v-for="(week, index) in calendars" :key="index" style="display:flex">
      <div v-for="(day, index) in week" :key="index">
        {{ day.date }}
      </div>
    </div>
  </div>
</template>

display:flexのcssしか適用していないのでわかりにくいですが、カレンダーになっていることがわかる??かと思います。

最初のカレンダー表示
最初のカレンダー表示

これにCSSを適用してカレンダーらしく表示させます。ボーダーを適切に設定しなければ線が重なってきれいにみれないので理解しやすいように意図的に設定した場所毎にボーダーの色を変更しています。これでどの部分がどのボーターに対応するかがわかるかと思います。


<template>
  <div>
    <h2>カレンダー{{ currentDate }}</h2>
    <div style="max-width:900px;border-top:5px solid red;">
      <div
        v-for="(week, index) in calendars"
        :key="index"
        style="display:flex;border-left:5px solid green"
      >
        <div
          v-for="(day, index) in week"
          :key="index"
          style="flex:1;min-height:125px;border-right:5px solid blue;border-bottom:5px solid blue"
        >
          {{ day.date }}
        </div>
      </div>
    </div>
  </div>
</template>

2020年のカレンダーを作成することができました!

カレンダーを表示(色付け)
カレンダーを表示(色付け)

色のグレーの1pxに変更します。かなりカレンダーらしくなりました。

グレーに変更したボーター
グレーに変更したボーター

別の月のカレンダー

2020年9月のカレンダーが作成されたので、別の月のカレンダーも表示できるようにclickイベントを設定します。


<h2>カレンダー{{ currentDate }}</h2>
<button @click="prevMonth">前の月</button>
<button @click="nextMonth">次の月</button>

prevMonthとnextMonthメソッドを追加します。


nextMonth() {
    this.currentDate = moment(this.currentDate).add(1, "month");
    this.getCalendar();
},
prevMonth() {
    this.currentDate = moment(this.currentDate).subtract(1, "month");
    this.getCalendar();
},

”次の月”ボタンを何回か押すと2020年12月のカレンダーを確認することができます。

ボタンクリックで別の月に移動
ボタンクリックで別の月に移動

vue.jsを使ったカレンダーのイベントの操作については追加予定です。