July 20, 2020
Reactで「Googleカレンダーのようなカレンダーを作りたいな。」と思って、作成したときのメモです。
目次
1.環境の説明
2.環境の準備
3.実装
— 3.1.とりあえずカレンダーを表示する。
— 3.2.予定登録用フォームを作成する。
4.終わりに
※このカレンダーで実装できていないこと。
・予定の変更/削除。
create-react-app (公式サイト)
Reactの環境を作成してくれるもの。1からReactの環境を作成しようとすると結構手間がかかりますが、これだとコマンド一発で構築してくれるのでとても便利。
@material-ui/core (公式サイト)
マテリアルデザインが簡単に実装できるライブラリ。別プロジェクトで使っていたため今回も採用。
今回は、Styleを定義できるHookAPIのみ使用し、Material-UIのデザインコンポーネントは使っておりません。
@fullcalendar/react
React用のFullCalendarのコアライブラリ。実現したいことによって、これ以外にもインストールする必要があります。
@fullcalendar/daygrid
@fullcalendar/timegrid
週表示をさせる時に左のサイドバーに時間を表示させる。
@fullcalendar/interaction
日付を選択して予定を追加するために必要。
react-datepicker (公式サイト)
カレンダーから日付/時間を選択して、フォームに入力することができるようになる。
date-fns
react-datepickerでロケールを設定する時に必要。
公式サイトより:
「The date picker relies on date-fns internationalization to localize its display components」
要は、「datepickerはdate-fnsに依存している」ってことらしいです。
@types/react-datepicker
react-datepickerの型定義情報。
それでは、環境を構築していきます。
① React環境(Typescirpt)を構築する。
$ yarn create react-app sample-calendar --typescript
② 各種ライブラリをインストールしていく。
$ yarn add @material-ui/core \
@fullcalendar/react \
@fullcalendar/timegrid \
@fullcalendar/interaction \
react-datepicker \
date-fns
# 型定義は開発環境のみに入れたいため。
$ yarn add -D @types/react-datepicker
以上で、準備完了です。
まずは、以下の要件が満たせる最低限のカレンダーを実装していきます。
それではコードを見ていきます。
./src/components/SampleCalendar.tsx
/**
* 各種モジュールのインストール
*/
import React from 'react'
// FullCalendarコンポーネント。
import FullCalendar from '@fullcalendar/react'
// FullCalendarで週表示を可能にするモジュール。
import timeGridPlugin from '@fullcalendar/timegrid'
// FullCalendarで月表示を可能にするモジュール。
import dayGridPlugin from '@fullcalendar/daygrid'
// FullCalendarで日付や時間が選択できるようになるモジュール。
import interactionPlugin from '@fullcalendar/interaction'
const SampleCalendar: React.FC = props => {
return (
<div>
<FullCalendar
locale="ja" // ロケール設定。
plugins={[timeGridPlugin, dayGridPlugin, interactionPlugin]} // 週表示、月表示、日付等のクリックを可能にするプラグインを設定。
initialView="timeGridWeek" // カレンダーの初期表示設定。この場合、週表示。
slotDuration="00:30:00" // 週表示した時の時間軸の単位。
selectable={true} // 日付選択を可能にする。interactionPluginが有効になっている場合のみ。
businessHours={{ // ビジネス時間の設定。
daysOfWeek: [1, 2, 3, 4, 5], // 0:日曜 〜 7:土曜
startTime: '00:00',
endTIme: '24:00'
}}
weekends={true} // 週末を強調表示する。
titleFormat={{ // タイトルのフォーマット。(詳細は後述。※1)
year: 'numeric',
month: 'short'
}}
headerToolbar={{ // カレンダーのヘッダー設定。(詳細は後述。※2)
start: 'title',
center: 'prev, next, today',
end: 'dayGridMonth,timeGridWeek'
}}
/>
</div>
)
}
export default SampleCalendar
上記のソースで$ yarn start
するとブラウザでカレンダーが確認できます。
※1 titleFormat
この設定の場合、「2020年7月」のように年と月で表示されます。
titleFormat={{
year: 'numeric',
month: 'short'
}}
※2 headerToolbar
headerToolbar={{
start: 'title', // タイトルを左に表示する。
center: 'prev, next, today', // 「前月を表示」、「次月を表示」、「今日を表示」ができるボタンを画面の中央に表示する。
end: 'dayGridMonth,timeGridWeek' // 月・週表示を切り替えるボタンを表示する。
}}
カレンダーに予定を追加するためのフォームを作成していきます。
できあがりは、こんな感じです。(最低限のデザイン)
そして、こちらがコード。前回から追加したコードには「★」が付いています。
// useStateを追加。
★ import React, {useState} from 'react'
import FullCalendar from '@fullcalendar/react'
import timeGridPlugin from '@fullcalendar/timegrid'
import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction'
/**
* 開始時間などを入力する際に、カレンダーから入力できるようにするためのライブラリとしてDatePickerを使用。
* DatePickerコンポーネント、ロケール設定用のモジュール。
*/
★ import DatePicker, { registerLocale } from "react-datepicker";
// DatePickerのロケールを設定に使用。
★ import ja from 'date-fns/locale/ja'
/**
* Material-UIを通して、Styleを適用するためのモジュール。
* - createStyles: 型推論を解決してくれるモジュール。
* - makeStyles: StyleをHookAPIで適用させるモジュール。
*/
★ import {createStyles, makeStyles} from "@material-ui/core/styles";
// Style
★ const useStyles = makeStyles(() =>
createStyles({
cover: {
opacity: 0,
visibility: 'hidden',
position: 'fixed',
width: '100%',
height: '100%',
zIndex: 1000,
top: 0,
left: 0,
background: 'rgba(0, 0, 0, 0.3)'
},
form: {
opacity: 0,
visibility: 'hidden',
position: 'fixed',
top: '30%',
left: '40%',
fontWeight: 'bold',
background: 'rgba(255, 255, 255)',
width: '400px',
height: '300px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 2000,
},
inView: { // cover, formを表示する時に適用するStyle。
opacity: 1,
visibility: 'visible'
},
})
)
// DatePickerのロケールを日本に設定。
★ registerLocale('ja', ja)
// 追加するイベントの型。
★ interface myEventsType {
id: number
title: string
start: Date
end: Date
}
const SampleCalendar: React.FC = props => {
★ const classes = useStyles()
/**
* 予定を追加する際にCalendarオブジェクトのメソッドを使用する必要がある。
* (CalendarオブジェクトはRef経由でアクセスする必要がある。)
*/
★ const ref = React.createRef<any>()
★ const [inputTitle, setInputTitle] = useState('') // フォームに入力されたタイトル。
★ const [inputStart, setInputStart] = useState(new Date) // イベントの開始時刻。
★ const [inputEnd, setInputEnd] = useState(new Date) // イベントの終了時刻。
★ const [inView, setInView] = useState(false) // イベント登録フォームの表示有無。(trueなら表示する。)
★ const [myEvents, setMyEvents] = useState<myEventsType[]>([]) // 登録されたイベントが格納されていく。myEventsTypタイプの配列。
/**
* カレンダーがクリックされた時にイベント登録用のフォームを表示する。
* それぞれのフォームが下記の状態で表示される。
* - タイトル: 空欄
* - 開始: クリックしたカレンダーの開始時間
* - 終了: クリックしたカレンダーの終了時間
*/
★ const handleCLick = (info: any) => {
/**
* infoにはカレンダーに登録されたイベントが入ってくる。そのイベントのIDを元にmyEvents
* に格納されたイベントを取り出してStateに保存する。
*/
const event = myEvents[info.event.id]
const title = event.title
const start = event.start
const end = event.end
setInputTitle(title)
setInputStart(start)
setInputEnd(end)
setInView(true)
}
/**
* カレンダーから登録された予定をクリックした時にイベント変更用のフォームを表示する。
* それぞれのフォームが下記の状態で表示される。
* - タイトル: 選択した予定のタイトル
* - 開始: 選択した予定の開始時間
* - 終了: 選択した予定の終了時間
*/
★ const handleSelect = (selectinfo: any) => {
const start = new Date(selectinfo.start)
const end = new Date(selectinfo.end)
start.setHours(start.getHours())
end.setHours(end.getHours())
setInputTitle('')
setInputStart(start)
setInputEnd(end)
setInView(true)
}
/**
* カレンダーに予定を追加する。
*/
★ const onAddEvent = () => {
const startTime = inputStart
const endTime = inputEnd
if (startTime >= endTime) {
alert('開始時間と終了時間を確認してください。')
return
}
const event: myEventsType = {
id: myEvents.length,
title: inputTitle,
start: startTime,
end: endTime
}
// Stateにイベントを追加する。ここで登録されたイベントは、予定を変更するときなどに使用する。
setMyEvents([...myEvents, event])
// カレンダーに予定を登録して表示するための処理。
ref.current.getApi().addEvent(event)
}
/**
* ここからはフォームを構成する要素。
*/
//フォームが表示された時に、グレー背景でフォーム以外を非アクティブ化に見えるようにするための要素。
★ const coverElement = (
<div
onClick={() => setInView(false)}
className={
inView
? `${classes.cover} ${classes.inView}`
: classes.cover
}
/>
)
★ const titleElement = (
<div>
<label>タイトル</label>
<input
type="text"
value={inputTitle}
name="inputTitle"
onChange={e => {
// タイトルが入力されたら、その値をStateに登録する。
setInputTitle(e.target.value)
}}
/>
</div>
)
★ const startTimeElement = (
<div>
<label>開始</label>
<DatePicker
locale="ja"
dateFormat="yyyy/MM/d HH:mm"
selected={inputStart}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={10}
todayButton="today"
name="inputStart"
onChange={(time: Date) => {
setInputStart(time)
}}
/>
</div>
)
★ const endTimeElement = (
<div>
<label>終了</label>
<DatePicker
locale="ja"
dateFormat="yyyy/MM/d HH:mm"
selected={inputEnd}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={10}
todayButton="today"
name="inputEnd"
onChange={(time: Date) => {
setInputEnd(time)
}}
/>
</div>
)
★ const btnElement = (
<div>
<input
type="button"
value="キャンセル"
onClick={() => {
setInView(false)
}}
/>
<input
type="button"
value="保存"
onClick={() => onAddEvent()}
/>
</div>
)
★ const formElement = (
<div
className={
inView
? `${classes.form} ${classes.inView}`
: classes.form
}
>
<form>
<div>予定を入力</div>
{titleElement}
{startTimeElement}
{endTimeElement}
{btnElement}
</form>
</div>
)
return (
<div>
{coverElement}
{formElement}
<FullCalendar
locale="ja"
plugins={[timeGridPlugin, dayGridPlugin, interactionPlugin]}
initialView="timeGridWeek"
slotDuration="00:30:00"
selectable={true}
businessHours={{
daysOfWeek: [1, 2, 3, 4, 5],
startTime: '00:00',
endTIme: '24:00'
}}
weekends={true}
titleFormat={{
year: 'numeric',
month: 'short'
}}
headerToolbar={{
start: 'title',
center: 'prev, next, today',
end: 'dayGridMonth,timeGridWeek'
}}
★ ref={ref}
★ eventClick={handleCLick}
★ select={handleSelect}
/>
</div>
)
}
export default SampleCalendar
以上でカレンダーの作成は完了です。まだ、予定の変更や削除が実装できていないので追加する必要があるので後日更新したいと思います。