Sometimes we need to implement a custom calendar view and with date-fns it's not as scary as you may think.
First, let's see some code:
const currentDate = ref(startOfDay(new Date()));
const days = computed(() => {
const monthStart = startOfMonth(currentDate.value);
const dayNumInWeek = getDay(monthStart);
const calendarStart = subDays(monthStart, dayNumInWeek !== 0 ? dayNumInWeek - 1 : 6);
return eachDayOfInterval({
start: calendarStart,
end: addDays(calendarStart, 41)
}).map((date) => {{
return {
isCurrent: compareAsc(currentDate.value, date) === 0,
isCurrentMonth: date.getMonth() === currentDate.value.getMonth(),
date: date
}
}});
});
It's not hard to understand and looks pretty simple. But if you do not understand it immediately let's explain it step by step:
Firstly we need to get the start and end of the current month:
const monthStart = startOfMonth(currentDate.value);
const monthEnd = endOfMonth(currentDate.value);
Our monthStart and monthEnd is Date objects contains date of start and end date of the month. So we can get which day the current month is start:
const dayNumInWeek = getDay(monthStart)
And now we can use it to get the start date for our calendar:
const calendarStart = subDays(monthStart, dayNumInWeek !== 0 ? dayNumInWeek - 1 : 6);
We need it because we display 7 weeks' days in our calendar and the first day of the current month can be on any day of the week. Sometimes a day first day of the month in the week context can be 0(Sunday) and we need to sub 6 days if it happens.
Finally, we need to get an interval between calendarStart and calendarStart + 41 because we want to display 42 days per time and map it into a useful object:
eachDayOfInterval({
start: calendarStart,
end: addDays(calendarStart, 41)
}).map((date) => {{
return {
isCurrent: compareAsc(currentDate.value, date) === 0,
isCurrentMonth: date.getMonth() === currentDate.value.getMonth(),
date: date
}
}});
Finally, let's implement the full example code with Vue 3 and Tailwind:
<template>
<div class="w-60 bg-white dark:border-[1px] dark:border-gray_border dark:bg-gray_800 rounded-lg shadow-lg">
<div class="flex justify-center space-x-2 py-3 items-center rounded-lg">
<button
type="button"
class="-my-1.5 flex flex-none items-center justify-center p-1.5"
@click="toPreviousMonth"
>
<span class="sr-only">Previous month</span>
<!-- Heroicon name: solid/chevron-left -->
<svg class="h-5 w-5 icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd"/>
</svg>
</button>
<h2 class="font-normal text-gray_800 text-sm dark:text-white font-inter">
{{ format(currentDate, 'MMMM yyyy') }}
</h2>
<button
type="button"
class="-my-1.5 -mr-1.5 ml-2 flex flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500"
@click="toNextMonth"
>
<!-- Heroicon name: solid/chevron-right -->
<svg class="h-5 w-5 icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"/>
</svg>
</button>
</div>
<div class="mt-2 grid grid-cols-7 text-center text-sm">
<div class="text-gray_500 font-inter dark:text-white">Mo</div>
<div class="text-gray_500 font-inter dark:text-white">Tu</div>
<div class="text-gray_500 font-inter dark:text-white">We</div>
<div class="text-gray_500 font-inter dark:text-white">Th</div>
<div class="text-gray_500 font-inter dark:text-white">Fr</div>
<div class="text-gray_500 font-inter dark:text-white">Sa</div>
<div class="text-gray_500 font-inter dark:text-white">Su</div>
</div>
<div class="mt-2 grid grid-cols-7 text-sm font-inter">
<button
v-for="(day, index) in days" :key="`day-${index}`"
type="button"
class="mx-auto flex h-8 w-8 items-center justify-center rounded-lg"
@click="currentDate = day.date"
:class="{'bg-blue_400 justify-center rounded-lg': day.isCurrent}"
>
<time :class="{
'text-number_calendar': !day.isCurrentMonth,
'text-gray_800 dark:text-white': day.isCurrentMonth
}">
{{ format(day.date, 'd') }}
</time>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import {
subDays,
addDays,
eachDayOfInterval,
format,
startOfMonth,
compareAsc,
startOfDay,
addMonths,
subMonths,
getDay,
} from 'date-fns'
import {computed, ref} from "vue";
const currentDate = ref(startOfDay(new Date()));
const days = computed(() => {
const monthStart = startOfMonth(currentDate.value);
const dayNumInWeek = getDay(monthStart);
const calendarStart = subDays(monthStart, dayNumInWeek !== 0 ? dayNumInWeek - 1 : 6);
return eachDayOfInterval({
start: calendarStart,
end: addDays(calendarStart, 41)
}).map((date) => {{
return {
isCurrent: compareAsc(currentDate.value, date) === 0,
isCurrentMonth: date.getMonth() === currentDate.value.getMonth(),
date: date
}
}});
});
function toNextMonth() {
currentDate.value = addMonths(startOfMonth(currentDate.value), 1)
}
function toPreviousMonth() {
currentDate.value = subMonths(startOfMonth(currentDate.value), 1)
}
</script>
Top comments (0)