Next.js + Fullcalendar v5 を攻略する

[hey アドベントカレンダー2020](https://tech.hey.jp/entry/2020/12/01/172331)の8日目の記事です。

Next.js + FullCalendar v5を使う際の諸々の知見がたまったのでまとめました。

## 概要

カレンダーをさくっと作成できるライブラリ [FullCalendar](https://fullcalendar.io/) を Next.js (+TypeScript)上で使う際のポイントをまとめます。割と初級者向けの内容になります。

## 導入

### セットアップ

まずは通常？通りNext.js + TypeScriptの環境をセットアップします。
[Next.js - Create Next App](https://nextjs.org/docs/api-reference/create-next-app)

```bash
$ yarn create next-app --example with-typescript
```

そして、FullCalendarを扱うため、パッケージを追加します。
FullCalendarはReact用のパッケージが用意されているのでそちら+、必要な機能に応じてプラグインを追加する形になります。
下のコマンドだと `@fullcalendar/daygrid` がカレンダーの見た目に関するプラグインです。
[FullCalendar - React Component](https://fullcalendar.io/docs/react)
[FullCaleddar - Plugin Index](https://fullcalendar.io/docs/plugin-index)

```bash
yarn add @fullcalendar/react @fullcalendar/daygrid
```

パッケージを追加できたら、FullCalendar Componentを任意のcomponentへ追加してみましょう。

```tsx:pages/index.tsx
import Layout from "../components/Layout";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";

const IndexPage = () => (
  <Layout>
    <FullCalendar
      plugins={[dayGridPlugin]}
      initialEvents={[{ title: "initial event", start: new Date() }]}
    />
  </Layout>
);

export default IndexPage;
```

docの通り普通にpropsなどを設定してComponentを作成しています。
`yarn dev` などで確かめてみましょう。

```bash
$ yarn dev
yarn run v1.16.0
$ next
ready - started server on http://localhost:3000
event - compiled successfully
event - build page: /
wait  - compiling...
error - ./node_modules/@fullcalendar/common/main.css
Global CSS cannot be imported from within node_modules.
Read more: https://err.sh/next.js/css-npm
Location: node_modules/@fullcalendar/common/main.js
Could not find files for / in .next/build-manifest.json
Could not find files for / in .next/build-manifest.json
```

!?!???!?!??!????

### Global CSS cannot be imported from within node_modules.

`Global CSS cannot be imported from within node_modules.` というエラーが出てしまいますね..こちらを解決していきましょう。

まずNext.jsではnode_modules内のファイルからcssがimportされることが禁止
されています。
[CSS Imported by a Dependency](https://github.com/vercel/next.js/blob/master/errors/css-npm.md)

そこで[FullCalendarの実装](https://github.com/fullcalendar/fullcalendar/blob/master/packages/common/src/main.ts)を確認してみると..

```ts
import './main.css'

// exports
// --------------------------------------------------------------------------------------------------

export const version: string = '<%= version %>' // important to type it, so .d.ts has generic string

// explicit API
...(以下略)
```

がっつりimportされていますね、どうやらこちらがエラーを出している原因なようです。
これは詰みというやつなのでしょうか..?

### 解決

Next.jsはポピュラーなフレームワーク（※個人の感想です）、FullCalendar側も何か対策はあるはず..?
ググラビリティを駆使すると次のissueにぶつかります。

["Global CSS cannot be imported from within node_modules." error in Next.js](https://github.com/fullcalendar/fullcalendar/issues/5393)

まさにこれ！なやつが見つかりました。
詳しいことはこちらのissueをみていただくとして、解決方法はこちらにまとまっています。

[fullcalendar/fullcalendar-example-projects/next#workarounds-explained](https://github.com/fullcalendar/fullcalendar-example-projects/tree/master/next#workarounds-explained)
(こちらの解決方法でやや古い箇所や自分が初見で理解できなかった箇所を補いつつ解説していきます。)

### next-transpile-modules

まず、[next-transpile-modules](https://www.npmjs.com/package/next-transpile-modules) こちらを使ってESMで書かれているFullCalendarをトランスパイル可能にします。
こちらのパッケージの最新版は `v6.0.0` ですがv5以上だと諸事情で動かないので、 `v4.1.0` を入れます。

```bash
$ yarn add next-transpile-modules@^4.1.0
```

そしておそらくデフォルトでは入っていない `next.config.js` を加えましょう。

```js:next.config.js
const withTM = require("next-transpile-modules")(["@fullcalendar"]);

module.exports = withTM({});
```

### babel-plugin-transform-require-ignore

そして次は[babel-plugin-transform-require-ignore](https://www.npmjs.com/package/babel-plugin-transform-require-ignore)をインストールします。

```bash
$ yarn add babel-plugin-transform-require-ignore
```

そしてbableの設定ファイルを加えます。

```js:babel.config.js
module.exports = {
  presets: ["next/babel"],
  overrides: [
    {
      include: ["./node_modules"],
      plugins: [
        [
          "babel-plugin-transform-require-ignore",
          {
            extensions: [".css"],
          },
        ],
      ],
    },
  ],
};
```

ポイントはoverrides部分です。この部分によって `".node_modules"` 内のファイルから `.css` をインポートしている部分を削除することができます。
`"./node_modules"` の箇所は `"./node_modules/@fullcalendar"` などとしても構いません。
またbabelがv7以上の場合 [.babelrc](https://babeljs.io/docs/en/config-files) だと動かないので注意です。
(Next.jsを昔からアップデートして運用している方などは注意が必要かもしれません。)

ここまで来るとコンパイル可能になり `yarn dev` などで表示することは可能になります。
↓こんな感じで表示されるかと思います。

![Screen Shot 2020-12-07 at 2.52.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94835/667bedd2-2b67-f5a7-9ce9-8ecc1fe32082.png)

cssが削除されてしまっているので、手動でimportする必要があります。

### css

cssをimportする箇所は、Next.js 9.5.4までは `pages/_app.js(ts)` 内で行う必要がありましたが、それ以降は実際に利用する箇所でimport可能です。

```tsx:pages/index.tsx
import Layout from "../components/Layout";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import '@fullcalendar/common/main.css'
import '@fullcalendar/daygrid/main.css'

const IndexPage = () => (
..以下略

```

これでいよいよ正常に表示することが可能になりました！

![Screen Shot 2020-12-07 at 3.06.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94835/00c2d4b0-dbad-6f14-6b8b-d4b7ec5e0fa7.png)

## カスタマイズ

それではFullCalendarのカスタマイズをみていきましょう。
FullCalendarのカスタマイズはかなり色々設定できるので公式ドキュメントを参考にしながら試してみてください。また先ほど述べたようにpluginでも色々できることはあります。

またTypeScriptだとFullCalendarが型定義を色々用意してくれているので実装が楽になるのでおすすめです。

本記事内では基本的なことに触れる程度にします。
[FullCalendar - doc](https://fullcalendar.io/docs#toc)

### イベントの表示

FullCalendarでは基本的に日付に合わせてEventを表示することができます。
FullCalendar Componentのevents propsにてそれが可能になります。

```tsx:pages/index.tsx

const IndexPage = () => (
  <Layout>
    <FullCalendar
      plugins={[dayGridPlugin]}
      events={[{ title: "initial event", start: new Date() }]}
    />
  </Layout>
);

```

events には基本的に [Event Object](https://fullcalendar.io/docs/event-object) ( `EventInput` type) を作成してその配列を渡す形になりますが、その渡し方は単純なデータだけでなく、[Function](https://github.com/fullcalendar/fullcalendar/blob/master/packages/common/src/event-sources/func-event-source.ts#L8)も渡すことができます。
このFunctionは日付などを含んだ引数を受け取り、Eventの配列およびPromiseを返すことができます。
つまり、Reduxなどを使ってpropsをただ受け渡すことも、また表示する日付に対応して外部ソースから直接fetchすることなども可能な仕様になっています。


### イベントの見た目を変える

FullCalendar v5ではイベントをレンダーするタイミングでの様々な[Hook](https://fullcalendar.io/docs/event-render-hooks)が用意されています。
（v4では[eventRender](https://fullcalendar.io/docs/v4/eventRender)が担っていた箇所です。）

`eventClassNames` ではclassNameを挿入することが可能です。cssレベルの内容であればこちらで可能です。

`eventContent` ではEventの内容そのものを変換することが可能で、htmlや、ReactComponentを差し込むことが可能です。html構成そのものを変えたい場合などに有効です。

```tsx:pages/index.tsx

import FullCalendar, { EventContentArg } from "@fullcalendar/react";
// 省略

const EventComponent = (title: string) => <div>{title}</div>;

const IndexPage = () => (
  <Layout>
    <FullCalendar
      plugins={[dayGridPlugin]}
      events={[{ title: "initial event1", start: new Date() }]}
      eventContent={(arg: EventContentArg) => EventComponent(arg.event.title)}
    />
  </Layout>
);

export default IndexPage;
```

`eventDidMount` `eventWillUnmount` では Reactコンポーネントの `componentDidMount` `componentWillUnmount()` にあたるタイミングでのhookが可能になります。

いずれも [EventContentArg](https://github.com/fullcalendar/fullcalendar/blob/f55ba69a8247dda0cc6bb0c4aefceed21bf17eb9/packages/common/src/component/event-rendering.ts#L189) を引数として受けられるので様々な条件での操作が可能になります。

### カレンダーのフック

カレンダー自体の操作にももちろんHookが用意されています。
例えば上記の `イベントの表示` 項目でReduxなどを用いstate管理することも可能と書きましたが、カレンダーを次に送った時、どうすれば次の表示分のEventを取得しにいけるでしょうか。

その時は [datesSet](https://fullcalendar.io/docs/datesSet) propを用いるのが有効です。`datesSet`はカレンダーの日付表示が変わったタイミングで発火するfunctionで、引数に表示される日付のレンジ(startとend)などを受け取ることができます。

```tsx:pages/index.tsx

import { useState } from "react";
import FullCalendar, { DatesSetArg, EventInput } from "@fullcalendar/react";
// import省略

const IndexPage = () => {
  const [events, setEvents] = useState<EventInput[]>([
    { title: "initial event1", start: new Date() },
  ]);

  return (
    <Layout>
      <FullCalendar
        plugins={[dayGridPlugin]}
        events={events}
        datesSet={(arg: DatesSetArg) => {
          setEvents([...events, { title: "additional", start: arg.start }]);
        }}
      />
    </Layout>
  );
};

export default IndexPage;
```

少々雑な例ですがこのようにEventを日付によって追加することが可能です。

## まとめ

以上の内容は大体こちらにまとまっているので何か詰まった人の参考になれば幸いです。
[next-fullcalendar-example](https://github.com/yk2220s/next-fullcalendar-example)

Next.js + TypeScript + FullCalendarの導入やカスタマイズ方法を簡単にまとめてみました。
カスタマイズに関しては、ここで触れたのはほんの基本的なことで、思ったよりも多くの操作ができるので、よかったら是非触れてみると楽しいかもしれません。さくっと高機能なカレンダーが作成できて便利なモジュールです。
それでは、よきFullCalendarライフを！

明日は@daiki_nishiokaさんの「要求仕様書とはなにか」です！