DEV Community

David
David

Posted on

Calendar component example with date-fns (Vue 3)

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
    }
  }});
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

And now we can use it to get the start date for our calendar:

const calendarStart = subDays(monthStart, dayNumInWeek !== 0 ? dayNumInWeek - 1 : 6);
Enter fullscreen mode Exit fullscreen mode

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
    }
  }});

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)