LoginSignup
2
5

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-07-23

背景

私が以前、作成したWebアプリでカレンダー機能が必要となったが、

  • 指定した日付に任意のhtmlを組み込む
  • カレンダーのサイズや色などを自由に変更可能

という条件を満たすカレンダーコンポーネントを見つけることができなかった。
そのため、作ってみた。

(以前作成したWebアプリはこちら)

作ったもの

以下が、ソースコードである。
src/components/Calendar.vueが該当のカレンダーコンポーネントである。

例えば、下記のようなカレンダーが作成できる。
image.png

仕様

プロパティ一覧

プロパティ名 デフォルト値 説明
year Number 現在日時の年。 取得したいカレンダーの年。
month Number 現在日時の月。 取得したいカレンダーの月。
weekHeight String "60px" カレンダーの曜日部分の各マスの高さ。
itemMinHeight String "300px" カレンダーの日付部分の各マスの最小の高さ。
※高さを固定にしたい場合、「itemMaxHeight」にも同じ値を入れる。
itemMaxHeight String "300px" カレンダーの日付部分の各マスの最大の高さ。
※高さを固定にしたい場合、「itemMinHeight」にも同じ値を入れる。
itemMinWidth String "300px" カレンダーの日付部分の各マスの最小の幅。
※幅を固定にしたい場合、「itemMaxWidth」にも同じ値を入れる。
itemMaxWidth String "300px" カレンダーの日付部分の各マスの最大の幅。
※幅を固定にしたい場合、「itemMinWidth」にも同じ値を入れる。
outsideDateColor String "gray" カレンダーの最初と最後の、該当月外の日付部分の色。
weekNames Array [
"SUN",
"MON",
"TUE",
"WED",
"THU",
"FRY",
"SAT"
]
カレンダーの曜日部分の表示内容。日曜日から順に配列に入れる。
null値を入れた箇所には、デフォルトの表示で置き換えられる。
saturdayColor String "blue" カレンダーの曜日の「土曜日」に該当する文字のフォント色。
sundayColor String "red" カレンダーの曜日の「日曜日」に該当する文字のフォント色。
weekdayColor String "black" カレンダーの曜日の「土曜日」、「日曜日」以外に該当する文字のフォント色。
weekFontSize String "26px" カレンダーの曜日の文字のフォントサイズ。
dateColor String "black" カレンダーの日付の文字のフォント色。
dateFontSize String "16px" カレンダーの日付の文字のフォントサイズ。
borderColor String "black" カレンダーの線の色。
borderStyle String "double" カレンダーの線のスタイル。
borderWidth String "4px" カレンダーの線の幅。
todayBorderColor String - 現在の日付が表示月の中にあれば、指定した色で囲む。
本プロパティを未設定の場合、現在の日付があっても、囲まれない。
backgroundColor String "transparent" カレンダー全体の背景色。

templateの組み込み方

カレンダーの任意の日付のマスに、templateを組み込みたい場合、v-slotを使用する。

<template v-slot:●●●>
  ※任意のテンプレート
</template>

上記の、●●●の部分に、テンプレートを組み込みたい日付(1日なら、1)を記載する。
またv-slotにて、指定されなかった日付全てを一括で設定したい場合、defaultを使用する。

<template v-slot:default>
  ※任意のテンプレート
</template>

コンポーネント呼び出し方の例

    <Calendar
      outsideDateColor="rgb(70,70,70)"
      itemMinWidth="290px"
      itemMaxWidth="290px"
      itemMinHeight="200px"
      itemMaxHeight="200px"
      weekFontSize="40px"
      weekdayColor="white"
      dateColor="white"
      dateFontSize="24px"
      borderStyle="double"
      borderWidth="6px"
      borderColor="white"
      todayBorderColor="yellow"
      backgroundColor="black"
    >

        <!-- 休日だけ、「休暇です」と赤字で表示する -->
        <template v-for="element in [3,4,10,11,17,18,24,25,31]" v-slot:[element]>
          <div :key="element">
            <p style="color: white; color:red">休暇です。</p>
            </div>
        </template>

        <!-- 休日以外は、「スケジュール入力テキストボックス」と、「ボタン」を設置する -->
        <template v-slot:default style="color:white">
          <p style="color:white">スケジュールを入力してね!</p><input type="text" />
          <button>Click!</button>
          </template>

    </Calendar>

注意事項

itemMaxWidth、itemMinWidth、itemMaxHeight、itemMinHeightの設定値によっては、レイアウトが崩れることがあります。
調整してください。

最後に

私がWebアプリを作ったときには、上手くコンポーネント化できず、べた書きしていた。
Webアプリの仕様にバリバリ依存したカレンダーになっていた。
上手にコンポーネント化することは、再利用性の向上や可読性の向上など様々なメリットをうむ。
ちょっと面倒くさくても、将来性を考えて、どんどんコンポーネント化しよう!

templateの組み込みについては、このやり方で良かったのか気になっている。
v-htmlを使うことも考えていたが、XSSの危険性があるため避けた。
もっといいやり方があったら、教えていただけますと幸いです。

Calendar.vue

コピペ用に記載しておきます。
長いのでスルーでおkです。

Calendar.vue
<template>
  <div>
    <div
      style="display: flex"
      :style="{
        'border-left-color': borderColor,
        'border-left-style': borderStyle,
        'border-left-width': borderWidth,
        height: weekHeight,
      }"
    >
      <div
        v-for="(day, index) in [
          weekNames[0] || 'SUN',
          weekNames[1] || 'MON',
          weekNames[2] || 'TUE',
          weekNames[3] || 'WED',
          weekNames[4] || 'THU',
          weekNames[5] || 'FRY',
          weekNames[6] || 'SAT',
        ]"
        :key="index"
        :style="{
          'min-width': itemMinWidth,
          'max-width': itemMaxWidth,
          'font-size': weekFontSize,
          'border-top-color': borderColor,
          'border-top-style': borderStyle,
          'border-top-width': borderWidth,
          'border-right-color': borderColor,
          'border-right-style': borderStyle,
          'border-right-width': borderWidth,
          'border-bottom-color': borderColor,
          'border-bottom-style': borderStyle,
          'border-bottom-width': borderWidth,
          'background-color': backgroundColor,
        }"
        class="days-style"
      >
        <div v-if="index === 0" :style="{ color: sundayColor }">
          {{ day }}
        </div>
        <div v-else-if="index === 6" :style="{ color: saturdayColor }">
          {{ day }}
        </div>
        <div v-else :style="{ color: weekdayColor }">
          {{ day }}
        </div>
      </div>
    </div>
    <div style="display: flex; flex-direction: column">
      <div
        v-for="(week, index) in calendars"
        :key="index"
        style="display: flex"
        :style="{
          'min-height': itemMinHeight,
          'max-height': itemMaxHeight,
          'border-left-color': borderColor,
          'border-left-style': borderStyle,
          'border-left-width': borderWidth,
        }"
      >
        <div
          v-for="(item, index) in week"
          :key="index"
          style="position: relative"
          :style="{
            'min-width': itemMinWidth,
            'max-width': itemMaxWidth,
            'border-right-color': borderColor,
            'border-right-style': borderStyle,
            'border-right-width': borderWidth,
            'border-bottom-color': borderColor,
            'border-bottom-style': borderStyle,
            'border-bottom-width': borderWidth,
            'background-color': backgroundColor,
          }"
        >
          <div
            v-if="todayBorderColor && item.isToday"
            class="today-style"
            :style="{
              'border-color': todayBorderColor,
              'border-width': borderWidth,
              width: 'calc(100% - (' + borderWidth + ' * 2))',
              height: 'calc(100% - (' + borderWidth + ' * 2))',
            }"
          />
          <div
            v-if="item.isOut"
            style="height: 100%; width: 100%; z-index: 1"
            :style="{
              'background-color': outsideDateColor || gray,
            }"
          >
            <div
              :style="{
                color: dateColor,
                'font-size': dateFontSize,
              }"
            >
              {{ item.date }}
            </div>
          </div>
          <div v-else style="height: 100%; width: 100%; z-index: 1">
            <div
              :style="{
                color: dateColor,
                'font-size': dateFontSize,
              }"
            >
              {{ item.date }}
            </div>
            <div
              style="width: 100%"
              :style="{
                height: 'calc(100% - ' + dateFontSize + ')',
              }"
            >
              <slot v-if="$scopedSlots[item.date]" :name="item.date"></slot>
              <slot v-else></slot>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import moment from "moment";

export default {
  props: {
    year: {
      type: Number,
      default: Number(moment().get("years")),
    },
    month: {
      type: Number,
      default: Number(moment().get("months")) + 1,
    },
    weekHeight: {
      type: String,
      default: "60px",
    },
    itemMinWidth: {
      type: String,
      default: "300px",
    },
    itemMaxWidth: {
      type: String,
      default: "300px",
    },
    itemMinHeight: {
      type: String,
      default: "300px",
    },
    itemMaxHeight: {
      type: String,
      default: "300px",
    },
    outsideDateColor: String,
    weekNames: {
      type: Array,
      default: () => [],
    },
    saturdayColor: {
      type: String,
      default: "blue",
    },
    sundayColor: {
      type: String,
      default: "red",
    },
    weekdayColor: {
      type: String,
      default: "black",
    },
    weekFontSize:  {
      type: String,
      default: "26px",
    },
    dateColor: {
      type: String,
      default: "black",
    },
    dateFontSize: {
      type: String,
      default: "16px",
    },
    borderColor: {
      type: String,
      default: "black",
    },
    borderStyle: {
      type: String,
      default: "double",
    },
    borderWidth: {
      type: String,
      default: "4px",
    },
    todayBorderColor: String,
    backgroundColor: {
      type: String,
      default: "transparent",
    },
  },
  data() {
    return {
      today: moment(),
      yearMonth: null,
    };
  },
  mounted() {
    this.yearMonth = moment({
      years: this.year,
      months: this.month - 1,
    });
  },
  methods: {
    getStartDate() {
      let date = moment(this.yearMonth);
      date.startOf("month");
      const youbiNum = date.day();
      return date.subtract(youbiNum, "days");
    },
    getEndDate() {
      let date = moment(this.yearMonth);
      date.endOf("month");
      const youbiNum = date.day();
      return date.add(6 - youbiNum, "days");
    },
  },
  computed: {
    calendars() {
      if (!this.yearMonth) return [];
      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++) {
          const isOut = Number(startDate.get("month")) !== this.month - 1;
          let item = {
            date: startDate.get("date"),
            isOut: isOut,
            isToday: !isOut && this.today.isSame(startDate, "day"),
          };
          weekRow.push(item);
          startDate.add(1, "days");
        }
        calendars.push(weekRow);
      }
      return calendars;
    },
  },
};
</script>

<style scoped>
.days-style {
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}
.today-style {
  position: absolute;
  border-style: solid;
}
</style>
2
5
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
2
5