ブラウザ上でカレンダーを使ってスケジュールを管理したい場合、ネット上に公開されているカレンダーライブラリを利用するかGoogle Calendarを利用するしか選択肢がないって決め付けていませんか?

自作のカレンダー作成はJavaScriptの入門者の人にとってハードルが高いように思うかもしれませんが作り方のベースさえ理解することができれば実はそれほど難しいものではありません。本文書ではJavaScript+Vue.jsを利用してスクラッチから自作カレンダーを作成していきます。

自作のカレンダーの作成方法を理解することができればGoogleカレンダーやライブラリにはない自分だけのオリジナルの機能をもったカレンダーアプリケーションを作成することができます。

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

カレンダーの作成

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

vue.jsの環境

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


 % vue create vue3-calendar
 % cd vue3-calendar
 % npm install moment 
 % npm run serve
ここではVue3の環境を利用して構築しています。Options APIを利用しており、Composition APIは利用していません。
fukidashi

カレンダーコンポーネントの作成

インストールディレクトリのsrc¥componentsの下にCalendar.vueファイルを作成します。

作成後App.vueファイルを下記のように更新し、Calendarコンポーネントをimportします。


<template>
  <Calendar />
</template>

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

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

Calendar.vueファイルも以下のように記述し、ブラウザ上にHello Calendarが表示されることを確認します。Hello Worldが正常に表示されたらCalendarコンポーネントが正常にimportされていることが確認できます。


<template>
    <p>Hello World</p>
</template>
<script>
export default {

}
</script>
Hello World表示
Hello World表示

カレンダーの表示

Step by Stepでカレンダーを作成していきますがカレンダー作成のルールには以下の2つがあります。

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

下記の図であれば1月のカレンダーで、カレンダーの最初の日はDecember(12月)の30の日曜日、最後の日はFebruary(2月)の2の土曜日となります。

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

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

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

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

moment.jsを使って9月1日の曜日を出します。startOfメソッドの引数に”month”を入れるとその月の最初の日を取得することができます。dayメソッドでは曜日を出すことができます。 曜日は数字で表示され日曜日の0が基準になります。9月1日は2と表示され、火曜日ということがわかります。動作確認ためライフサイクルフックのmountedを利用して曜日を確認しています。


<template>
    <p>Hello World</p>
</template>
<script>
import moment from "moment";
export default {
  mounted(){
    let date = moment().startOf("month");
    const youbiNum = date.day();
    console.log(youbiNum)
  } 
}
</script>
コードを実行する日によって表示される曜日の番号は異なります。2020年9月の場合はブラウザのデベロッパーツールのコンソールに2と表示されます。
fukidashi

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

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

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


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");
    },
  },
  mounted(){
    console.log(this.getStartDate())
  } 
}
moment(this.currentDate)を実施することでthis.currentDateのコピーを行っています。this.currntDate.clone()でも同様の操作を行うことができます。
fukidashi

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


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");
    },
    getEndDate() {
      let date = moment(this.currentDate);
      date.endOf("month");
      const youbiNum = date.day();
      return date.add(6 - youbiNum, "days");
    },
  },
  mounted(){
    console.log(this.getStartDate())
    console.log(this.getEndDate())
  } 
}

ブラウザのデベロッパーツールのコンソールには実行した日のその月の最初の日曜日と最後の土曜日の情報が表示されます。

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

最初の日曜日と最後の土曜日が取得できるようになったのでここからブラウザにカレンダーを表示させるコードを記述していきます。

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

2015年2月のカレンダー
2015年2月のカレンダー

下記の図の場合は、1月のカレンダーですが、高さは5(5週)ということになります。この場合は5週分が表示されていることがわかります。このようにカレンダーの高さは表示する月によって変動することになります。

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

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


mounted(){
  const startDate = this.getStartDate();
  const endDate = this.getEndDate();
  const weekNumber = Math.ceil(endDate.diff(startDate, "days") / 7);
  console.log(weekNumber) //5と表示される
} 
実行した月によってweekNumberの結果は異なります。
fukidashi

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

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


  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;
  },
},
mounted(){
  console.log(this.getCalendar());
} 

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

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

この配列を展開することでブラウザ上にカレンダーを表示させます。

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

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


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

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


<template>
  <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>
</template>

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

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

これにCSSを適用してカレンダーらしく表示させます。ボーダーを適切に設定しなければ線が重なってきれいにみれないので理解しやすいように場所毎に異なるボーダーの色を設定しています。これでどの要素の設定がどの場所のボーターに影響を与えるのがわかるかと思います。


<template>
  <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>
</template>

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

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

どの要素のCSSのボーダーがどの場所に適用されるのか理解できたらすべてのボーダーカラーをグレーの1pxに変更します。


<div style="max-width:900px;border-top:1px solid gray;">
  <div
    v-for="(week, index) in calendars"
    :key="index"
    style="display:flex;border-left:1px solid gray"
  >
    <div
      v-for="(day, index) in week"
      :key="index"
      style="
          flex:1;min-height:125px;
          border-right:1px solid gray;
          border-bottom:1px solid gray
        "
    >
      {{ day.date }}
    </div>
  </div>
</div>

線を統一しただけでかなりカレンダーらしくなりました。

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

別の月のカレンダー

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");
},
prevMonth() {
    this.currentDate = moment(this.currentDate).subtract(1, "month");
},

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

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

この文書を読む前はカレンダーをどのように作成しているのかわからなかった人もいるかと思いますがここで説明したように複雑なコードを使うことなくカレンダーを作成することができました。どのような仕組みで表示させているかがわかればそれほど難しいものではないことがわかってもらえたかと思います。アプリケーションではカレンダーの枠を作成しただけではなくカレンダーの中にイベントを表示させるのは必須です。イベントの表示方法、インタラクティブはカレンダーのイベント操作の設定方法について下記の文書で詳細に説明しています。