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

[FullCalendar](https://fullcalendar.io/)は、さまざまにカスタマイズしたカレンダーがつくれるオープンソースのJavaScriptライブラリです。ReactやVueそしてAngularといったフレームワークにも対応しています。とくに、Reactについては、バーチャルDOMノードを生成するという踏み込んだつくり込みです。

ただ、残念ながらReactを用いた情報があまりありません。まして、TypeScriptまで加えると、ほぼ見当たらない状況です。そうした中で、拠りどころとなるのがFullCalendar公式の「[FullCalendar React+TypeScript Example Project](https://github.com/fullcalendar/fullcalendar-example-projects/tree/2fb4a584a04684b1c3c7e77920fd2d0a8c6b0da0/react-typescript)」でしょう。本シリーズ記事(全3回)は、このコードをもとにして、チュートリアルのかたちで解説します。

ただし、公式のコードはクラスを用いています。これは、今主流になりつつある関数コンポーネントに書き替えました。ほかにも、モジュール分けしたり、知っておくと便利な機能を加えたり、解説も工夫したつもりです。

#### サンプル001■React + TypeScript + FullCalendar: Example calendar 01
[![2107001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/f2515c91-e253-cbb6-316b-7d373448e654.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-typescript-fullcalendar-example-calendar-01-64jit)

なお、FullCalendarにまだ触れたことがない方は、先に「[React + TypeScript + FullCalendar: 簡単なカレンダーをつくる](https://qiita.com/FumioNonaka/items/1810f7e211638988af62)」をお読みいただくとよいでしょう。


# カレンダーを表示する
Reactアプリケーションのひな型は、Create React Appを使ってつくります(「[React + TypeScriptのひな形作成とFullCalendarのインストール](https://qiita.com/FumioNonaka/items/1810f7e211638988af62#react--typescript%E3%81%AE%E3%81%B2%E3%81%AA%E5%BD%A2%E4%BD%9C%E6%88%90%E3%81%A8fullcalendar%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)」)。

```terminal
npx create-react-app react-typescript-fullcalendar --template typescript
```

そして、プロジェクトのディレクトリ(`react-typescript-fullcalendar`)に切り替えたら、FullCalendarに加えて、プラグインを3つインストールしてください。

```terminal
npm install --save @fullcalendar/react @fullcalendar/daygrid @fullcalendar/core @fullcalendar/interaction
```

まずは、ユーティリティのモジュール(`src/event-utils.ts`)に、予定のデータ(`INITIAL_EVENTS`)を配列で定めます(コード001)。予定(イベントオブジェクト)に加えるプロパティはつぎの表001の3つです(「[Event Object](https://fullcalendar.io/docs/event-object)」参照)。`id`には一意の数字が与えられるよう、関数(`createEventId()`)を`export`しました。日付はYYYY-MM-DD形式で、時刻も添える場合は`T`で結んでください(「[日付と時刻の組合せ](https://ja.wikipedia.org/wiki/ISO_8601#%E6%97%A5%E4%BB%98%E3%81%A8%E6%99%82%E5%88%BB%E3%81%AE%E7%B5%84%E5%90%88%E3%81%9B)」参照)。

#### 表001■イベントオブジェクトのプロパティ1
|プロパティ |説明 |
|:--|:--|
|id |イベントを示す一意の識別子となる文字列。[`getEventById`](https://fullcalendar.io/docs/Calendar-getEventById)に用いることができる。 |
|title |イベントとしてカレンダーに示されるタイトルの文字列。 |
|start |イベントが始まる日時を示す文字列か数値または[`Date`](https://fullcalendar.io/docs/date-object)オブジェクト。 |


#### コード001■ユーティリティモジュール
```react:src/event-utils.ts
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の設定](https://qiita.com/FumioNonaka/items/1810f7e211638988af62#%E6%97%A5%E6%9C%AC%E8%AA%9E%E8%A1%A8%E7%A4%BA%E3%81%AB%E3%81%99%E3%82%8B-%E3%83%BC-locale%E3%81%AE%E8%A8%AD%E5%AE%9A)」参照)。あとのふたつは、`plugins`に配列で加えてください。`initialView`は、標準的な月単位の`"dayGridMonth"`です。

```react:src/App.tsx
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`](https://github.com/fullcalendar/fullcalendar-example-projects/blob/2fb4a584a04684b1c3c7e77920fd2d0a8c6b0da0/react-typescript/src/main.css)の定めに少しだけ手を加えました。冒頭に掲げたCodeSandboxサンプル001の`src/styles.css`をご覧ください。


# イベントオブジェクトを取得する
[`eventsSet`](https://fullcalendar.io/docs/eventsSet)は[イベントオブジェクト](https://fullcalendar.io/docs/event-object)を取り出すためのハンドラです。予定のデータが、初期化されたり、変更になったときに呼び出されます。今は、データが変えられませんから、呼び出されるのは初期化時のみです。そこで、データをコンソールに出力して確かめることにしました。なお、コールバック(`handleEvents()`)は`useCallback`フックで包むのが、Reactのお約束です(「[Create React App 入門 09: useCallbackフックで無駄な処理を省く](https://www.fumiononaka.com/Business/html5/FN2102003.html)」参照)。


```react:src/App.tsx
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`](https://fullcalendar.io/docs/selectable)を`true`にして日付が選べるようにしておかなければなりません。そのうえで、選んだときのイベントハンドラは[`select`](https://fullcalendar.io/docs/select-callback)です。

[`window.prompt()`](https://developer.mozilla.org/docs/Web/API/Window/prompt)メソッドはダイアログを開き、ユーザーの入力した文字列が返されます。前後のスペースは[`String.prototype.trim()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/trim)で除きました。コールバック(`handleDateSelect()`)が受け取る引数(`selectInfo`)の[`view`](https://fullcalendar.io/docs/view-object)プロパティから取り出す`calendar`が、このカレンダーを操作するオブジェクト(`calendarApi`)です。

カレンダーオブジェクト(`calendarApi`)に対して[`addEvent()`](https://fullcalendar.io/docs/Calendar-addEvent)メソッドを呼び出すと、引数のイベントオブジェクトが予定データの配列に加えられます。前掲コード001の予定データの初期値(`INITIAL_EVENTS`)より、ふたつプロパティが増えました。説明は以下の表002のとおりです。

```react:src/App.tsx
// 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()`](https://fullcalendar.io/docs/Calendar-unselect)は、日付が選択されていない状態に戻します。そして、`text`を条件判定したのは、ユーザーがタイトルを書き込んでいないか、スペースしか入力していない場合に、予定の追加を取りやめるためです。


# 予定をクリックで削除する
予定が加えられるようになったので、つぎは削除です。予定の欄をクリックしたら、ダイアログで確認して消してしまえるようにしましょう。追加した予定が書き替えられるようにするには、`FullCalendar`コンポーネントの[`editable`](https://fullcalendar.io/docs/editable)プロパティに`true`を与えなければなりません。そして、クリックしたときのイベントハンドラが[`eventClick`](https://fullcalendar.io/docs/eventClick)です。

コールバック(`handleEventClick()`)が受け取る引数のオブジェクトからは、`event`プロパティでそのイベントオブジェクトが受け取れます。削除してよいか確認のダイアログを開くのが[`window.confirm()`](https://developer.mozilla.org/ja/docs/Web/API/Window/confirm)メソッドです。[OK]ボタンを押せば`true`が返りますので、イベントオブジェクトに対して[`remove()`](https://fullcalendar.io/docs/Event-remove)を呼び出すことにより削除できます。

```react:src/App.tsx
// 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■ルートモジュール
```react:src/App.tsx
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](https://qiita.com/FumioNonaka/items/03645bc5e92237c4b143)」
「[FullCalendar React+TypeScript Example Projectを関数コンポーネントで書いてみる 03](https://qiita.com/FumioNonaka/items/22173f5a7999c09d123f)」