Calendar

Overview

Calendar is a powerful component to manage date. It is gretly inspired by DevDojo. Many things can be done with this component. Let see what we can achieve with examples below. It is used into DatePicker component.

Default Calendar

Basicly, this example shows the minimum requirement to use Calendar. Calendar provides you a set of custom listener you can use to do whatever suit best to your feature.

Here we retrieve selected date with @select-date props custom event.

Date Value:
Formatted Date:

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";
---

<div
  x-data={`
  {
    date: null,
    formattedDate: "",
  }
`}
  class="bg-white w-full"
>
  <div class="border flex gap-x-2 p-4 rounded">
    <span class="shrink-0">Date Value:</span>
    <span x-text="date"></span>
  </div>
  <div class="border flex gap-x-2 p-4 rounded">
    <span class="shrink-0">Formatted Date:</span>
    <span x-text="formattedDate"></span>
  </div>
  <Calendar
    id="default-calendar"
    x-ref="datePicker"
    x-on:select-date="console.log($event.detail); date = $event.detail.date; formattedDate = $event.detail.formattedDate"
  />
</div>

Multiple Select Calendar

Multiple dates can be selected. However, it’s up to you to define how multiple dates will be selected.

Don’t worry, with @select-date props custom event, it’s an easy trick. See example below.

P.S: Note that you have selectedDayClass props which allow to define cell day class based on its state.

Selected Dates

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";
---

<div
  x-data={`
  {
    dates: [],
    isDayEqualsToToday(day) {
      return new Date().getDate() === day;
    },
    isSelectedDayInDates(day, dates) {
      return dates.some((item) => item.date.getDate() === day);
    }
  }
`}
  class="bg-white w-full"
>
  <div class="border flex flex-col gap-y-2 p-4 rounded">
    <h3 class="text-center">Selected Dates</h3>
    <ul
      class="grid grid-cols-2 xl:grid-cols-4 gap-4 items-center justify-center"
    >
      <template x-for="date in dates">
        <li class="w-full flex justify-center">
          <span class="text-sm" x-text="date.formattedDate"></span>
        </li>
      </template>
    </ul>
  </div>
  <Calendar
    id="multiple-select-calendar"
    stateDayClass={`{
    'bg-primary-200': isDayEqualsToToday(day),
    'text-gray-600 hover:bg-primary-200': !(isDayEqualsToToday(day) && isSelectedDayInDates(day, dates)),
    'bg-primary-500 text-white hover:bg-opacity-75': isSelectedDayInDates(day, dates)
  }`}
    x-ref="datePicker"
    @select-date="dates = [...dates, $event.detail];"
    @reset-date="dates = [];"
    showResetAndTodayButtons
  />
</div>

Disabled Dates Calendar

A pratical way to use a calendar is to deal with unavailble dates. There are many ways to achieve this.

See those 5 examples below to get the hint.

For this first example, we define an array of dates as disabled dates.

Disabled Dates

  • Tue Sep 05 2023
  • Wed Sep 06 2023
  • Thu Sep 07 2023
  • Fri Sep 08 2023

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";

const disabledDates = ["2023-09-05", "2023-09-06", "2023-09-07", "2023-09-08"];
---

<div class="flex flex-col gap-y-2 bg-white border p-2">
  <div>
    <h3 class="text-center">Disabled Dates</h3>
    <ul class="grid grid-cols-2 gap-2 text-sm">
      {
        disabledDates.map((date) => (
          <li class="flex items-center justify-center">
            <span x-date-format="M d, YYYY">
              {new Date(date).toDateString()}
            </span>
          </li>
        ))
      }
    </ul>
  </div>
  <Calendar
    id="disabled-dates-calendar-1"
    x-ref="datePicker"
    {disabledDates}
    @select-date="console.log($event.detail); date = $event.detail.date; formattedDate = $event.detail.formattedDate"
  />
</div>

This example is same as above but include today as well.

Disabled Dates

  • Sat Nov 18 2023
  • Tue Sep 05 2023
  • Wed Sep 06 2023
  • Thu Sep 07 2023
  • Fri Sep 08 2023

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";

const disabledDates = [
  new Date().toDateString(),
  "2023-09-05",
  "2023-09-06",
  "2023-09-07",
  "2023-09-08",
];
---

<div class="flex flex-col gap-y-2 bg-white border p-2">
  <div>
    <h3 class="text-center">Disabled Dates</h3>
    <ul class="grid grid-cols-2 gap-2 text-sm">
      {
        disabledDates.map((date) => (
          <li class="flex items-center justify-center">
            <span x-date-format="M d, YYYY">
              {new Date(date).toDateString()}
            </span>
          </li>
        ))
      }
    </ul>
  </div>
  <Calendar
    id="disabled-dates-calendar-2"
    x-ref="datePicker"
    {disabledDates}
    @select-date="console.log($event.detail); date = $event.detail.date; formattedDate = $event.detail.formattedDate"
  />
</div>

In this example, we disabled dates from beginning of month to a specific date excluded.

Disabled Dates

  • Fri Sep 01 2023
  • Sat Sep 02 2023
  • Sun Sep 03 2023
  • Mon Sep 04 2023
  • Tue Sep 05 2023
  • Wed Sep 06 2023
  • Thu Sep 07 2023
  • Fri Sep 08 2023
  • Sat Sep 09 2023
  • Sun Sep 10 2023
  • Mon Sep 11 2023
  • Tue Sep 12 2023
  • Wed Sep 13 2023
  • Thu Sep 14 2023
  • Fri Sep 15 2023
  • Sat Sep 16 2023

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";

const disabledDates: string[] = [];
const date = new Date("2023-09-17");
for (let index = 1; index < date.getDate(); index++) {
  disabledDates.push(
    new Date(date.getFullYear(), date.getMonth(), index).toDateString()
  );
}
---

<div class="flex flex-col gap-y-2 bg-white border p-2">
  <div>
    <h3 class="text-center">Disabled Dates</h3>
    <ul class="flex flex-wrap items-center mx-auto gap-2 text-sm">
      {
        disabledDates.map((date) => (
          <li class="flex items-center justify-center border p-2">
            <span x-date-format="M d, YYYY">
              {new Date(date).toDateString()}
            </span>
          </li>
        ))
      }
    </ul>
  </div>
  <Calendar
    id="disabled-dates-calendar-3"
    x-ref="datePicker"
    {disabledDates}
    @select-date="console.log($event.detail); date = $event.detail.date; formattedDate = $event.detail.formattedDate"
  />
</div>

Here, we don’t specify a disabled dates array but rather use a condition.

As mentionned in the code, we disabled dates before today.

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";
---

<div class="flex flex-col gap-y-2 bg-white border p-2 min-w-max">
  <Calendar
    id="disabled-dates-calendar-4"
    x-ref="datePicker"
    disableDay={`
      () => {
        const currentDate = new Date(selectedYear, selectedMonth, day);
        const todayDate = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate())
        return todayDate.valueOf() > currentDate.valueOf();
      }
    `}
    @select-date="console.log($event.detail); date = $event.detail.date; formattedDate = $event.detail.formattedDate"
  />
</div>

This one is the opposite of the one above. Dates after today are disbaled.

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";
---

<div class="flex flex-col gap-y-2 bg-white border p-2 min-w-max">
  <Calendar
    id="disabled-dates-calendar-4"
    x-ref="datePicker"
    disableDay={`
      () => {
        const currentDate = new Date(selectedYear, selectedMonth, day);
        const todayDate = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate())
        return todayDate.valueOf() < currentDate.valueOf();
      }
    `}
    @select-date="console.log($event.detail); date = $event.detail.date; formattedDate = $event.detail.formattedDate"
  />
</div>

Year Min Max Calendar

It would be convenient to delimit dates which will be selected.

Calendar gives you this opportunity by setting minYear and maxYear props.

If it can be set with astro props, it is also possible to set it through alpinejs. Check these two examples below.

Set minYear and maxYear via Astro Props

Is year = minYear ?
Is year = maxYear ?

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";

const minYear = 1960;
const maxYear = 2030;
---

<div
  x-data={`
  {
    date: null,
    isYearEqualMinYear: false,
    isYearEqualMaxYear: false,
  }
`}
  x-init={`
    isYearEqualMinYear = new Date().getFullYear() === ${minYear} && new Date().getMonth() === 0;
    isYearEqualMaxYear = new Date().getFullYear() === ${maxYear} && new Date().getMonth() === 11;
    $watch("date", dateValue => {
      isYearEqualMinYear = new Date(dateValue).getFullYear() === ${minYear} && new Date(dateValue).getMonth() === 0;
      isYearEqualMaxYear = new Date(dateValue).getFullYear() === ${maxYear} && dateValue.getMonth() === 11;
    })
  `}
  class="bg-white w-full border divide-y-2"
>
  <h3 class="text-center w-full py-2">
    Set minYear and maxYear via Astro Props
  </h3>
  <div class="flex gap-x-2 p-4">
    <span class="shrink-0">Is year = minYear ?</span>
    <span x-text="isYearEqualMinYear" class="text-info-400"></span>
  </div>
  <div class="flex gap-x-2 p-4">
    <span class="shrink-0">Is year = maxYear ?</span>
    <span x-text="isYearEqualMaxYear" class="text-info-400"></span>
  </div>
  <div>
    <Calendar
      id="year-min-max-calendar-1"
      x-ref="datePicker"
      class="border-transparent rounded-none shadow-none"
      minYear={1960}
      maxYear={2030}
      x-on:select-date={`
        console.log("in @select-date : ", $event.detail);
        date = $event.detail.date;
      `}
      x-on:select-month={`
        console.log("in @select-month", $event.detail);
        date = $event.detail.date;
      `}
      x-on:select-year={`
        console.log("in @select-year", $event.detail);
        date = $event.detail.date;
      `}
    />
  </div>
</div>

Set minYear and maxYear via Alpine x-init

Is year = minYear ?
Is year = maxYear ?

Copied !

---
import Calendar from "$components/calendar/Calendar.astro";

const minYear = 1960;
const maxYear = 2030;
---

<div
  x-data={`
  {
    date: null,
    isYearEqualMinYear: false,
    isYearEqualMaxYear: false,
  }
`}
  x-init={`
    isYearEqualMinYear = new Date().getFullYear() === ${minYear} && new Date().getMonth() === 0;
    isYearEqualMaxYear = new Date().getFullYear() === ${maxYear} && new Date().getMonth() === 11;
    $watch("date", dateValue => {
      isYearEqualMinYear = new Date(dateValue).getFullYear() === ${minYear} && new Date(dateValue).getMonth() === 0;
      isYearEqualMaxYear = new Date(dateValue).getFullYear() === ${maxYear} && dateValue.getMonth() === 11;
    })
  `}
  class="bg-white w-full border divide-y-2"
>
  <h3 class="text-center w-full py-2">
    Set minYear and maxYear via Alpine x-init
  </h3>
  <div class="flex gap-x-2 p-4 rounded">
    <span class="shrink-0">Is year = minYear ?</span>
    <span x-text="isYearEqualMinYear" class="text-info-400"></span>
  </div>
  <div class="flex gap-x-2 p-4 rounded">
    <span class="shrink-0">Is year = maxYear ?</span>
    <span x-text="isYearEqualMaxYear" class="text-info-400"></span>
  </div>
  <div>
    <Calendar
      id="year-min-max-calendar-2"
      x-ref="datePicker"
      class="border-transparent rounded-none shadow-none"
      x-init="minYear = 1960; maxYear = 2030;"
      stateDayClass={`{
      'bg-info-400 text-white': isToday(day),
      'text-gray-600 hover:text-white hover:bg-info-400': !(isToday(day) && isSelectedDay(day)),
      'bg-info-700 text-white hover:bg-opacity-75': isSelectedDay(day)
    }`}
      x-on:select-date={`
        console.log("in @select-date : ", $event.detail);
        date = $event.detail.date;
      `}
      x-on:select-month={`
        console.log("in @select-month", $event.detail);
        date = $event.detail.date;
      `}
      x-on:select-year={`
        console.log("in @select-year", $event.detail);
        date = $event.detail.date;
      `}
    />
  </div>
</div>

Custom Slot Days Calendar

AstroPine’s Calendar can be customized. This example below define a custom days grid layout with slot days.

Copied !

---
import { loremIpsum } from "lorem-ipsum";
import Calendar from "$components/calendar/Calendar.astro";
---

<Calendar
  disabledDates={["2023-10-08"]}
  id="custom-slot-calendar"
  x-ref="datePicker"
  x-on:select-date="console.log($event.detail)"
>
  <ul slot="days" class="grid grid-cols-7 grid-flow-row">
    <template x-for="blankDay in blankDaysInMonth">
      <div class="p-1 text-sm text-center border border-transparent"></div>
    </template>
    <template x-for="(day, dayIndex) in daysInMonth" :key="dayIndex">
      <li class="w-full h-full border aspect-auto">
        <button
          x-on:click="selectDay(day)"
          x-bind:class={`{
                'border-neutral-500 text-neutral-600 border-r-8': isToday(day),
                'text-gray-600 hover:border-neutral-600 hover:border-r-8': !(isToday(day) && isSelectedDay(day)),
                'border-info-500 text-info-500 hover:bg-opacity-75 border-r-8': isSelectedDay(day)
            }`}
          x-bind:disabled="isDisabledDay(day)"
          class="flex flex-col justify-between w-full border aspect-square text-sm leading-none text-center p-2 disabled:cursor-not-allowed disabled:text-gray-200 disabled:border-transparent"
        >
          <span x-text="day"></span>
          <div class="flex flex-wrap items-center">
            {
              loremIpsum({
                units: "words",
                count: Math.ceil(Math.random() * 3 + 1),
              })
            }
          </div>
        </button>
      </li>
    </template>
  </ul>
</Calendar>