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>
動作確認
参考
salesforce-custom-lwc-component/lwc-custom-date-picker at main · …