Flatpickrで
日時入力をカレンダー表示にする

Flatpickrとは

FlatpickrはJS製の軽量な日時入力補助ライブラリで、入力エリアにカレンダーや時間を表示してUIを強化できます。

https://flatpickr.js.org/

FlatpickrのExample画面

似たような日付入力補助ライブラリ

日時入力フィールドがあるようなフォームはほぼこれらのライブラリを設定されているんじゃないかなーという印象。

jQuery UIの「datepicker」は昔ながらって感じでわかりやすいですが、jQuery依存なのはイマイチな感じです。「datetimepicker」はその名の通り時間も扱えるので便利ですが、こちらもjQuery依存。「Pikaday」は日本語化しようと思うと「Moment.js」が別途必要ですし、「bootstrap-datepicker」はBootStrapに加えてjQuery依存です。どのライブラリも何かと手間が多いし依存関係が面倒です。

その点、今回紹介する「Flatpickr」はjQueryやMoment.jsのような別のライブラリは不要。軽量で単体で動くし見た目もいい感じで使いやすかったのでメモしておきます。

ドキュメントについて

公式ドキュメントは英語ですが、日本語に翻訳してくれているサイトがありました。
公式の英語の表現がわかりにくいところはこちらを参考にさせていただきました。

flatpickrドキュメント翻訳

https://tr.you84815.space/flatpickr/index.html

対応ブラウザ

IE9以降、Edge、iOS Safari 6以降、Chrome 8以降、Firefox6以降となっています。
モダンブラウザで全然使えますね。

IE9に関しては公式ドキュメントをご確認ください。(たぶんもう必要ないけど念のため)

Usage in IE9 - flatpickr

https://flatpickr.js.org/ie9/

Flatpickrのデモ

今回作ったデモはこちら。

Flatpickrのデモ

淡々と入力エリアを設置しただけのシンプルなデモです。
オプションが多くて全てを試せないので、実際よく使うような機能を使ってみました。

このデモのGithubリポジトリはこちら。
※記事で説明に使っているコードとデモのコードは関数名や変数名などが若干異なる場合があります。

inos3910 / flatpickr-demo

https://github.com/inos3910/flatpickr-demo

インストール

npmyarnの場合は下記

# npm
npm i -D flatpickr

# yarn
yarn add -D flatpickr

SASSを使う場合はCSSをインポート(※postcss-importを利用)

@import 'flatpickr/dist/flatpickr.min.css';

JSはファイルの先頭で必要なファイルをインポート

import flatpickr from 'flatpickr/dist/flatpickr.min.js';
import { Japanese } from "flatpickr/dist/l10n/ja.js" //日本語用モジュール

CDNの場合は下記のタグで読み込めばOK

<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<!-- 日本語化する場合は下記を追記 -->
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.js"></script>

実装方法

デモの10パターンについてメモしますが、HTMLはすべて下記を使い回しています。デモの内容によって修正がある場合は別途追記します。

<input type="text" name="datepicker" id="js-datepicker">

CSSについてはプラグインファイルを読み込んだ後、わかりやすく土日に色を付けるように追記しています。これは好みの問題ですので色も変えられますし、必要なければ追記しなくてもデモは動きます。

@import 'flatpickr/dist/flatpickr.min.css';

$red        : #f00;
$blue       : #25bdcf;

/* 日曜日:赤 */
.flatpickr-calendar .flatpickr-innerContainer .flatpickr-weekdays .flatpickr-weekday:nth-child(7n + 1),
.flatpickr-calendar .flatpickr-innerContainer .flatpickr-days .flatpickr-day:not(.flatpickr-disabled):not(.prevMonthDay):not(.nextMonthDay):nth-child(7n + 1) {
  color: $red;
}

/* 土曜日:青 */
.flatpickr-calendar .flatpickr-innerContainer .flatpickr-weekdays .flatpickr-weekday:nth-child(7),
.flatpickr-calendar .flatpickr-innerContainer .flatpickr-days .flatpickr-day:not(.flatpickr-disabled):not(.prevMonthDay):not(.nextMonthDay):nth-child(7n) {
  color: $blue;
}

/* 祝日 */
.flatpickr-day.is-holiday{
  background: lighten($red, 40%) !important;
}

/* 入力欄の文字列を選択させないようにしておく  */
.flatpickr-calendar .numInput{
  user-select: none;
}

デフォルト

何もオプションも付けないプレーンな形で実装する場合は下記。

flatpickr('#js-datepicker');

デフォルトのデモ

日本語化

日本語化は最初に読み込んだモジュールをオプションに指定するだけ。

import flatpickr from 'flatpickr/dist/flatpickr.min.js';
import { Japanese } from "flatpickr/dist/l10n/ja.js";

flatpickr('#js-datepicker', {
  locale : Japanese, // 日本語用モジュールを適用
  dateFormat : 'Y.m.d(D)', // 2021.05.24(月)の形式で表示
  defaultDate : new Date() // 入力エリアの初期値
});

CDN使う場合は下記。

<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.js"></script>
<script>
  flatpickr('#js-datepicker', {
    locale     : 'ja',
    dateFormat : 'Y.m.d(D)', 
    defaultDate: new Date() 
  });
</script>

日本語化のデモ

期間制限

入力できる期間を制限する場合に、例えば最小を翌日、最大を3ヶ月後にするには下記のような実装です。

//翌日の日時
let minDate = new Date();
minDate = minDate.setDate(minDate.getDate() + 1);
//今日から3ヶ月後の日時
let maxDate = new Date();
maxDate = maxDate.setMonth(maxDate.getMonth() + 3);

flatpickr('#js-datepicker', {
  locale      : Japanese,
  dateFormat  : 'Y.m.d(D)',
  defaultDate : minDate,
  minDate     : minDate,
  maxDate     : maxDate
});

期間制限のデモ

祝日を反映

祝日一覧をAPIから取得し、祝日の場合は日付の背景を薄い赤にする

//祝日一覧をAPIから取得する
function fetchHolidays() {
  const today = new Date();
  const year  = today.getFullYear();
  return fetch(`https://holidays-jp.github.io/api/v1/${year}/date.json`)
  .then((res) => {
    if (!res.ok) {
      throw new Error(`${res.status} ${res.statusText}`);
    }
    return res.json();
  })
  .then((json) => {
    return json;
  })
  .catch((reason) => {
    console.error(reason);
  });
}

//日付をフォーマット YYYY-MM-DD
function formatDate(date) {
  const year  = date.getFullYear();
  const month = date.getMonth() + 1;
  const mm    = ('00' + month).slice(-2);
  const day   = date.getDate();
  const dd    = ('00' + day).slice(-2);
  return `${year}-${mm}-${dd}`;
}

//祝日の場合にクラスをつける
function addHolidayClass(dayElem, holidays){
  const date      = dayElem.dateObj;
  const selectDay = formatDate(date);
  if(selectDay in holidays){
    dayElem.classList.add('is-holiday');
  }
}

async function flatpickrInit() {
  const holidays = await fetchHolidays();
  flatpickr('#js-datepicker', {
    locale      : Japanese,
    dateFormat  : 'Y.m.d(D)',
    defaultDate : minDate,
    minDate     : minDate,
    maxDate     : maxDate,
    onDayCreate : (dObj, dStr, fp, dayElem) => {
      this.addHolidayClass(dayElem);
    }
  });
}

flatpickrInit();

APIの取得にはfetchを使いましたが、お好みで。エラーハンドリングはしてません。

祝日のAPIはひとまず下記を使いました。

Holidays JP API

https://holidays-jp.github.io/

注意点として、このAPIはGithub Pagesで作ってあるので、帯域を超えたり作者の方がリポジトリを変更・削除すると使えなくなる可能性があります。デモなので使わせてもらいましたが、実案件で使う場合は、下記の記事のようにGoogleカレンダーとGASなどを使って自前でAPIを作成することをオススメします。↓

この祝日APIは日付の形式が「YYYY-MM-DD」で返ってくるため、onDayCreateイベントでFlatpickerのカレンダーの日付を生成する際にformatDate関数で形式を合わせて祝日かどうか判定しています。祝日の場合はis-holidayというクラスをカレンダーの1マスに付けて、あとはCSSで薄い赤を背景に指定しています。

祝日を反映のデモ

特定の期間を選択不可にする

指定した日付を選択できなくすることができます。
ここでは例として翌日、5日後~10日後、毎週水曜日を選択不可にします。

async function flatpickrInit() {
  //翌日
  const tommorow = new Date();
  tommorow.setDate(tommorow.getDate() + 1);

  //5日後の日時
  const fiveDaysLater = new Date();
  fiveDaysLater.setDate(fiveDaysLater.getDate() + 5);

  //10日後の日時
  const tenDaysLater = new Date();
  tenDaysLater.setDate(tenDaysLater.getDate() + 10);
  
  const holidays = await fetchHolidays();
  flatpickr('#js-datepicker', {
    locale      : Japanese,
    dateFormat  : 'Y.m.d(D)',
    defaultDate : minDate,
    minDate     : minDate,
    maxDate     : maxDate,
    onDayCreate : (dObj, dStr, fp, dayElem) => {
      //祝日はclassをつける
      this.addHolidayClass(dayElem);
    },
    disable     : [
    // 明日を非表示に(文字列で指定)
    this.formatDate(tommorow),
    //5日後 ~ 10日後までを非表示(オブジェクトで指定)
    {
      from: this.formatDate(fiveDaysLater),
      to:  this.formatDate(tenDaysLater)
    },
    //水曜日だけ非表示(無名関数の戻り値で指定)
    (date) => date.getDay() === 3
    ]
  });
}

flatpickrInit();

デモはいつ見ても動作が確認できるようにあえて固定の日付を指定しないようにしているのでちょっとわかりにくいですが、実際は'2021-05-31'みたいに固定の日付の文字列でも、new Date('2021-05-31')のようなDateオブジェクトでもOKです。

特定の期間を選択不可にするデモ

カレンダーと時間を同時に表示

日付選択用のカレンダーに時間選択用のフィールドも追加して表示します。
時間の表示も範囲指定のオプションで9:00〜18:00に制限します。

async function flatpickrInit() {
  const holidays = await fetchHolidays();
  flatpickr('#js-datepicker-5', {
    locale      : Japanese,
    dateFormat  : 'Y.m.d(D)H:i',
    defaultDate : minDate,
    minDate     : minDate,
    maxDate     : maxDate,
    enableTime  : true,
    minTime     : '09:00',
    maxTime     : '18:00',
    onDayCreate : (dObj, dStr, fp, dayElem) => {
      this.addHolidayClass(dayElem);
    }
  });
}

flatpickrInit();

カレンダーと時間を同時に表示するデモ

時間のみ

カレンダーを非表示にして時間のみを表示します。

flatpickr('#js-datepicker', {
  enableTime : true,
  enableTime  : true,
  noCalendar  : true,
  dateFormat  : "H:i",
  defaultDate : minDate,
  time_24hr   : true, //24時間表記
});

時間のみのデモ

日付によって時間の選択範囲を変更①

選択した日付に応じて表示する時間の選択範囲を変更します。
デモではデフォルトが9:00〜18:00、翌日・2日後・3日後を選択すると10:00〜20:00、日曜を選択すると13:00〜20:00に時間の選択範囲が変更されるようにします。

async function flatpickrInit() {
  const holidays = await fetchHolidays();
  flatpickr('#js-datepicker', {
    locale            : Japanese,
    dateFormat        : 'Y.m.d(D)',
    defaultDate   : minDate,
    minDate           : minDate,
    maxDate           : maxDate,
    enableTime  : true,
    minTime     : '09:00',
    maxTime     : '18:00',
    onDayCreate       : (dObj, dStr, fp, dayElem) => {
      addHolidayClass(dayElem, holidays);
    },
    onChange      : (selectedDates, dateStr, instance) => {
      if(!selectedDates[0]){
        return;
      }

      //翌日
      const tommorow = new Date();
      tommorow.setDate(tommorow.getDate() + 1);

      //2日後の日時
      const twoDaysLater = new Date();
      twoDaysLater.setDate(twoDaysLater.getDate() + 2);

      //3日後の日時
      const threeDaysLater = new Date();
      threeDaysLater.setDate(threeDaysLater.getDate() + 3);

      const dates = [
      formatDate(tommorow),
      formatDate(twoDaysLater),
      formatDate(threeDaysLater)
      ];

      const selectDate = this.formatDate(selectedDates[0]);

      //翌日〜3日後は10::00〜20:00
      if(dates.includes(selectDate)){
        instance.set('minTime', '10:00');
        instance.set('maxTime', '20:00');
      }
      //日曜は13:00〜20:00
      else if(selectedDates[0].getDay() === 0){
        instance.set('minTime', '13:00');
        instance.set('maxTime', '20:00');
      }
      else {
        instance.set('minTime', '09:00');
        instance.set('maxTime', '18:00');
      }
    }
  });
}

flatpickrInit();

ポイントはFlatpickrのonChangeイベントと、setメソッドです。
onChangeイベントは、カレンダーの日付が選択された時にトリガーします。setメソッドはオプションを指定すると変更することができます。onChangeイベントの引数でインスタンスが取れるのでそれを使ってinstance.set('minTime', '13:00');のようにして条件によってオプションを都度変更します。

日付によって時間の選択範囲を変更①のデモ

日付によって時間の選択範囲を変更②
(カレンダーと時間の入力フィールドを分ける場合)

Flatpickrの日付選択のカレンダーUIだけを利用して、時間の入力は別の入力エリアを設ける形式にしたい場合。ここでは時間のフィールドは<input type="time">を使ったブラウザのデフォルトUIの表示を使うパターン。

HTMLを調整します。

<form>
  <div class="c-form__row">
    <div class="c-form__row-item">
      <input type="text" name="datepicker-8" id="js-datepicker-8">
    </div>
    <!-- /.c-form__row-item -->
    <div class="c-form__row-item">
      <input type="time" min="09:00" max="18:00" required id="js-time-8">
    </div>
    <!-- /.c-form__row-item -->
  </div>
  <!-- /.c-form__row -->
  <input class="c-form__submit" type="submit" value="Submit(押すとmin、maxが適用される)">
</form>

HTMLで日付用と時間用のフィールドを横並びに設置します。
<input type="time">min属性で最小値、max属性で最大値を設定できるので設定しておきます。min属性とmax属性はフォームがsubmitされないと検証されないので、required属性も加えてsubmitボタンも置いておきます。

function timeChangeByDate(selectedDate) {
  if(!selectedDate){
    return;
  }

  const $time = document.querySelector('#js-time');
  if(!$time){
    return;
  }

  //明日
  const tommorow = new Date();
  tommorow.setDate(tommorow.getDate() + 1);

  //2日後の日時
  const twoDaysLater = new Date();
  twoDaysLater.setDate(twoDaysLater.getDate() + 2);

  //3日後の日時
  const threeDaysLater = new Date();
  threeDaysLater.setDate(threeDaysLater.getDate() + 3);

  const dates = [
  formatDate(tommorow),
  formatDate(twoDaysLater),
  formatDate(threeDaysLater)
  ];

  const selectDay = formatDate(selectedDate);

  //明日〜3日後は10::00〜20:00
  if(dates.includes(selectDay)){
    $time.min = '10:00';
    $time.max = '20:00';
  }
  //日曜は13:00〜20:00
  else if(selectedDate.getDay() === 0){
    $time.min = '13:00';
    $time.max = '20:00';
  }
  //デフォルト09:00〜18:00
  else {
    $time.min = '09:00';
    $time.max = '18:00';
  }
}

async function flatpickrInit() {
  const holidays = await fetchHolidays();
  flatpickr('#js-datepicker', {
    locale            : Japanese,
    dateFormat        : 'Y.m.d(D)',
    defaultDate   : minDate,
    minDate           : minDate,
    maxDate           : maxDate,
    enableTime  : true,
    minTime     : '09:00',
    maxTime     : '18:00',
    onDayCreate       : (dObj, dStr, fp, dayElem) => {
      addHolidayClass(dayElem, holidays);
    },
    onChange      : (selectedDates, dateStr, instance) => timeChangeByDate(selectedDates[0]),
    onClose       : (selectedDates, dateStr, instance) => timeChangeByDate(selectedDates[0]),
    onReady       : (selectedDates, dateStr, instance) => timeChangeByDate(selectedDates[0])
  });
}

flatpickrInit();

カレンダー+時間①とほぼ同じですが、こちらの場合はカレンダーを閉じた時にトリガーするonCloseイベント、カレンダーが準備完了になった時にトリガーするonReadyイベントを加え各タイミングで同じく日時によって時間を制限する処理を実行したいのでその処理を共通の関数(timeChangeByDate)にしました。

これでカレンダーで日付を選んで、次に時間を選んで、最後にsubmitボタンを押した時に、min以上max以下でない場合にバリデーションのメッセージが出ます。

以上で完成。

ここまで作っておいてですが、、、、この方法はイマイチでした。笑
デフォルトUIはまぁ悪くないのですが、時間の範囲指定が反映されないのが厳しいです。ユーザーが時間を選ぶ時はmin以下・max以上の時間でも指定することができるため、submitする瞬間までその時間を選べると思ってしまうのはUX的にも良くないです。この対策のために別でバリデーション付けるのも面倒ですしなんか違う気もしますし。ひとまずデモなのでこのまま公開しますが、実案件ではほぼ100%この方法は使いません。デフォルトUIが改善されるか、いい対策方法があれば良いのですが・・・。

日付によって時間の選択範囲を変更②のデモ

日付によって時間の選択範囲を変更③
(カレンダーと時間の入力フィールドを分ける場合)

こちらは②のパターンを改良して、<input type="time">は使わずにセレクトボックスに選択肢を表示するパターン。

HTMLを修正します。

<div class="c-form__row">
  <div class="c-form__row-item">
    <input type="text" id="js-datepicker">
  </div>
  <!-- /.c-form__row-item -->
  <div class="c-form__row-item">
    <select id="js-time"></select>
  </div>
  <!-- /.c-form__row-item -->
</div>
<!-- /.c-form__row -->

セレクトボックスの中身はJSで追加・変更します。

function addTimeList(min = '00:00', max = '23:59') {
  const $time = document.querySelector('#js-time');
  if(!$time){
    return;
  }

  const today       = formatDate(new Date());
  const minTime     = new Date(`${today} ${min}`);
  const minDateTime = minTime.getTime();
  const maxTime     = new Date(`${today} ${max}`);
  const maxDateTime = maxTime.getTime();

  let optionDom = '';
  for (let i = 0; i < 24; i++) {
    const h = ('00' + i).slice(-2);
    for (let j = 0; j < 60; j++) {
      const m = ('00' + j).slice(-2);
      const optionDate = new Date(`${today} ${h}:${m}`);
      const optionDateTime = optionDate.getTime();
      if(optionDateTime < minDateTime || optionDateTime > maxDateTime){
        continue;
      }
      optionDom += `<option value="${h}:${m}">${h}:${m}</option>`;
    }
  }
  $time.insertAdjacentHTML('beforeend', optionDom);
}

function selectTimeChangeByDate(selectedDate) {
  if(!selectedDate){
    return;
  }

  const $time = document.querySelector('#js-time');
  if(!$time){
    return;
  }

  //明日
  const tommorow = new Date();
  tommorow.setDate(tommorow.getDate() + 1);

  //2日後の日時
  const twoDaysLater = new Date();
  twoDaysLater.setDate(twoDaysLater.getDate() + 2);

  //3日後の日時
  const threeDaysLater = new Date();
  threeDaysLater.setDate(threeDaysLater.getDate() + 3);

  const dates = [
  formatDate(tommorow),
  formatDate(twoDaysLater),
  formatDate(threeDaysLater)
  ];

  $time.innerHTML = '';

  const selectDay = formatDate(selectedDate);

  //明日〜3日後は10:00〜20:00
  if(dates.includes(selectDay)){
    addTimeList('10:00', '20:00');
  }
  //日曜は13:00〜20:00
  else if(selectedDate.getDay() === 0){
    addTimeList('13:00', '20:00');
  }
  //デフォルト09:00〜18:00
  else {
    addTimeList('09:00', '18:00');
  }

}

async function flatpickrInit() {
  const holidays = await fetchHolidays();
  flatpickr('#js-datepicker', {
    locale            : Japanese,
    dateFormat        : 'Y.m.d(D)',
    defaultDate   : minDate,
    minDate           : minDate,
    maxDate           : maxDate,
    enableTime  : true,
    minTime     : '09:00',
    maxTime     : '18:00',
    onDayCreate       : (dObj, dStr, fp, dayElem) => {
      addHolidayClass(dayElem, holidays);
    },
    onChange      : (selectedDates, dateStr, instance) => selectTimeChangeByDate(selectedDates[0]),
    onClose       : (selectedDates, dateStr, instance) => selectTimeChangeByDate(selectedDates[0]),
    onReady       : (selectedDates, dateStr, instance) => selectTimeChangeByDate(selectedDates[0])
  });
}

flatpickrInit();

addTimeList関数を追加し、セレクトボックスの中身にoption要素を必要なだけ追加します。第1引数に最小時間(min)、第2引数に最大時間(max)を入れると、その間の時間一覧を1分刻みでセレクトボックスに追加するような内容を書いています。for文を修正すれば15分刻みや30分刻みに変更できます。

この関数はDOMを削除したり追加したりするのでパフォーマンス的にそこまで良くはないと思いますが、フォームの一箇所に設置するくらいなら全然これで問題ないと思います。jQueryも不要ですし。

日付によって時間の選択範囲を変更③

モバイルでの表示について

モバイルブラウザでアクセスした場合、自動的にブラウザのネイティブUIの日付選択ボックスに変わります。
ネイティブのUIでなく常にFlatpickrのUIを表示したい場合は下記のオプションを追記します。

disableMobile: true

ネイティブのUIでもFlatpickrのイベントはちゃんとトリガーするので問題なく使えます。

個人的にはFlatpickrのUIはモバイルには適さない場合があると思うのでネイティブのUIに変わってもいいのですが、「xx月xx日を選択不可にする」のような特定の日付を無効化する処理が入ると強制的にFlatpickrのUIが適用されます

また、ネイティブのUIでは時間の範囲指定や祝日・日時の範囲指定などはブラウザによっては見た目に反映されないので、別途モバイル専用の対応が必要になるかもしれません。特にFlatpickrの時間選択はカレンダーに比べてスマホなどのタッチデバイスで操作しにくいので、「日付によって時間の選択範囲を変更③」のデモのように日付とは別で時間用にセレクトボックスのフィールドを設けて表示する方がいいかなと思いました。

おわりに

今回はFlatpickrで日時入力をカレンダー表示にする実装方法についてメモしました。

この記事では自分が制作でよく使うであろう実装方法を中心にメモしましたが、オプションが豊富なので他にもいろんな使い方ができそうです。WordPressと組み合わせて、簡易な予約システムを作ることもできます。

日時入力エリアはそこそこ実装する機会が多く、こういったライブラリに頼りがちです。前に使っていたライブラリがいつの間にか使えなくなってる!なんてこともあり得るので、選択肢のひとつとしてFlatpickrも覚えておくといいかなと思います。

記事一覧

RELATED

inoue
TIPS

【CSS + JS】テキストの無限ループアニメーション

inoue
TIPS

【Shopify】カートへの遷移を飛ばしてチェックアウトに進むボタンを設置する

inoue
TIPS

【JS】PhotoSwipe v5を使って画像をポップアップ表示する

inoue
TIPS

gulpでjpg・pngからAVIF画像を生成する

inoue
TIPS

【JS】MutationObserverでDOMを監視[PhotoSwipe(v5)+Swiper(v8)連携]

inoue
TIPS

【Google Sheets API】 スプレッドシートのデータをJSONで取得する

NEW POSTS

inoue
TRY

【WordPress】GA4連携の人気記事ランキング機能を自作プラグイン化してみた

inoue
TRY

【netlify】ビルドイメージを更新 [ Ubuntu Xenial 16.04 → Ubuntu Focal 20.04 ]

inoue
TIPS

【PHP】Google Business Profile APIを使ってクチコミを取得する

inoue
TRY

スクリーンショットのAPI「screendot」を使ってみた

inoue
TIPS

【window.matchMedia】メディアクエリでhoverが使えるデバイスを判定

inoue
TIPS

lax.jsの使い方【スクロール連動アニメーションの実装】

ブログ記事一覧
  • HOME
  • TIPS
  • Flatpickrで日時入力をカレンダー表示にする