FullCalendar React+TypeScript Example Projectを関数コンポーネントで書いてみる 03

FullCalendar公式の「[FullCalendar React+TypeScript Example Project](https://github.com/fullcalendar/fullcalendar-example-projects/tree/2fb4a584a04684b1c3c7e77920fd2d0a8c6b0da0/react-typescript)」を関数コンポーネントで書き替えるチュートリアルの最終回は、カレンダーの左側にパネルを差し込み、さらに公式作例にない機能も加えます。でき上がりはCodeSandboxに公開したつぎのサンプル001でお確かめください。

#### サンプル001■React + TypeScript + FullCalendar: Example calendar 03
[![2107003_006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/55bf3de5-0dbc-2b8f-857d-1825e52d8726.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-typescript-fullcalendar-example-calendar-03-4hw1t?file=/src/App.tsx)

# サイドバーを加える
カレンダーの左に差し込むパネルがサイドバーです。操作説明のテキストとチェックボックスをひとつ加えます。新たなモジュール`src/Sidebar.tsx`に分けて定めることにしましょう(公式「[FullCalendar React+TypeScript Example Project](https://github.com/fullcalendar/fullcalendar-example-projects/tree/2fb4a584a04684b1c3c7e77920fd2d0a8c6b0da0/react-typescript)」では分けていません)。チェックボックスは、週末(土日)の表示・非表示を切り替えます。

親のルートモジュールから受け取るふたつのプロパティは、チェックボックスのチェックを切り替えたときの`onChange`コールバック(`toggleWeekends`)と、週末の表示・非表示を示す真偽値の状態変数(`weekendsVisible`)です。

```react:src/Sidebar.tsx
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>ドラッグ&amp;ドロップでイベントの長さ変更</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`](https://fullcalendar.io/docs/weekends)で切り替えます。デフォルト値は表示する`true`です。チェックを外すと、土日が隠れるようになりました(図001)。

```react:src/App.tsx
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■チェックボックスで週末の表示・非表示が切り替わる
![2107003_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ad6b9fbd-b67a-9e91-4cda-07f055bbe009.png)


# サイドバーに予定一覧を加える
サイドバーに予定(イベント)を一覧表示しましょう。データはルートコンポーネント`App`の状態変数`currentEvents`に配列で収めてありました。これを`Sidebar`コンポーネントにプロパティとして渡しておきます。

```react:src/App.tsx
function App() {

  return (
    <div className="demo-app">
      <Sidebar
        currentEvents={currentEvents}

      />

    </div>
  );
}
```

日付を適切な書式の文字列に整えるのが、[`formatDate`](https://fullcalendar.io/docs/formatDate)関数です。第1引数に日付、第2引数には書式が定められたオブジェクトを渡してください(詳しくは「[`Date Formatting`](https://fullcalendar.io/docs/date-formatting)」参照)。なお、第1引数は`null`でないことを型アサーションするために、[非`null`アサーション演算子`!`](https://fumiononaka.com/Business/html5/FN2106001.html#sec01-03)が添えてあります。

```react:src/Sidebar.tsx
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■予定一覧が加わった
![2107003_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/5478d362-89dc-113a-3eff-80a4e2cf0b1b.png)


# 予定リストのビューを加える
`FullCalendar`にビューをもうひとつ加えます。つぎのプラグインをインストールしてください。

```terminal
npm install --save @fullcalendar/list
```

`import`したプラグイン(`listPlugin`)は、`FullCalendar`コンポーネントの`plugins`の配列に収めます。これは予定リストのビューです(図003)。ヘッダの右側ボタンに`listMonth`として加えましょう(「[Docs / List View](https://fullcalendar.io/docs/list-view)」参照)。公式「FullCalendar React+TypeScript Example Project」には、このビューが含まれていません(この予定リストがあれば、サイドバーの予定一覧はいらなくなりそうです)。

```react:src/App.tsx
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■予定リストのビューが加わった
![2107003_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/6fdd81b7-95d3-3218-6331-593548cb86de.png)


# 日時の書式を変える
公式「FullCalendar React+TypeScript Example Project」にはない追加として、さらに日時の書式を変えてみます。まず、「12時」という漢字表記が少し目障りです(図004)。普通に「12:00」でよいでしょう。

#### 図004■月の日付セルの予定時刻表示
![2107003_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/d5bcdaf9-0cfb-d849-ee59-385ca38da8d1.png)

ビューの設定項目は、「月」の日付セルに示された予定時刻と、「週」および「日」の左の時刻欄(図005)のふたつあります。

#### 図005■週の左の時刻欄
![2107003_005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/74fca224-3bec-8fc4-64dd-999cb1c8d382.png)

前者が[`eventTimeFormat`](https://fullcalendar.io/docs/eventTimeFormat)、後者は[`slotLabelFormat`](https://fullcalendar.io/docs/slotLabelFormat)です。フォーマットの定め方は同じで、つぎのように時と分をともに2桁の数値にしました。詳しくは、「[`eventTimeFormat`](https://fullcalendar.io/docs/eventTimeFormat)」の項をご参照ください。また、[`nowIndicator`](https://fullcalendar.io/docs/nowIndicator)は週と日のビューで、現在時刻が赤線(デフォルト)で示されます。

```react:src/App.tsx
<FullCalendar

  eventTimeFormat={{ hour: "2-digit", minute: "2-digit" }}
  slotLabelFormat={[{ hour: "2-digit", minute: "2-digit" }]}

  nowIndicator={true}

/>
```

あとひとつ、月のビューで日付に「日」はなくてよさそうです(前掲図004)。実際、英語表示(デフォルト)では日付には数字しかありません([公式作例](https://fullcalendar.io/docs/initialize-globals-demo)参照。ちなみに、時刻はam/pmの12時間表記です)。探してみても、ことさら数字に単位をつけるようなオプション設定が見当たらないのです。

ただ、日付セルに示されるテキストは、[`dayCellContent`](https://fullcalendar.io/docs/day-cell-render-hooks)で読み書きできました。そこで、取り出した文字列から「日」を除いて戻すことにより、数字だけにしたのがつぎのコードです。与えるのはコールバックで、引数(`event`)から`dayNumberText`で日付のテキストを参照して書き替えました。

```react:src/App.tsx
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■ルートとサイドバーのモジュール
```react:src/App.tsx
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;
```

```react:src/Sidebar.tsx
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>ドラッグ&amp;ドロップでイベントの長さ変更</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](https://qiita.com/FumioNonaka/items/936ca66c78361a02bbd4)」
「[FullCalendar React+TypeScript Example Projectを関数コンポーネントで書いてみる 02](https://qiita.com/FumioNonaka/items/03645bc5e92237c4b143)」