背景
私が以前、作成したWebアプリでカレンダー機能が必要となったが、
- 指定した日付に任意のhtmlを組み込む
- カレンダーのサイズや色などを自由に変更可能
という条件を満たすカレンダーコンポーネントを見つけることができなかった。
そのため、作ってみた。
(以前作成したWebアプリはこちら)
作ったもの
以下が、ソースコードである。
src/components/Calendar.vue
が該当のカレンダーコンポーネントである。
仕様
プロパティ一覧
プロパティ名 | 型 | デフォルト値 | 説明 |
---|---|---|---|
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です。
<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>