1.目的

LWC での DatePicker の作成方法を共有します。

2.ソース構成図

lwc
 ├─datePicker
 └─datePickerContainer
  • datePicker
    image.png
.select-box {
    background-color: rgb(255, 255, 255);
    border: 1px solid rgb(192, 192, 192);
    border-radius: 0.25rem;
    transition: border 0.1s linear, background-color 0.1s linear;
    height: calc(1.875rem + (1px * 2));
}

.select-box[disabled] {
    background-color: rgb(233, 234, 236);
    border-color: rgb(196, 198, 202);
    cursor: not-allowed;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.inpS {
    width: 70%;
    margin: 0 0.5rem 0 0;
    text-align:center;
    text-align-last:center;
}

.error-message {
    color: var(--lwc-colorTextError, rgb(194, 57, 52));
}

.select-has-error {
    background-color: var(--lwc-colorBackgroundInput, rgb(255, 255, 255));
    border-color: var(--lwc-colorBorderError, rgb(194, 57, 52));
    box-shadow: var(--lwc-colorBorderError, rgb(194, 57, 52)) 0 0 0 var(--lwc-borderWidthThin, 1px) inset;
    background-clip: padding-box;
}
<template>
  <div class="slds-form-element">
    <label class="slds-form-element__label" data-id="label">{label}</label>
    <div class="slds-form-element__control">
      <div class="slds-grid slds-form_horizontal">
        <div class="slds-col slds-col slds-size_1-of-3 slds-small-size_1-of-3 slds-medium-size_1-of-3">
          <select class="select-box inpS" data-id="year" disabled={getdisable} onchange={yearBoxChange}
            required={getRequired}>
          </select>
          年
        </div>
        <div class="slds-col slds-col slds-size_1-of-3 slds-small-size_1-of-3 slds-medium-size_1-of-3">
          <select class="select-box inpS" data-id="month" disabled={getdisable} onchange={monthBoxChange}
            required={getRequired}>
          </select>
          月
        </div>
        <div class="slds-col slds-col slds-size_1-of-3 slds-small-size_1-of-3 slds-medium-size_1-of-3">
          <select class="select-box inpS" data-id="day" disabled={getdisable} onchange={dateBoxChange}
            required={getRequired}>
          </select>
          日
        </div>
      </div>
    </div>
    <div data-help-message="true" role="alert" class="error-message"></div>
  </div>
</template>
import { LightningElement, api, track } from 'lwc';

export default class datePicker extends LightningElement {
    //開始年
    @api startYear = 1901;
    //ラベル
    @api label;
    //可用
    @api disabled = false;
    //必須
    @api required = false;
    //年
    @track yearVal;
    //月
    @track monthVal;
    //日
    @track dayVal;
    //yearElement
    yearBox;
    //monthElement
    monthBox;
    //dateElement
    dateBox;

    // 日付データ
    today = new Date();
    thisYear = this.today.getFullYear();
    thisMonth = this.today.getMonth() + 1;
    thisDate = this.today.getDate();
    datesOfYear = [31, this.countDatesOfFeb(this.thisYear), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    /**
     * 年月日の取得
     *  @return {string} YYYY/MM/DD
     */
    @api
    get ymdval() {
        this.yearVal = this.yearBox.childNodes[this.yearBox.selectedIndex].value;
        this.monthVal = this.monthBox.childNodes[this.monthBox.selectedIndex].value;
        this.dayVal = this.dateBox.childNodes[this.dateBox.selectedIndex].value;
        let ymd = `${this.yearVal}/${this.monthVal}/${this.dayVal}`;
        if(ymd.length === 10)
            return ymd;
        return '';
    }

    /**
     * 年月日の設定
     * @param {string} val YYYY/MM/DDまたはYYYY-MM-DD
     */
    set ymdval(val) {
        if (val && val.length === 10) {
            this.yearVal = val.substring(0, 4);
            this.monthVal = val.substring(5, 7);
            this.dayVal = val.substring(8, 10);
        } else {
            this.yearVal = '';
            this.monthVal = '';
            this.dayVal = '';
        }
        if (this.yearVal && this.monthVal && this.dayVal &&
            this.yearBox && this.monthBox && this.dateBox) {
            this.yearBox.innerHTML = '';//年クリア
            this.monthBox.innerHTML = '';//月クリア
            this.dateBox.innerHTML = '';//日クリア
            this.datesOfYear = [31, this.countDatesOfFeb(this.yearVal), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
            this.createOption(this.yearBox, this.startYear, this.thisYear, this.yearVal);//年の設定
            this.createOption(this.monthBox, 1, 12, this.monthVal);//月の設定
            this.createOption(this.dateBox, 1, this.datesOfYear[this.monthVal - 1], this.dayVal);//日の設定
        }
    }

    /**
     * 可用
     */
    get getdisable() {
        return this.disabled === 'true' || this.disabled === true;
    }

    /**
     * 必須
     */
    get getRequired() {
        return this.required === 'true' || this.required === true;
    }

    /**
     * 親から初期化
     */
    renderedCallback() {
        // console.log('<=======DatePickerDebug=========>');
        this.yearBox = this.template.querySelector('select[data-id="year"]');
        this.monthBox = this.template.querySelector('select[data-id="month"]');
        this.dateBox = this.template.querySelector('select[data-id="day"]');
        // 初期値を設定
        if (!(this.yearBox.innerHTML && this.monthBox.innerHTML && this.dateBox.innerHTML)){
            this.createOption(this.yearBox, this.startYear, this.thisYear, this.yearVal);//年の設定
            this.createOption(this.monthBox, 1, 12, this.monthVal);//月の設定
            this.createOption(this.dateBox, 1, this.datesOfYear[this.monthVal - 1], this.dayVal);//日の設定
        }
        let labelElement = this.template.querySelector('label[data-id="label"]');
        if (this.required === 'true' || this.required === true) {
            labelElement.innerHTML = `<abbr lightning-input_input="" title="必須" class="slds-required">*</abbr> ${this.label}`;
        }else{
            labelElement.innerHTML = this.label;
        }
    }

    // 年イベント
    yearBoxChange(e) {
        // this.monthBox.innerHTML = '';//月クリア
        this.yearVal = e.target.value;
        this.monthVal = this.monthBox.childNodes[this.monthBox.selectedIndex].value;
        this.dayVal = this.dateBox.childNodes[this.dateBox.selectedIndex].value;
        this.datesOfYear = [31, this.countDatesOfFeb(this.yearVal), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        // this.createOption(this.monthBox, 1, 12, this.monthVal);
        this.dateBox.innerHTML = '';//日クリア
        this.createOption(this.dateBox, 1, this.datesOfYear[this.monthVal ? this.monthVal - 1 : this.thisMonth -1], this.dayVal);
        const changenEvent = new CustomEvent('change', {
            detail: this.ymdval
        });
        this.dispatchEvent(changenEvent);
        this.checkValidity();
    }

    // 月イベント
    monthBoxChange(e) {
        this.monthVal = e.target.value;
        this.yearVal = this.yearBox.childNodes[this.yearBox.selectedIndex].value;
        this.dayVal = this.dateBox.childNodes[this.dateBox.selectedIndex].value;
        this.dateBox.innerHTML = '';//日クリア
        this.createOption(this.dateBox, 1, this.datesOfYear[this.monthVal ? this.monthVal - 1 : this.thisMonth -1], this.dayVal);
        const changenEvent = new CustomEvent('change', {
            detail: this.ymdval
        });
        this.dispatchEvent(changenEvent);
        this.checkValidity();
    }

    // 日イベント
    dateBoxChange(e) {
        this.dayVal = e.target.value;
        const changenEvent = new CustomEvent('change', {
            detail: this.ymdval
        });
        this.dispatchEvent(changenEvent);
        this.checkValidity();
    }

    // ライブラリ
    /**
     * 任意の年が閏年であるかをチェックする
     * @param {number} year チェックしたい西暦年号
     * @return {boolean} 閏年であるかを示す真偽値
     */
    isLeapYear(year) {
        return (year % 4 === 0) && (year % 100 !== 0) || (year % 400 === 0);
    }

    /**
     * 任意の年の2月の日数を数える
     * @param {number} year チェックしたい西暦年号
     * @return {number} その年の2月の日数
     */
    countDatesOfFeb(year) {
        return this.isLeapYear(year) ? 29 : 28;
    }


    /**
     * セレクトボックスの中にオプションを生成する
     * @param {string} dom セレクトボックスのDOMのid属性値
     * @param {number} startNum オプションを生成する最初の数値
     * @param {number} endNum オプションを生成する最後の数値
     * @param {string} current 現在の日付にマッチする数値
     */
    createOption(dom, startNum, endNum, current) {
        let blankOption = document.createElement('option');
        dom.appendChild(blankOption);
        for (let j = startNum; j <= endNum; j++) {
            let option = document.createElement('option');
            if (j === Number(current)) {
                option.value = this.paddingFormat(j);
                option.innerHTML = this.paddingFormat(j);
                option.selected = true;
            } else {
                option.value = this.paddingFormat(j);
                option.innerHTML = this.paddingFormat(j);
            }
            dom.appendChild(option);
        }
    }

    /**
     * ゼロ埋まる
     * @param {string} i
     */
    paddingFormat(i) {
        if (i.toString().length < 2)
            return '0' + i;
        return i;
    }

    /**
     * チェック結果
     */
    @api
    checkValidity() {
        let validity = true;
        let className = 'select-has-error';
        let errorbar = this.template.querySelector('div[class = "error-message"]');
        if (!this.disabled &&  this.required && (!this.ymdval || this.ymdval.length != 10)) {
            errorbar.innerHTML = 'この項目を選択してください。'
            this.yearBox.classList.add(className);
            this.monthBox.classList.add(className);
            this.dateBox.classList.add(className);
            validity = false;
        } else {
            errorbar.innerHTML = '';
            this.yearBox.classList.remove(className);
            this.monthBox.classList.remove(className);
            this.dateBox.classList.remove(className);
            validity = true;
        }
        return validity;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

  • datePickerContainer
    image.png
<template>
    <div class="slds-card" style="height:500px;width:1200px;">
        <div>{dateStr}</div>
        <div class="slds-col slds-size_3-of-12">
            <c-date-picker label="年月日:" required={required} onchange={dateChange}></c-date-picker>
        </div>
        <lightning-button label="内容チェック" onclick={dateCheckHandler}></lightning-button>
    </div>
</template>
import { LightningElement, track } from 'lwc';

export default class DatePickerContainer extends LightningElement {
    @track dateStr;
    @track required = true;

    /**
     * 日付選択
     * @param {*} e
     */
    dateChange(e) {
        e.preventDefault();
        this.dateStr = e.detail;

    }

    /**
     * 日付チェック
     * @param {*} e
     */
    dateCheckHandler(e) {
        e.preventDefault();
        [...this.template.querySelectorAll('c-date-picker')].reduce((previousValue, currentValue) => {
            return previousValue && currentValue.checkValidity();
        }, true)
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

3.ロカールで動作確認

datePickerContainer 中に右クリックし、SFDX:Preview Component Locallyを押下する
image.png
Use Desktop Browserを選択する
image.png
サーバを立ち上げて、ブラウザを自動的に開く
image.png