LWC 標準の日付選択コンポーネントは<lightning-input type="date">となりますが、カスタマイズことが出来かねますので、今回 LWC で 日付選択コンポーネントを作る方法を共有します。

実装方法

  • customCalendar
<template>
  <div
    tabindex="-1"
    class="slds-datepicker slds-dropdown slds-dropdown_right date-picker-postion"
  >
    <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}"
          >
            <label>
              <lightning-icon icon-name="utility:left" size="x-small">
              </lightning-icon>
              <span class="slds-assistive-text">Previous Month</span>
            </label>
          </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}"
          >
            <label>
              <lightning-icon icon-name="utility:right" size="x-small">
              </lightning-icon>
              <span class="slds-assistive-text">Next Month</span>
            </label>
          </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="Sunday">Sun</abbr>
          </th>
          <th scope="col">
            <abbr title="Monday">Mon</abbr>
          </th>
          <th scope="col">
            <abbr title="Tuesday">Tue</abbr>
          </th>
          <th scope="col">
            <abbr title="Wednesday">Wed</abbr>
          </th>
          <th scope="col">
            <abbr title="Thursday">Thu</abbr>
          </th>
          <th scope="col">
            <abbr title="Friday">Fri</abbr>
          </th>
          <th scope="col">
            <abbr title="Saturday">Sat</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 lwc:if="{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 lwc:elseif="{item.selected}">
                <td class="slds-is-selected" key="{item.day}">
                  <span
                    class="slds-day"
                    data-value="{item.value}"
                    onclick="{dateSelectChange}"
                  >
                    {item.day}
                  </span>
                </td>
              </template>
              <template lwc:elseif="{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 lwc:else>
                <td key="{item.day}">
                  <span
                    class="slds-day"
                    data-value="{item.value}"
                    onclick="{dateSelectChange}"
                  >
                    {item.day}
                  </span>
                </td>
              </template>
            </template>
          </tr>
        </template>
      </tbody>
    </table>
    <button
      class="slds-button slds-align_absolute-center slds-text-link"
      onclick="{todayClickHandler}"
    >
      <label>Today</label>
    </button>
  </div>
</template>
import { LightningElement, track, api } from "lwc";
import { dateFormat } from "c/utils";

export default class CustomCalendar extends LightningElement {
  //今日
  today = new Date();
  //内部値
  @track _value;
  //年選択リスト
  @track selectYearList = [];
  //日選択リスト
  @track dayList = [];
  //現在年
  @track currentYear;
  //現在月
  @track currentMonth;
  //現在日
  @track currentDay;

  /**
   * 値取得
   */
  @api get value() {
    return this._value;
  }

  /**
   * 値設定
   */
  set value(val) {
    this._value = val;
    if (val) {
      this.showDate = val;
    } else {
      this.showDate = dateFormat(this.today, "YYYY-mm-dd");
    }
    //年
    this.currentYear = Number(this.showDate.substring(0, 4));
    //月
    this.currentMonth = Number(this.showDate.substring(5, 7));
    //日
    this.currentDay = Number(this.showDate.substring(8, 10));
    //カレンダー作成
    this.createCalendar(this.currentYear, this.currentMonth);
    //プルダウン年を作成
    this.createYearOption(
      1900,
      this.today.getFullYear() + 100,
      this.currentYear
    );
  }

  /**
   * 前の月表示
   * @param {*} e イベント
   */
  prev(e) {
    e.preventDefault();

    if (this.currentMonth === 1) {
      this.currentMonth = 12;
      if (this.currentYear === 1900) {
        this.currentYear = 1900;
      } else {
        this.currentYear -= 1;
      }
    } else {
      this.currentMonth -= 1;
    }
    this.createCalendar(this.currentYear, this.currentMonth);
    //プルダウン年を作成
    this.createYearOption(
      1900,
      this.today.getFullYear() + 100,
      this.currentYear
    );
  }

  /**
   * 次の月表示
   * @param {*} e イベント
   */
  next(e) {
    e.preventDefault();
    if (this.currentMonth === 12) {
      this.currentMonth = 1;
      this.currentYear += 1;
    } else {
      this.currentMonth += 1;
    }

    this.createCalendar(this.currentYear, this.currentMonth);
    //プルダウン年を作成
    this.createYearOption(
      1900,
      this.today.getFullYear() + 100,
      this.currentYear
    );
  }

  /**
   * カレンダー作成
   * @param {*} year 年
   * @param {*} month 月
   */
  createCalendar(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) / 7);
    this.dayList = [];
    let selectedYear;
    let selectedMonth;
    if (this.value) {
      //年
      selectedYear = Number(this.showDate.substring(0, 4));
      //月
      selectedMonth = Number(this.showDate.substring(5, 7));
    }

    // 1行ずつ設定
    for (let i = 0; i < row; i++) {
      this.dayList.push({ value: [], id: i });
      // 1colum単位で設定
      for (let j = 0; j < 7; j++) {
        if (i === 0 && j < startDayOfWeek) {
          // 1行目で1日まで先月の日付を設定
          this.dayList[i].value.push({
            adjacentMonth: true,
            today: false,
            selected: false,
            day: lastMonthEndDate - startDayOfWeek + j + 1,
            value: `${this.currentYear}-${(this.currentMonth - 1)
              .toString()
              .padStart(2, "0")}-${(lastMonthEndDate - startDayOfWeek + j + 1)
              .toString()
              .padStart(2, "0")}`,
          });
        } else if (count >= endDate) {
          // 最終行で最終日以降、翌月の日付を設定
          count++;
          this.dayList[i].value.push({
            adjacentMonth: true,
            today: false,
            selected: false,
            day: count - endDate,
            value: `${this.currentYear}-${(this.currentMonth + 1)
              .toString()
              .padStart(2, "0")}-${(count - endDate)
              .toString()
              .padStart(2, "0")}`,
          });
        } else {
          // 当月の日付を曜日に照らし合わせて設定
          count++;
          if (
            this.value &&
            year === selectedYear &&
            month === selectedMonth &&
            count === this.currentDay
          ) {
            this.dayList[i].value.push({
              adjacentMonth: false,
              today: false,
              selected: true,
              day: count,
              value: `${this.currentYear}-${this.currentMonth
                .toString()
                .padStart(2, "0")}-${count.toString().padStart(2, "0")}`,
            });
          } else if (
            year === this.today.getFullYear() &&
            month === this.today.getMonth() + 1 &&
            count === this.today.getDate()
          ) {
            this.dayList[i].value.push({
              adjacentMonth: false,
              today: true,
              selected: false,
              day: count,
              value: `${this.currentYear}-${this.currentMonth
                .toString()
                .padStart(2, "0")}-${count.toString().padStart(2, "0")}`,
            });
          } else {
            this.dayList[i].value.push({
              adjacentMonth: false,
              today: false,
              selected: false,
              day: count,
              value: `${this.currentYear}-${this.currentMonth
                .toString()
                .padStart(2, "0")}-${count.toString().padStart(2, "0")}`,
            });
          }
        }
      }
    }
  }

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

  /**
   * 年を選択
   * @param {*} e
   */
  yearSelectChange(e) {
    e.preventDefault();
    let selectedIndex = e.target.selectedIndex;
    this.currentYear = e.target.options[selectedIndex].value;
    this.createCalendar(this.currentYear, this.currentMonth);
  }

  /**
   * CSSスタイル削除
   */
  removeCurrentlySelectedDateAttributes() {
    const element = this.template.querySelector(
      "td[class*='slds-is-selected']"
    );
    if (element) {
      element.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: dateStr,
      })
    );
  }

  /**
   * 今日を選択
   * @param {*} e
   */
  todayClickHandler(e) {
    e.preventDefault();
    this.dispatchEvent(
      new CustomEvent("select", {
        detail: dateFormat(this.today, "YYYY-mm-dd"),
      })
    );
  }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>
  • customDatePicker
<template>
  <div
    tabindex="-1"
    class="slds-form-element__control slds-size_1-of-1 slds-dropdown-trigger slds-dropdown-trigger_click slds-is-open"
  >
    <div class="custom-input-label">
      <template if:true="{required}">
        <span class="required-mark">*</span>
      </template>
      {label}
    </div>
    <div
      class="slds-form-element__control slds-size_1-of-1 slds-input-has-icon slds-input-has-icon_right"
    >
      <div class="{inputClass}">
        <input
          class="slds-input"
          onfocus="{focusHandler}"
          onclick="{clickHandler}"
          onchange="{changeHandler}"
          value="{value}"
        />
        <div
          class="slds-button slds-button_icon slds-input__icon slds-input__icon_right"
          title="Select a date"
        >
          <lightning-icon icon-name="utility:event" size="x-small">
          </lightning-icon>
          <span class="slds-assistive-text">Select a date</span>
        </div>
      </div>
      <template lwc:if="{isHasError}">
        <div class="slds-has-error">
          <div class="slds-form-element__help slds-show">{errorMessage}</div>
        </div>
      </template>
      <template lwc:if="{_open}">
        <c-custom-calendar
          onselect="{calendarSelectHandler}"
          value="{calendarVal}"
          onmouseover="{handleMouseOver}"
          onmouseout="{handleMouseOut}"
        ></c-custom-calendar>
      </template>
    </div>
  </div>
</template>
import { LightningElement, api, track } from "lwc";
import { toDate } from "c/utils";

/**
 * 日付選択コンポーネント
 */
export default class CustomDatePicker extends LightningElement {
  //ラベル
  @api label;
  //必須
  @api required;
  //名
  @api name;
  //最小値
  @api min;
  //最大値
  @api max;
  //最大値超えるメッセージ
  @api messageWhenRangeOverflow = "The number is too high.";
  //最小値以下のメッセージ
  @api messageWhenRangeUnderflow = "The number is too low.";
  //必須メッセージ
  @api messageWhenValueMissing = "Complete this field.";
  //エラーありフラグ
  @track isHasError = false;
  //値
  @track _value = "";
  //カレンダー選択値
  @track calendarVal;
  //エラーメッセージ
  @track errorMessage;

  _open = false;
  _over = false;

  @api
  get value() {
    return this._value;
  }

  set value(val) {
    if (val.length >= 8) {
      //年
      const year = val.substring(0, 4);
      //月
      const month = val.substring(5, 7);
      //日
      const day = val.substring(8, 10);
      this._value = `${month}/${day}/${year}`;
      this.calendarVal = val;
    }
  }

  /**
   * CSSスタイル
   */
  get inputClass() {
    if (this.isHasError) {
      return "slds-input-has-icon slds-input-has-icon_right slds-has-error";
    }
    return "slds-input-has-icon slds-input-has-icon_right";
  }

  /**
   * エラーがあるかどうかをチェックする
   */
  hasError() {
    const reg = new RegExp(/^[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/);
    if (this.max && this.value && toDate(this.value) > toDate(this.max)) {
      this.isHasError = true;
      this.errorMessage = this.messageWhenRangeOverflow;
    } else if (
      this.min &&
      this.value &&
      toDate(this.value) < toDate(this.min)
    ) {
      this.isHasError = true;
      this.errorMessage = this.messageWhenRangeUnderflow;
    } else if (this.required && !this.value) {
      this.errorMessage = this.messageWhenValueMissing;
      this.isHasError = true;
      this._value = null;
    } else if (this.value && !reg.exec(this.value)) {
      this.errorMessage = this.messageWhenValueMissing;
      this.isHasError = true;
      this._value = null;
    } else if (this.value && reg.exec(this.value)) {
      this.errorMessage = this.messageWhenValueMissing;
      this.isHasError = false;
    } else if (!this.value) {
      this.errorMessage = this.messageWhenValueMissing;
      this.isHasError = false;
    }
  }

  /**
   * チェンジイベントハンドラ
   * @param {*} event
   */
  changeHandler(event) {
    this.closeDropdown();
    this._value = event.target.value;
    this.hasError();
  }

  /**
   * 入力チェック
   * @returns チェック結果
   */
  @api checkValidity() {
    this.hasError();
    return !this.isHasError;
  }

  /**
   * カレンダー選択
   * @param {*} event
   */
  calendarSelectHandler(event) {
    event.preventDefault();
    const value = event.detail;
    this.template.host.value = value;
    this.calendarVal = value;
    this.hasError();
    this.dispatchEvent(
      new CustomEvent("select", {
        detail: value,
      })
    );
    this.closeDropdown();
  }

  /*
   * following pair of functions are a clever way of handling a click outside,
   * despite us not having access to the outside dom.
   * see: https://salesforce.stackexchange.com/questions/255691/handle-click-outside-element-in-lwc
   */
  focusHandler(event) {
    //prevent firing more than once per focus (too many listeners get added)
    if (this._open) {
      return;
    }
    event.cancelBubble = true;
    event.stopPropagation();
    event.preventDefault();
    this.openDropdown();
    setTimeout(() => {
      document.addEventListener("click", this.handleClose, false);
    }, 100);
  }

  clickHandler(event) {
    event.cancelBubble = true;
    event.stopPropagation();
    event.preventDefault();
  }

  /**
   *
   * @param {*} event
   * @returns
   */
  handleClose = (event) => {
    if (this._over) {
      return;
    }
    event.stopPropagation();
    this.closeDropdown();
    document.removeEventListener("click", this.handleClose, false);
  };

  /**
   *
   */
  openDropdown() {
    this._open = true;
    this._over = false;
  }

  /**
   *
   */
  closeDropdown() {
    this._open = false;
    this._over = false;
  }

  /**
   *
   */
  handleMouseOver() {
    this._over = true;
  }

  /**
   *
   */
  handleMouseOut() {
    this._over = false;
  }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>
  • customDatePickerContainer
<template>
  <c-custom-date-picker onselect="{selectHandler}"></c-custom-date-picker>
  date: {date}
</template>
import { LightningElement, track } from "lwc";

export default class CustomDatePickerContainer extends LightningElement {
  @track date;

  selectHandler(event) {
    this.date = event.detail;
  }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__Tab</target>
    </targets>
</LightningComponentBundle>

動作確認

datepicker.png

datepicker.png

datepicker.png

参考