SalesforceのLWCでカスタムCalendarコンポーネントを作る方法を紹介します。

https://www.lightningdesignsystem.com/components/datepickers/

上記公式サイトのUIを基づいて、LWCでカスタムCalendarコンポーネントを作成する例です。

.noclick {
  pointer-events: none;
}

.day {
  cursor: pointer;
}
<template>
  <div class="slds-datepicker slds-dropdown" role="dialog">
    <div class="slds-datepicker__filter slds-grid">
      <div
        class="slds-datepicker__filter_month slds-grid slds-grid_align-spread slds-grow"
      >
        <div class="slds-align-middle">
          <button
            class="slds-button slds-button_icon slds-button_icon-container"
            title="Previous Month"
            onclick="{prev}"
          >
            <lightning-icon icon-name="utility:left" size="x-small">
            </lightning-icon>
            <span class="slds-assistive-text">Previous Month</span>
          </button>
        </div>
        <h2 class="slds-align-middle">{currentMonth}月</h2>
        <div class="slds-align-middle">
          <button
            class="slds-button slds-button_icon slds-button_icon-container"
            title="Next Month"
            onclick="{next}"
          >
            <lightning-icon icon-name="utility:right" size="x-small">
            </lightning-icon>
            <span class="slds-assistive-text">Next Month</span>
          </button>
        </div>
      </div>
      <div class="slds-shrink-none">
        <div class="slds-select_container">
          <select class="slds-select" onchange="{yearSelectChange}">
            <template
              for:each="{selectYearList}"
              for:item="item"
              for:index="index"
            >
              <template if:true="{item.selected}">
                <option key="{item.value}" value="{item.value}" selected>
                  {item.value}
                </option>
              </template>
              <template if:false="{item.selected}">
                <option key="{item.value}" value="{item.value}">
                  {item.value}
                </option>
              </template>
            </template>
          </select>
        </div>
      </div>
    </div>
    <table class="slds-datepicker__month" role="grid">
      <thead>
        <tr>
          <th scope="col">
            <abbr title="日曜日">日</abbr>
          </th>
          <th scope="col">
            <abbr title="月曜日">月</abbr>
          </th>
          <th scope="col">
            <abbr title="火曜日">火</abbr>
          </th>
          <th scope="col">
            <abbr title="水曜日">水</abbr>
          </th>
          <th scope="col">
            <abbr title="木曜日">木</abbr>
          </th>
          <th scope="col">
            <abbr title="金曜日">金</abbr>
          </th>
          <th scope="col">
            <abbr title="土曜日">土</abbr>
          </th>
        </tr>
      </thead>
      <tbody>
        <template for:each="{dayList}" for:item="items" for:index="index">
          <tr key="{items.id}">
            <template for:each="{items.value}" for:item="item" for:index="idx">
              <template if:true="{item.adjacentMonth}">
                <td class="slds-day_adjacent-month" key="{item.day}">
                  <span
                    class="slds-day"
                    data-value="{item.value}"
                    onclick="{dateSelectChange}"
                  >
                    {item.day}
                  </span>
                </td>
              </template>
              <template if:false="{item.adjacentMonth}">
                <template if:true="{item.today}">
                  <td class="slds-is-today" key="{item.day}">
                    <span
                      class="slds-day"
                      data-value="{item.value}"
                      onclick="{dateSelectChange}"
                    >
                      {item.day}
                    </span>
                  </td>
                </template>
                <template if:false="{item.today}">
                  <td key="{item.day}">
                    <span
                      class="slds-day"
                      data-value="{item.value}"
                      onclick="{dateSelectChange}"
                    >
                      {item.day}
                    </span>
                  </td>
                </template>
              </template>
            </template>
          </tr>
        </template>
      </tbody>
    </table>
    <button
      class="slds-button slds-align_absolute-center slds-text-link"
      onclick="{todayClickHandler}"
    >
      今日
    </button>
  </div>
</template>
import { LightningElement, track } from "lwc";
const DAY_OF_WEEK = ["日", "月", "火", "水", "木", "金", "土"];
export default class Calendar extends LightningElement {
  //現在の日付
  today = new Date();
  // 月末だとずれる可能性があるため、1日固定で取得
  showDate = new Date(this.today.getFullYear(), this.today.getMonth(), 1);
  @track selectYearList = [];
  @track dayList = [];
  @track currentYear = this.showDate.getFullYear();
  @track currentMonth = this.showDate.getMonth() + 1;
  @track currentDay = this.showDate.getDate();

  // 前の月表示
  prev(e) {
    e.preventDefault();
    this.showDate.setMonth(this.showDate.getMonth() - 1);
    this.currentMonth = this.showDate.getMonth() + 1;
    this.showProcess(this.showDate);
  }

  // 次の月表示
  next(e) {
    e.preventDefault();
    this.showDate.setMonth(this.showDate.getMonth() + 1);
    this.currentMonth = this.showDate.getMonth() + 1;
    this.showProcess(this.showDate);
  }

  // カレンダー表示
  showProcess(date) {
    let year = date.getFullYear();
    let month = date.getMonth();
    this.createProcess(year, month);
  }

  // カレンダー作成
  createProcess(year, month) {
    let count = 0;
    let startDayOfWeek = new Date(year, month, 1).getDay();
    let endDate = new Date(year, month + 1, 0).getDate();
    let lastMonthEndDate = new Date(year, month, 0).getDate();
    let row = Math.ceil((startDayOfWeek + endDate) / DAY_OF_WEEK.length);
    this.dayList = [];
    // 1行ずつ設定
    for (let i = 0; i < row; i++) {
      this.dayList.push({ value: [], id: i });
      // 1colum単位で設定
      for (let j = 0; j < DAY_OF_WEEK.length; j++) {
        if (i == 0 && j < startDayOfWeek) {
          // 1行目で1日まで先月の日付を設定
          this.dayList[i].value.push({
            adjacentMonth: true,
            today: false,
            day: lastMonthEndDate - startDayOfWeek + j + 1,
            value: `${this.currentYear}-${this.currentMonth - 1}-${
              lastMonthEndDate - startDayOfWeek + j + 1
            }`,
          });
        } else if (count >= endDate) {
          // 最終行で最終日以降、翌月の日付を設定
          count++;
          this.dayList[i].value.push({
            adjacentMonth: true,
            today: false,
            day: count - endDate,
            value: `${this.currentYear}-${this.currentMonth + 1}-${
              count - endDate
            }`,
          });
        } else {
          // 当月の日付を曜日に照らし合わせて設定
          count++;
          if (
            year == this.today.getFullYear() &&
            month == this.today.getMonth() &&
            count == this.today.getDate()
          ) {
            this.dayList[i].value.push({
              adjacentMonth: false,
              today: true,
              day: count,
              value: `${this.currentYear}-${this.currentMonth}-${count}`,
            });
          } else {
            this.dayList[i].value.push({
              adjacentMonth: false,
              today: false,
              day: count,
              value: `${this.currentYear}-${this.currentMonth}-${count}`,
            });
          }
        }
      }
    }
  }

  /**
   * セレクトボックスの中にオプションを生成する
   * @param {number} startNum オプションを生成する最初の数値
   * @param {number} endNum オプションを生成する最後の数値
   * @param {string} current 現在の日付にマッチする数値
   */
  createYearOption(startNum, endNum, current) {
    for (let j = startNum; j <= endNum; j++) {
      let selected;
      if (j === Number(current)) {
        selected = true;
      } else {
        selected = false;
      }
      this.selectYearList.push({
        value: j,
        selected: selected,
      });
    }
  }

  /**
   * 年を選択
   * @param {*} e
   */
  yearSelectChange(e) {
    e.preventDefault();
    let selectedIndex = e.target.selectedIndex;
    this.currentYear = e.target.options[selectedIndex].value;
    let currentDate = new Date(
      this.currentYear,
      this.currentMonth - 1,
      this.currentDay
    );
    this.showProcess(currentDate);
  }

  /**
   * スタイル削除
   */
  removeCurrentlySelectedDateAttributes() {
    const e = this.template.querySelector("td[class*='slds-is-selected']");
    e && e.classList.remove("slds-is-selected");
  }

  /**
   * 日付を選択
   * @param {*} e
   */
  dateSelectChange(e) {
    e.preventDefault();
    let target = e.target;
    this.removeCurrentlySelectedDateAttributes();
    target.parentElement.classList.add("slds-is-selected");
    let dateStr = target.dataset.value;
    this.dispatchEvent(
      new CustomEvent("select", {
        detail: new Date(dateStr),
      })
    );
  }

  /**
   * 今日を選択
   * @param {*} e
   */
  todayClickHandler(e) {
    e.preventDefault();
    this.showDate = new Date();
    this.dispatchEvent(
      new CustomEvent("select", {
        detail: this.showDate,
      })
    );
  }

  connectedCallback() {
    this.showProcess(this.today);
    let thisYear = this.today.getFullYear();
    this.createYearOption(thisYear - 100, thisYear + 100, thisYear); //年の設定
  }
}
  • 動作

コンポーネント中に右クリックし、SFDX:Preview Component Locallyを押下する
image.png

Use Desktop Browserを選択する
image.png

サーバを立ち上げて、ブラウザを自動的に開く
image.png