FullCalendarは、さまざまにカスタマイズしたカレンダーがつくれるオープンソースのJavaScriptライブラリです。ReactやVueそしてAngularといったフレームワークにも対応しています。とくに、Reactについては、バーチャルDOMノードを生成するという踏み込んだつくり込みです。
ただ、残念ながらReactを用いた情報があまりありません。まして、TypeScriptまで加えると、ほぼ見当たらない状況です。そうした中で、拠りどころとなるのがFullCalendar公式の「FullCalendar React+TypeScript Example Project」でしょう。本シリーズ記事(全3回)は、このコードをもとにして、チュートリアルのかたちで解説します。
ただし、公式のコードはクラスを用いています。これは、今主流になりつつある関数コンポーネントに書き替えました。ほかにも、モジュール分けしたり、知っておくと便利な機能を加えたり、解説も工夫したつもりです。
サンプル001■React + TypeScript + FullCalendar: Example calendar 01
なお、FullCalendarにまだ触れたことがない方は、先に「React + TypeScript + FullCalendar: 簡単なカレンダーをつくる」をお読みいただくとよいでしょう。
カレンダーを表示する
Reactアプリケーションのひな型は、Create React Appを使ってつくります(「React + TypeScriptのひな形作成とFullCalendarのインストール」)。
npx create-react-app react-typescript-fullcalendar --template typescript
そして、プロジェクトのディレクトリ(react-typescript-fullcalendar
)に切り替えたら、FullCalendarに加えて、プラグインを3つインストールしてください。
npm install --save @fullcalendar/react @fullcalendar/daygrid @fullcalendar/core @fullcalendar/interaction
まずは、ユーティリティのモジュール(src/event-utils.ts
)に、予定のデータ(INITIAL_EVENTS
)を配列で定めます(コード001)。予定(イベントオブジェクト)に加えるプロパティはつぎの表001の3つです(「Event Object」参照)。id
には一意の数字が与えられるよう、関数(createEventId()
)をexport
しました。日付はYYYY-MM-DD形式で、時刻も添える場合はT
で結んでください(「日付と時刻の組合せ」参照)。
表001■イベントオブジェクトのプロパティ1
プロパティ | 説明 |
---|---|
id | イベントを示す一意の識別子となる文字列。getEventById に用いることができる。 |
title | イベントとしてカレンダーに示されるタイトルの文字列。 |
start | イベントが始まる日時を示す文字列か数値またはDate オブジェクト。 |
コード001■ユーティリティモジュール
import { EventInput } from "@fullcalendar/react";
let eventGuid = 0;
const todayStr = new Date().toISOString().replace(/T.*$/, ""); // 今日の日付をYYYY-MM-DD形式にする
export const createEventId = () => String(eventGuid++);
export const INITIAL_EVENTS: EventInput[] = [
{
id: createEventId(),
title: "All-day event",
start: todayStr,
},
{
id: createEventId(),
title: "Timed event",
start: todayStr + "T12:00:00", // 時刻はTで結ぶ
},
];
つぎに、ルートモジュールsrc/App.tsx
です。ユーティリティモジュールからimport
した予定データ(INITIAL_EVENTS
)は、FullCalendar
コンポーネントのinitialEvents
に与えます。また、3つ読み込んだプラグインのうち、allLocales
はlocales
に定めましょう(「日本語表示にする ー localeの設定」参照)。あとのふたつは、plugins
に配列で加えてください。initialView
は、標準的な月単位の"dayGridMonth"
です。
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import allLocales from '@fullcalendar/core/locales-all';
import interactionPlugin from "@fullcalendar/interaction";
import { INITIAL_EVENTS } from "./event-utils";
function App() {
return (
<div className="demo-app">
<div className="demo-app-main">
<FullCalendar
plugins={[dayGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
initialEvents={INITIAL_EVENTS}
locales={allLocales}
locale="ja"
/>
</div>
</div>
);
}
export default App;
CSSは公式プロジェクトのsrc/main.css
の定めに少しだけ手を加えました。冒頭に掲げたCodeSandboxサンプル001のsrc/styles.css
をご覧ください。
イベントオブジェクトを取得する
eventsSet
はイベントオブジェクトを取り出すためのハンドラです。予定のデータが、初期化されたり、変更になったときに呼び出されます。今は、データが変えられませんから、呼び出されるのは初期化時のみです。そこで、データをコンソールに出力して確かめることにしました。なお、コールバック(handleEvents()
)はuseCallback
フックで包むのが、Reactのお約束です(「Create React App 入門 09: useCallbackフックで無駄な処理を省く」参照)。
import { useCallback, useState } from "react";
// import FullCalendar from "@fullcalendar/react";
import FullCalendar, { EventApi } from "@fullcalendar/react";
function App() {
const [currentEvents, setCurrentEvents] = useState<EventApi[]>([]);
const handleEvents = useCallback((events: EventApi[]) => {
console.log("events:", events); // 確認用
setCurrentEvents(events);
}, []);
return (
<div className="demo-app">
<div className="demo-app-main">
<FullCalendar
eventsSet={handleEvents}
/>
</div>
</div>
);
}
予定を入力できるようにする
それでは、カレンダーに予定を加えられるようにしましょう。そのためには、FullCalendar
コンポーネントのselectable
をtrue
にして日付が選べるようにしておかなければなりません。そのうえで、選んだときのイベントハンドラはselect
です。
window.prompt()
メソッドはダイアログを開き、ユーザーの入力した文字列が返されます。前後のスペースはString.prototype.trim()
で除きました。コールバック(handleDateSelect()
)が受け取る引数(selectInfo
)のview
プロパティから取り出すcalendar
が、このカレンダーを操作するオブジェクト(calendarApi
)です。
カレンダーオブジェクト(calendarApi
)に対してaddEvent()
メソッドを呼び出すと、引数のイベントオブジェクトが予定データの配列に加えられます。前掲コード001の予定データの初期値(INITIAL_EVENTS
)より、ふたつプロパティが増えました。説明は以下の表002のとおりです。
// import FullCalendar, { EventApi } from "@fullcalendar/react";
import FullCalendar, { DateSelectArg, EventApi } from "@fullcalendar/react";
// import { INITIAL_EVENTS } from "./event-utils";
import { INITIAL_EVENTS, createEventId } from './event-utils'
function App() {
const handleDateSelect = useCallback((selectInfo: DateSelectArg) => {
let title = prompt("イベントのタイトルを入力してください")?.trim();
let calendarApi = selectInfo.view.calendar;
calendarApi.unselect();
if (title) {
calendarApi.addEvent({
id: createEventId(),
title,
start: selectInfo.startStr,
end: selectInfo.endStr,
allDay: selectInfo.allDay,
});
}
}, []);
return (
<div className="demo-app">
<div className="demo-app-main">
<FullCalendar
selectable={true}
select={handleDateSelect}
/>
</div>
</div>
);
}
表002■イベントオブジェクトのプロパティ2
プロパティ | 説明 |
---|---|
end | イベントが終わる日時を示す文字列か数値またはDate オブジェクト。 |
allDay | 終日のイベントとするかどうかの真偽値。true の場合、時間は示されない。 |
addEvent()
メソッドを呼び出す前に、処理がふたつありました。unselect()
は、日付が選択されていない状態に戻します。そして、text
を条件判定したのは、ユーザーがタイトルを書き込んでいないか、スペースしか入力していない場合に、予定の追加を取りやめるためです。
予定をクリックで削除する
予定が加えられるようになったので、つぎは削除です。予定の欄をクリックしたら、ダイアログで確認して消してしまえるようにしましょう。追加した予定が書き替えられるようにするには、FullCalendar
コンポーネントのeditable
プロパティにtrue
を与えなければなりません。そして、クリックしたときのイベントハンドラがeventClick
です。
コールバック(handleEventClick()
)が受け取る引数のオブジェクトからは、event
プロパティでそのイベントオブジェクトが受け取れます。削除してよいか確認のダイアログを開くのがwindow.confirm()
メソッドです。[OK]ボタンを押せばtrue
が返りますので、イベントオブジェクトに対してremove()
を呼び出すことにより削除できます。
// import FullCalendar, { DateSelectArg, EventApi } from "@fullcalendar/react";
import FullCalendar, {
DateSelectArg,
EventApi,
EventClickArg,
} from "@fullcalendar/react";
function App() {
const handleEventClick = useCallback((clickInfo: EventClickArg) => {
if (
window.confirm(`このイベント「${clickInfo.event.title}」を削除しますか`)
) {
clickInfo.event.remove();
}
}, []);
return (
<div className="demo-app">
<div className="demo-app-main">
<FullCalendar
editable={true}
eventClick={handleEventClick}
/>
</div>
</div>
);
}
これでカレンダーに予定を書き込んだり、消したりできるようになりました。ルートモジュールsrc/App.tsx
の記述全体は、つぎのコード002のとおりです。具体的な動きについては、CodeSandboxに公開した冒頭のサンプル001でお確かめください。
コード002■ルートモジュール
import { useCallback, useState } from "react";
import FullCalendar, {
DateSelectArg,
EventApi,
EventClickArg,
} from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import allLocales from '@fullcalendar/core/locales-all';
import interactionPlugin from "@fullcalendar/interaction";
import { INITIAL_EVENTS, createEventId } from './event-utils'
function App() {
const [currentEvents, setCurrentEvents] = useState<EventApi[]>([]);
const handleEvents = useCallback((events: EventApi[]) =>
setCurrentEvents(events)
, []);
const handleDateSelect = useCallback((selectInfo: DateSelectArg) => {
let title = prompt("イベントのタイトルを入力してください")?.trim();
let calendarApi = selectInfo.view.calendar;
calendarApi.unselect();
if (title) {
calendarApi.addEvent({
id: createEventId(),
title,
start: selectInfo.startStr,
end: selectInfo.endStr,
allDay: selectInfo.allDay,
});
}
}, []);
const handleEventClick = useCallback((clickInfo: EventClickArg) => {
if (
window.confirm(`このイベント「${clickInfo.event.title}」を削除しますか`)
) {
clickInfo.event.remove();
}
}, []);
return (
<div className="demo-app">
<div className="demo-app-main">
<FullCalendar
plugins={[dayGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
selectable={true}
editable={true}
initialEvents={INITIAL_EVENTS}
locales={allLocales}
locale="ja"
eventsSet={handleEvents}
select={handleDateSelect}
eventClick={handleEventClick}
/>
</div>
</div>
);
}
export default App;
シリーズ
「FullCalendar React+TypeScript Example Projectを関数コンポーネントで書いてみる 02」
「FullCalendar React+TypeScript Example Projectを関数コンポーネントで書いてみる 03」