Laravel + FullCalendar v4+Ajaxでイベントの表示・登録

# 前置き

FullCalendarをLaravelで実装している記事がネット上に少なかったように感じたので紹介します。

フロント側だけでのイベント操作は、ほとんどコード書かなくてもできますが、それだけではページを更新するたびにリセットされてしまいます。したがって今記事では、Ajaxでデータを受送信し、DBの値を取得・操作してみます。

具体的には
・DBから値を取得してカレンダーにイベントを表示
・日付をクリックするとイベント追加
・イベントをドラックアンドドロップで日付変更
を実現します。

<img width="548" alt="スクリーンショット 2020-01-01 0.43.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320110/a1d236f7-2441-7c56-b282-30a541cb8568.png">
↑完成イメージ↑

# 環境
環境を紹介
## 各バージョン
OS : macOS Catalina 10.15.1
PHP　: 7.3.1
MySQL : Ver 14.14 Distrib 5.6.42, for osx10.14 (x86_64) using EditLine wrapper
Laravel :  5.8.35
FullCalendar : v4

FullCalendarはバージョンが違うと動かないので気をつけてください。ネット上はにいろんな記事が落ちてると思いますが、まず最初にバージョンを確認するといいです。

(2022年1月追記。現在、FullCalendaはv5が出ています)

## データベース設計

今記事で扱うデータベースの設計です。

```2019_12_30_144053_create_events_table.php

class CreateEventsTable extends Migration
{
    public function up()
    {
        Schema::create('events', function (Blueprint $table) {
            $table->string('event_id', 34)->primary();
            //僕はuuidにしてます
            $table->date('date');
            $table->string('title');
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('events');
    }
}
```

今回はid,title,dateと最小限のカラムだけ用意しました。

eventObjectのstart,endはそれぞれイベントの開始時間と終了時間を示しますが、今回、内容を簡潔化するために
・日付のみで、時間のデータは持たない
・日付は1日のみ。複数日を跨がない
ため、eventObjectからはstartだけを受け取り、dateとしてテーブルに入れます。

具体的にはそれぞれ
・event_id => EV20e0e2cde562b0c8cdfad8b975dab7c6
・date => 2019-12-22
・title => 友達と遊ぶ
といったデータを想定しています。

# 手順

## ファイル読み込み

まずFullCalendarを読み込みたいと思います。

いくつかやり方はありますが、zipファイルをダウンロードしてLaravelプロジェクトに追加する方法でやりました。
下のリンクの"Download"ってところからファイルを解凍してください。
https://fullcalendar.io/docs/getting-started

いろいろファイルがありますが、必要なのはpackage下の各ディレクトリのmain.jsとmain.cssです。(cssファイルはディレクトリによってはないこともある？？)

daygridとかinteraction,timegridみたいのはプラグインの名前です。みなさんが使いたいものを選んで読み込んでください。
https://fullcalendar.io/docs/plugin-index

僕は今回、coreに加えてdaygrid,interactionを読み込みました。
概要は以下です。
core        →Calendarクラスを提供
daygrid     →カレンダーを月別か日別に表示
interaction → dateClick, selectableアクションが使え、eventをドラックアンドドロップ、リサイズができる

僕はこんな感じで設置しました。
<img width="319" alt="スクリーンショット 2019-12-28 4.13.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320110/8eee9b84-31a7-6172-3fa0-c8b770c2cc40.png">

んでbladeファイルの方でこれらを読み込んで行きます。

``` resources/views/layouts/app.blade.php

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

<script src='/js/fullcalendar/core/main.js'></script>
<script src='/js/fullcalendar/daygrid/main.js'></script>
<script src='/js/fullcalendar/interaction/main.js'></script>

<script src="/js/ajax-setup.js"></script>
<script src='/js/fullcalendar.js'></script>
<script src='/js/event-control.js'></script>
ここ上の3個はあとで使います。ファイルを作成した後、あらかじめ読み込んでおきます。

<link href='/css/fullcalendar/core/main.css' type="text/css" rel='stylesheet' />
<link href='/css/fullcalendar/daygrid/main.css' type="text/css" rel='stylesheet' />

```

## カレンダー表示まで

んで次に任意のbladeファイルを用意して、そこに書いてください。viewはこれだけで大丈夫です。

```calendar.blade.php
@extends('layouts.app')
@section('content')
    <div id="calendar"></div>
@endsection
```

次にjsです。さっきapp.blade.phpに読み込んだファイルに、カレンダーの設定等を書き込みます。
scriptタグで囲ってあげて、calendar.blade.phpの下に書いてもいいと思います。

```public/js/fullcalendar.js

document.addEventListener('DOMContentLoaded', function() {
    var calendarEl = document.getElementById('calendar');

    var calendar = new FullCalendar.Calendar(calendarEl, {
        plugins: [ 'interaction', 'dayGrid' ],
        //プラグイン読み込み
        defaultView: 'dayGridMonth',
        //カレンダーを月ごとに表示
        editable: true,
        //イベント編集
        firstDay : 1,
        //秋の始まりを設定。1→月曜日。defaultは0(日曜日)
        eventDurationEditable : false,
        //イベントの期間変更
        selectLongPressDelay:0,
        // スマホでタップしたとき即反応
        events: [
            {
                title: 'イベント',
                start: '2019-01-01'
            }
        ],
        //一旦イベントのサンプルを表示。動作確認用。

        eventDrop: function(info){
        //eventをドラッグしたときの処理
             //editEventDate(info);
            //あとで使う関数
        },

        dateClick: function(info) {
        //日付をクリックしたときの処理
            //addEvent(calendar,info);
            //あとで使う関数
        },
    });
    calendar.render();
});

```

今回使用しているプロパティは一部です。他にもたくさんあるのでドキュメンや別の記事を見るといいです。
用意した関数の中に、これから処理を書いていって、ajax通信をします。
ここまででひとまずフロント側は動きました。

## DBの値からeventを表示

```web.php
Route::get('/setEvents', 'EventController@setEvents')
```
setEventsというURLにいくとEventController@setEventsが動くようrouteを設定しました。


```public/js/fullcalendar.js
        events: "/setEvents";
        // eventObjectsを取得するJSONフィードのURLを指定
```
先ほど書いたサンプルデータではなく、上のようにDBの値を呼び出すためのURLに書き換えます。

```EventController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Event;

class EventController extends Controller
{
    public function setEvents(Request $request)
    {
        //表示した月のカレンダーの始まりの日を終わりの日をそれぞれ取得。
        $start = $this->formatDate($request->input('start'));
        $end = $this->formatDate($request->input('end'));

        //カレンダーの期間内のイベントを配列で取得
        //EventsObjectが対応している配列キーの名前に変更するため、dateをstartとする
        $result = Event::select('id', 'title', 'date as start')->whereBetween('date', [$start, $end])->get()->toArray();

        echo json_encode($result);
        //json形式にして出力
    }

    // "2019-12-12T00:00:00+09:00"のようなデータを今回のDBに合うように"2019-12-12"に整形
    private function formatDate($date)
    {
        return str_replace('T00:00:00+09:00', '', $date);
    }

}

```


ドキュメント（ https://fullcalendar.io/docs/events-json-feed ）を日本語訳すると、以下のような説明があります。
>
FullCalendarは、新しいイベントデータが必要になるたびにURLにアクセスします。これは、ユーザーがprev / nextをクリックするか、ビューを変更すると発生します。FullCalendarは、イベントが必要な日付範囲を決定し、GETパラメーターでその情報を渡します。

今回のコードでは、"/setEvents?start=2013-12-01T00:00:00-05:00&end=2014-01-12T00:00:00-05:00"といったURLにアクセスします。ですので、コントローラーのアクションのRequestで、カレンダーの月別の始まりと終わりの日付が受け取れます。

これでデータベースの値を表示できました。

## event操作

CSRF保護をせずにAjax通信すると419 (unknown status)エラーが出てしまいます。Laravelのドキュメント(日本語訳サイトですが。)に載ってるコードをまんま記述します。
https://readouble.com/laravel/5.8/ja/csrf.html

```public/js/ajax-setup.js
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});
```


次に、fullcalendar.jsにて、もうすでに関数を書いておいたので、それらをコメントアウトを解除して、その中身を書きます。

```public/js/event-control.js

function addEvent(calendar,info){

    // タイトルの値を受け取る処理は、説明を簡潔にするために割愛し、適当な値を与えておきます
    var title = "サンプルイベント";
    
    $.ajax({
        url: '/ajax/addEvent',
        type: 'POST',
        dataTape: 'json',
        data:{
            "title":title,
            // 日程取得
            "date":info.dateStr
            
        }
    }).done(function(result) {
        // Ajaxに成功したらフロント側にeventを追加で表示
        calendar.addEvent({
            // PHP側から受け取ったevent_idをeventObjectのidにセット
            id:result['event_id'],
            title:title,
            start: info.dateStr,
        });
    });
}

function editEventDate(info){
    var event_id = info.event.id;
    //ドロップしたあとの日付
    var date = formatDate(info.event.start);

    $.ajax({
        url: '/ajax/editEventDate',
        type: 'POST',
        data:{
            "id":event_id,
            "newDate":date
        }
    })
}

function formatDate(date) {
    var year = date.getFullYear();
    var month = date.getMonth() + 1;
    var day = date.getDate();
    var newDate = year + '-' + month + '-' + day;
    return newDate;
}
//info.event.startの日付を"2019-12-12"のように整形する関数

```
使用したプロパティ、関数のドキュメントです。
eventClick → https://fullcalendar.io/docs/eventClick
dateClick → https://fullcalendar.io/docs/dateClick
addEvent() → https://fullcalendar.io/docs/Calendar-addEvent


各Ajaxのroute設定をします。

```web.php
Route::post('/ajax/addEvent', 'EventController@addEvent');
Route::post('/ajax/editEventDate', 'EventController@editEventDate');
```

次にコントローラーの中身です。

```EventController.php
public function addEvent(Request $request)
    {
        $data = $request->all();
        $event = new Event();
        //僕はmodel.phpでuuidを作成する関数を書いていましたが、みなさんはご自由に。
        $event->event_id = $this->generateId();
        $event->date = $data['date'];
        $event->title = $data['title'];
        $event->save();

        // Ajaxで受け取ったデータをデータベースに追加した後、今度はidを返す。
        return response()->json(['event_id' => $event->event_id]);
    }

    public function editEventDate(Request $request)
    {
        // Ajaxで受け取ったデータからデータベースの日付データを変更。
        $event = Event::find($request->input('id'));
        $event->date = $request->input('newDate');
        $event->save();
    }

```

これで終わりです

# 参考

https://tech.arms-soft.co.jp/entry/2017/02/14/154000
https://teratail.com/questions/189446
https://github.com/oclean66/prolygon
https://teratail.com/questions/170934
https://qiita.com/SOJO/items/bb24e7d09320ea96cfc3

# 終わりに

いろいろググったりしましたが、結局一番ドキュメントを読みました。ある程度人の記事を読んで概要を掴んだらドキュメントをたくさん見るのが結局早いと思います。
FullCalendarのv3とv4で結構違うっぽいので気をつけてください。

やる気になるんで、いいねとかください。変なところあったら優しく教えてほしいです。