はじめに
こんにちは。カイザーです。最近、フロントエンドの勉強をしており、その中でReact Big Calendarでのタイムゾーン対応について、かなりハマったので共有します。
今回、ユーザが自由に(端末の地域設定とは別に)タイムゾーンを選択できるWEBサイトを構築していたのですが、React Big Calendarでは複数タイムゾーンに対応されていませんでした。
Issueでもタイムゾーン対応について議論されていますが、未だに複数タイムゾーンへの対応が組み込まれる気配がありません。(しかも、なんと3年も議論されています。)
この中で、主な開発者であるjquenseさんは、複数タイムゾーンへの対応をしない理由として「React Big CalendarはViewのレンダリングを行うのが責務であり、タイムゾーンの計算は責務外である」としています。
そのため、利用者からReact Big Calndarをラップすることで、複数タイムゾーンへの対応をするコードが投稿されていたため、そちらを参考に、説明していきます。
端末のタイムゾーンのみに対応する場合
これについては、特に難しいことはありません。React Big Calendarへの日時指定はDate型で行いますが、Date型はブラウザのタイムゾーンに依存する形となっているためです。
以下を実行してみると、UTCの2019年5月28日0時0分でイベント登録していますが、端末のタイムゾーンにローカライズされて表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import React from "react"; import ReactDOM from "react-dom"; import BigCalendar from "react-big-calendar"; import moment from "moment"; import "react-big-calendar/lib/css/react-big-calendar.css"; import "./styles.css"; const localizer = BigCalendar.momentLocalizer(moment); const myEventsList = [ { title: "イベント", start: new Date("28 May 2019 00:00:00 +0000"), // UTCの2019/5/28 0時0分 end: new Date("28 May 2019 10:00:00 +0000") } ]; function App() { return ( <div className="App"> <BigCalendar localizer={localizer} events={myEventsList} startAccessor="start" endAccessor="end" /> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); |
こちらから実際に実行できます。
私の環境(Asia/Tokyo)で実行してみると、イベントは9時から19時までとなっています。
ブラウザのタイムゾーンに依存すれば良い場合は、これで良いのですが、ブラウザのタイムゾーンに依存せずに、表示する場合は、日時を変換させる必要があります。
複数のタイムゾーンに対応する。
ブラウザのタイムゾーンに依存せずに、複数のタイムゾーンに対応させるには、React Big Calendarのラッパを作成し、そこで日時を変換させる必要があります。
日時の変換には「moment」を使用します。
日時を指定のタイムゾーンに変換する
「moment」を使って、日時を変換する関数を作成します。
1 2 3 4 5 6 7 8 9 10 | /** * 日時を指定したタイムゾーンに変換する * quoted from: https://github.com/intljusticemission/react-big-calendar/issues/118#issuecomment-301954768 * @param {*} datetime 変換したい日時 (Date) * @param {*} timeZoneName タイムゾーン名(String) */ export const convertDateTimeToDate = (datetime, timeZoneName) => { const m = moment.tz(datetime, timeZoneName); return new Date(m.year(), m.month(), m.date(), m.hour(), m.minute(), 0); }; |
この関数は、先ほどのIssueに投稿されているプログラムからの引用となります。
momentをタイムゾーン指定で使用することで、Dateを「timeZoneName」引数で指定されたタイムゾーンに変換します。
React Big CalendarはDate型を必要としているため、最後に変換されたmomentの日時を使用して、Date型のインスタンスを返却します。
イベントの開始日時・終了日時を動的に設定する
「端末のタイムゾーンのみに対応する場合」では、BigCalendarへ渡すpropsの「startAccessor」と「endAccessor」に、それぞれ開始日時と終了日時が格納されているキーをStringで指定していました。
今回は、先ほどの「convertDateTimeToDate」関数を使用し、動的に日付を変更するため、propsの「startAccessor」と「endAccessor」を関数呼び出しに変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class BigCalendarTz extends Component { startAccessor = event => { return convertDateTimeToDate(accessor(event, "start"), this.props.timeZone); }; endAccessor = event => { return convertDateTimeToDate(accessor(event, "end"), this.props.timeZone); }; render() { return ( <div className="App"> <BigCalendar localizer={localizer} events={myEventsList} startAccessor={this.startAccessor} // startAccessorはタイムゾーンを動的に変化させるため、関数をセット endAccessor={this.endAccessor} // endAccessorはタイムゾーンを動的に変化させるため、関数をセット /> </div> ); } } |
startAccessor関数、endAccessor関数を経由させて、convertDateTimeToDate関数を呼び出しています。
また、BigCalendarに渡すpropsの、「startAccessor」と「endAccessor」に、それぞれ「this.startAccessor」「this.endAccessor」を指定することで、関数が呼び出されるようになります。
これでラップクラス「BigCalendarTz」は完成です。importも含めた全量は以下となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | import React, { Component } from "react"; import BigCalendar from "react-big-calendar"; import moment from "moment"; import { accessor } from "react-big-calendar/lib/utils/accessors"; import "react-big-calendar/lib/css/react-big-calendar.css"; import "./styles.css"; const localizer = BigCalendar.momentLocalizer(moment); const myEventsList = [ { title: "イベント", start: new Date("28 May 2019 00:00:00 +0000"), // UTCの2019/5/28 0時0分 end: new Date("28 May 2019 10:00:00 +0000") } ]; /** * 日時を指定したタイムゾーンに変換する * quoted from: https://github.com/intljusticemission/react-big-calendar/issues/118#issuecomment-301954768 * @param {*} datetime 変換したい日時 (Date) * @param {*} timeZoneName タイムゾーン名(String) */ export const convertDateTimeToDate = (datetime, timeZoneName) => { const m = moment.tz(datetime, timeZoneName); return new Date(m.year(), m.month(), m.date(), m.hour(), m.minute(), 0); }; class BigCalendarTz extends Component { startAccessor = event => { return convertDateTimeToDate(accessor(event, "start"), this.props.timeZone); }; endAccessor = event => { return convertDateTimeToDate(accessor(event, "end"), this.props.timeZone); }; render() { return ( <div className="App"> <BigCalendar localizer={localizer} events={myEventsList} startAccessor={this.startAccessor} // startAccessorはタイムゾーンを動的に変化させるため、関数をセット endAccessor={this.endAccessor} // endAccessorはタイムゾーンを動的に変化させるため、関数をセット /> </div> ); } } export default BigCalendarTz; |
ラップクラス「BigCalendarTz」をタイムゾーン指定付きで呼び出す
呼び出す際は、以下のようになります。
1 2 3 4 5 6 7 | render() { return ( <div className="App"> <BigCalendarTz timeZone={this.state.selectedTimeZone} /> </div> ); } |
この場合はstateのselectedTimeZoneに、String型でタイムゾーン名が入っている前提となります。
サンプルコード
CodeSandboxで実際に実行できます。
ドロップダウンからタイムゾーンを変更させると、リアクティブにカレンダーのイベントが更新されます。
2019年5月28日を選択した上で、タイムゾーンを変更させると、効果が分かりやすいです。
index.jsでタイムゾーンを選択するドロップダウンを実装しており、その内容をpropsでBigCalendarTzに渡しています。
さいごに
React Big Calendarで複数タイムゾーンに対応したい場合は、ぜひ試してみてください。