FullCalendar公式の「FullCalendar React+TypeScript Example Project」を関数コンポーネントで書き替えるチュートリアルの最終回は、カレンダーの左側にパネルを差し込み、さらに公式作例にない機能も加えます。でき上がりはCodeSandboxに公開したつぎのサンプル001でお確かめください。
サンプル001■React + TypeScript + FullCalendar: Example calendar 03
サイドバーを加える
カレンダーの左に差し込むパネルがサイドバーです。操作説明のテキストとチェックボックスをひとつ加えます。新たなモジュールsrc/Sidebar.tsx
に分けて定めることにしましょう(公式「FullCalendar React+TypeScript Example Project」では分けていません)。チェックボックスは、週末(土日)の表示・非表示を切り替えます。
親のルートモジュールから受け取るふたつのプロパティは、チェックボックスのチェックを切り替えたときのonChange
コールバック(toggleWeekends
)と、週末の表示・非表示を示す真偽値の状態変数(weekendsVisible
)です。
interface Props {
toggleWeekends: () => void;
weekendsVisible: boolean;
}
const Sidebar: React.VFC<Props> = ({ toggleWeekends, weekendsVisible }) => (
<div className="demo-app-sidebar">
<div className="demo-app-sidebar-section">
<h2>操作方法</h2>
<ul>
<li>日付を選んでイベント作成</li>
<li>ドラッグ&ドロップでイベントの長さ変更</li>
<li>クリックでイベント削除</li>
</ul>
</div>
<div className="demo-app-sidebar-section">
<label>
<input
type="checkbox"
checked={weekendsVisible}
onChange={toggleWeekends}
/>
週末を表示
</label>
</div>
</div>
);
export default Sidebar;
週末を表示するかどうかは、FullCalendar
コンポーネントのweekends
で切り替えます。デフォルト値は表示するtrue
です。チェックを外すと、土日が隠れるようになりました(図001)。
import Sidebar from "./Sidebar";
function App() {
const [weekendsVisible, setWeekendsVisible] = useState(true);
const handleWeekendsToggle = useCallback(
() => setWeekendsVisible(!weekendsVisible),
[weekendsVisible]
);
return (
<div className="demo-app">
<Sidebar
toggleWeekends={handleWeekendsToggle}
weekendsVisible={weekendsVisible}
/>
<div className="demo-app-main">
<FullCalendar
weekends={weekendsVisible}
/>
</div>
</div>
);
}
図001■チェックボックスで週末の表示・非表示が切り替わる
サイドバーに予定一覧を加える
サイドバーに予定(イベント)を一覧表示しましょう。データはルートコンポーネントApp
の状態変数currentEvents
に配列で収めてありました。これをSidebar
コンポーネントにプロパティとして渡しておきます。
function App() {
return (
<div className="demo-app">
<Sidebar
currentEvents={currentEvents}
/>
</div>
);
}
日付を適切な書式の文字列に整えるのが、formatDate
関数です。第1引数に日付、第2引数には書式が定められたオブジェクトを渡してください(詳しくは「Date Formatting
」参照)。なお、第1引数はnull
でないことを型アサーションするために、非null
アサーション演算子!
が添えてあります。
import { EventApi, formatDate } from "@fullcalendar/react";
interface Props {
currentEvents: EventApi[];
}
const renderSidebarEvent = (event: EventApi) => (
<li key={event.id}>
<b>
{formatDate(event.start!, {
year: "numeric",
month: "short",
day: "numeric",
locale: "ja",
})}
</b>
<i>{event.title}</i>
</li>
);
// const Sidebar: React.VFC<Props> = ({ toggleWeekends, weekendsVisible }) => {
const Sidebar: React.VFC<Props> = ({
currentEvents,
}) => (
<div className="demo-app-sidebar">
<div className="demo-app-sidebar-section">
<h2>予定一覧({currentEvents.length})</h2>
<ul>{currentEvents.map(renderSidebarEvent)}</ul>
</div>
</div>
);
これで、親コンポーネントの状態変数(currentEvents
)の配列から予定データをすべて取り出して、サイドバーのコンポーネント(Sidebar
)に一覧表示できるようになりました(図002)。
図002■予定一覧が加わった
予定リストのビューを加える
FullCalendar
にビューをもうひとつ加えます。つぎのプラグインをインストールしてください。
npm install --save @fullcalendar/list
import
したプラグイン(listPlugin
)は、FullCalendar
コンポーネントのplugins
の配列に収めます。これは予定リストのビューです(図003)。ヘッダの右側ボタンにlistMonth
として加えましょう(「Docs / List View」参照)。公式「FullCalendar React+TypeScript Example Project」には、このビューが含まれていません(この予定リストがあれば、サイドバーの予定一覧はいらなくなりそうです)。
import listPlugin from "@fullcalendar/list";
function App() {
return (
<div className="demo-app">
<div className="demo-app-main">
<FullCalendar
// plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
plugins={[
listPlugin,
]}
headerToolbar={{
// end: "dayGridMonth,timeGridWeek,timeGridDay",
end: "dayGridMonth,timeGridWeek,timeGridDay,listMonth",
}}
/>
</div>
</div>
);
}
図003■予定リストのビューが加わった
日時の書式を変える
公式「FullCalendar React+TypeScript Example Project」にはない追加として、さらに日時の書式を変えてみます。まず、「12時」という漢字表記が少し目障りです(図004)。普通に「12:00」でよいでしょう。
図004■月の日付セルの予定時刻表示
ビューの設定項目は、「月」の日付セルに示された予定時刻と、「週」および「日」の左の時刻欄(図005)のふたつあります。
図005■週の左の時刻欄
前者がeventTimeFormat
、後者はslotLabelFormat
です。フォーマットの定め方は同じで、つぎのように時と分をともに2桁の数値にしました。詳しくは、「eventTimeFormat
」の項をご参照ください。また、nowIndicator
は週と日のビューで、現在時刻が赤線(デフォルト)で示されます。
<FullCalendar
eventTimeFormat={{ hour: "2-digit", minute: "2-digit" }}
slotLabelFormat={[{ hour: "2-digit", minute: "2-digit" }]}
nowIndicator={true}
/>
あとひとつ、月のビューで日付に「日」はなくてよさそうです(前掲図004)。実際、英語表示(デフォルト)では日付には数字しかありません(公式作例参照。ちなみに、時刻はam/pmの12時間表記です)。探してみても、ことさら数字に単位をつけるようなオプション設定が見当たらないのです。
ただ、日付セルに示されるテキストは、dayCellContent
で読み書きできました。そこで、取り出した文字列から「日」を除いて戻すことにより、数字だけにしたのがつぎのコードです。与えるのはコールバックで、引数(event
)からdayNumberText
で日付のテキストを参照して書き替えました。
import FullCalendar, {
DayCellContentArg,
} from "@fullcalendar/react";
function App() {
return (
<div className="demo-app">
<div className="demo-app-main">
<FullCalendar
dayCellContent={(event: DayCellContentArg) =>
(event.dayNumberText = event.dayNumberText.replace("日", ""))
}
/>
</div>
</div>
);
}
ここまで手を加えたのが、冒頭に掲げたサンプル001です。併せて、ふたつのモジュールのコードを、つぎのコード001にまとめておきましょう。
コード001■ルートとサイドバーのモジュール
import { useCallback, useState } from "react";
import FullCalendar, {
DateSelectArg,
DayCellContentArg,
EventApi,
EventClickArg,
EventContentArg,
} from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import allLocales from "@fullcalendar/core/locales-all";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
import listPlugin from "@fullcalendar/list";
import { INITIAL_EVENTS, createEventId } from "./event-utils";
import Sidebar from "./Sidebar";
function App() {
const [currentEvents, setCurrentEvents] = useState<EventApi[]>([]);
const [weekendsVisible, setWeekendsVisible] = useState(true);
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();
}
}, []);
const renderEventContent = (eventContent: EventContentArg) => (
<>
<b>{eventContent.timeText}</b>
<i>{eventContent.event.title}</i>
</>
);
const handleWeekendsToggle = useCallback(
() => setWeekendsVisible(!weekendsVisible),
[weekendsVisible]
);
return (
<div className="demo-app">
<Sidebar
currentEvents={currentEvents}
toggleWeekends={handleWeekendsToggle}
weekendsVisible={weekendsVisible}
/>
<div className="demo-app-main">
<FullCalendar
plugins={[
dayGridPlugin,
timeGridPlugin,
interactionPlugin,
listPlugin,
]}
headerToolbar={{
start: "prev,next today",
center: "title",
end: "dayGridMonth,timeGridWeek,timeGridDay,listMonth",
}}
eventTimeFormat={{ hour: "2-digit", minute: "2-digit" }}
slotLabelFormat={[{ hour: "2-digit", minute: "2-digit" }]}
initialView="dayGridMonth"
eventContent={renderEventContent}
selectable={true}
editable={true}
selectMirror={true}
dayMaxEvents={true}
navLinks={true}
businessHours={true}
nowIndicator={true}
weekends={weekendsVisible}
initialEvents={INITIAL_EVENTS}
locales={allLocales}
locale="ja"
eventsSet={handleEvents}
select={handleDateSelect}
eventClick={handleEventClick}
dayCellContent={(event: DayCellContentArg) =>
(event.dayNumberText = event.dayNumberText.replace("日", ""))
}
/>
</div>
</div>
);
}
export default App;
import { EventApi, formatDate } from "@fullcalendar/react";
interface Props {
currentEvents: EventApi[];
toggleWeekends: () => void;
weekendsVisible: boolean;
}
const renderSidebarEvent = (event: EventApi) => (
<li key={event.id}>
<b>
{formatDate(event.start!, {
year: "numeric",
month: "short",
day: "numeric",
locale: "ja",
})}
</b>
<i>{event.title}</i>
</li>
);
const Sidebar: React.VFC<Props> = ({
currentEvents,
toggleWeekends,
weekendsVisible,
}) => (
<div className="demo-app-sidebar">
<div className="demo-app-sidebar-section">
<h2>操作方法</h2>
<ul>
<li>日付を選んでイベント作成</li>
<li>ドラッグ&ドロップでイベントの長さ変更</li>
<li>クリックでイベント削除</li>
</ul>
</div>
<div className="demo-app-sidebar-section">
<label>
<input
type="checkbox"
checked={weekendsVisible}
onChange={toggleWeekends}
/>
週末を表示
</label>
</div>
<div className="demo-app-sidebar-section">
<h2>予定一覧({currentEvents.length})</h2>
<ul>{currentEvents.map(renderSidebarEvent)}</ul>
</div>
</div>
);
export default Sidebar;
シリーズ
「FullCalendar React+TypeScript Example Projectを関数コンポーネントで書いてみる 01」
「FullCalendar React+TypeScript Example Projectを関数コンポーネントで書いてみる 02」