☆Yuus Memo☆
非エンジニアの方でも業務を効率化できるプログラムを紹介します!
Laravel

Laravel + Vue.jsでGoogleカレンダーのクローンを作ろう!!【Laravel8対応】フロントエンド編⑤

皆さんこんにちは!!
前回に引き続き、フロント(Vue.js)側の開発を始めていきます。

前回までで予定のCRUD機能は一通り作りました。
今回はカレンダー(予定の種類分け)昨日のCRUDを作ります。

API部分はAPI編で仮作成してあると思うので、そちらの編集も併せて行って行きます。


基本的な流れは、予定管理を作成した時と同じなので、ここまで進めてこられた方なら、すんなりと進める事が可能だと思います。

最後までがんばりましょう!!

カレンダー一覧機能の作成

早速作って行きましょう!

カレンダー用の補助関数を作成

/resources/js/functions/serializers.jsの末尾にserializeCalendarメソッドを追加します。

import { format } from 'date-fns';

export const serializeEvent = event => {
    if (event === null) {
        return null;
    }
    const start = new Date(event.start);
    const end = new Date(event.end);
    return {
        ...event,
        start,
        end,
        startDate: format(start, 'yyyy/MM/dd'),
        startTime: format(start, 'HH:mm'),
        endDate: format(end, 'yyyy/MM/dd'),
        endTime: format(end, 'HH:mm'),
        color: event.color || '#216a1a',
    };
};

// カレンダー用の関数を追加
export const serializeCalendar = calendar => {
    if (calendar === null) {
        return null;
    }
    return {
        ...calendar,
        color: calendar.color || '#216a1a',
    };
};

続いてカレンダー用のVuexストアを作成します。

カレンダーストアの作成

ターミナルで次の様に実行し、ファイルを作成して下さい。

my-calendar $ touch resources/js/store/modules/calendars.js

resources/js/store/modules/calendars.jsを次の様に編集します。

import axios from 'axios';
import { serializeCalendar } from '../../functions/serializers';

const state = {
    calendars: [],
};

const getters = {
    calendars: state => state.calendars.map(calendar => serializeCalendar(calendar)),
};

const mutations = {
    setCalendars: (state, calendars) => (state.calendars = calendars),
};

const actions = {
    async fetchCalendars({ commit }) {
        const response = await axios.get('/api/calendars');
        commit('setCalendars', response.data);
    },
};

export default {
    namespaced: true,
    state,
    getters,
    mutations,
    actions,
};

resources/js/store/index.jsで、今作成したストアを読み込みます。

import Vue from 'vue';
import Vuex from 'vuex'
import events from './modules/events';
import calendars from './modules/calendars'; // 追加

Vue.use(Vuex);

export default new Vuex.Store({
    modules: {
        events,
        calendars, // 追加
    }
});

前回、作成した際の流れを思い出しながら作業して下さい。
繰り返し同じ事をすることで、自分の技術として身につけることが出来ます。

一覧表示するコンポーネントの作成

次のコマンドを実行し、カレンダーリストコンポーネントを作成して下さい。

my-calendar $ touch resources/js/components/calendars/CalendarList.vue

前回の記事で最後にcalendarsディレクトリを作成していない方は、作成して下さい。

resources/js/components/calendars/CalendarList.vueを次の様に編集して下さい。

<template>
    <v-list dense>
        <v-list-item-group :value="selectedItem">
            <v-subheader>マイカレンダー</v-subheader>
            <v-list-item v-for="calendar in calendars" :key="calendar.id">
                <v-list-item-content class="pa-1">
                    <v-checkbox
                        dense
                        v-model="calendar.visibility"
                        :color="calendar.color"
                        :label="calendar.name"
                        class="pb-2"
                        hide-details="true"
                    ></v-checkbox>
                </v-list-item-content>
            </v-list-item>
        </v-list-item-group>
    </v-list>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'

export default {
    name: 'CalendarList',
    data: () => ({
        selectedItem: null,
    }),
    computed: {
        ...mapGetters('calendars', ['calendars']),
    },
    created() {
        this.fetchCalendars();
    },
    methods: {
        ...mapActions('calendars', ['fetchCalendars']),
    },
};
</script>

カレンダーデータをv-forでループさせて、v-checkboxでカレンダーごとにチェックボックスを表示しています。
v-model="calendar.visibility"でvisibilityの値に応じてチェックするかどうかを決め、:color="calendar.color"でチェックボックスをcolorの値の色に変更し、:label="calendar.name"でnameの値を表示します。

特別難しいことは無いと思います。

不明点があれば、公式ガイドで知識の穴埋めを行って下さい。

実際に画面へ表示させてみましょう。
resources/js/components/pageParts/Calendar.vueを開き次の様に編集して下さい。

画面左側に予定種類一覧が表示されていれば成功です。

レイアウトに関しては、<v-sheet>を使用しています。
詳しくは、こちらでご確認ください。

前回の記事で、「予定作成時にカレンダーの種類を選択する機能は、カレンダー管理機能を作りながら解説する。」と書いていたことを覚えていますか?

カレンダーを取得する機能の作成が終わったので、次にその機能を用意します。

イベント作成時にカレンダーを選択する機能を作る

ここでは予定作成・編集フォームでカレンダーを選択し保存できるように作って行きます。

まずはAPI側の登録処理で、仮のカレンダーIDを入れていた箇所を修正します。
/app/Http/Controllers/EventController.phpを開き_saveEventメソッドを編集します。

// ~ 省略

/**
     * @param Request $request
     * @param $event
     * @return \Illuminate\Http\JsonResponse
     */
    public function _saveEvent(Request $request, $event): \Illuminate\Http\JsonResponse
    {
        $event->name = $request->input('name');
        $event->start = new Carbon($request->input('start'));
        $event->end = new Carbon($request->input('end'));

        $event->timed = $request->input('timed');
        $event->calendar_id = $request->input('calendar_id');

        $event->description = $request->input('description');
        $event->color = $request->input('color');

        if ($event->save()) {
            return response()->json(Event::with('calendar')->find($event->id)); // カレンダーを一緒に返す
        } else {
            return response()->json(['error' => 'Save Error']);
        }
    }

// ~ 省略

次に、保存されているカレンダーの名前一覧を表示して選択できるフォームを作成します。
ターミナルで次のコマンドを実行して下さい。

my-calendar $ touch resources/js/components/form/CalendarSelectForm.vue

resources/js/components/form/CalendarSelectForm.vueを次の様に編集して下さい。

<template>
    <v-menu offset-y>
        <template v-slot:activator="{ on }">
            <v-btn v-on="on">{{ calendar.name }}</v-btn>
        </template>
        <v-list max-height="300px">
            <v-list-item v-for="c in calendars" :key="c.id" @click="calendar = c">
                {{ c.name }}
            </v-list-item>
        </v-list>
    </v-menu>
</template>

<script>
import { mapGetters } from 'vuex';

export default {
    name: 'CalendarSelectForm',
    props: ['value'],
    computed: {
        ...mapGetters('calendars', ['calendars']),
        calendar: {
            get() {
                if (this.value === undefined || this.value === null) {
                    this.$emit('input', this.calendars[0]);
                    return this.calendars[0];
                }
                return this.value;
            },
            set(value) {
                this.$emit('input', value);
            },
        },
    },
};
</script>

イベント関連のフォームを作成していた時とあまり変わりませんが、今回はpropsでカレンダーオブジェクトを受け取ります。

propsで渡されるvalueの値はカレンダーオブジェクトですが、予定作成時はその値がundefined(もしくはnull)で渡ります。
予定作成時ならデフォルトでcalendarsの最初のデータをその予定のカレンダーとして扱うようにしています。

カレンダー選択フォームを表示

EventFormDialogコンポーネントでカレンダー選択フォームを表示するようにします。
resources/js/components/events/EventFormDialog.vueを次のように変更します。

<template>
    <v-card class="pb-12">
        <v-card-actions class="d-flex justify-end pa-2">
            <v-btn icon @click="closeDialog">
                <v-icon size="20px">mdi-close</v-icon>
            </v-btn>
        </v-card-actions>
        <v-card-text>
            <DialogSection icon="mdi-square" :color="color">
                <v-text-field v-model="name" label="タイトル"></v-text-field>
            </DialogSection>
            <DialogSection icon="mdi-clock-outline">
                <DateForm v-model="startDate" />
                <div v-show="!allDay">
                    <TimeForm v-model="startTime" />
                </div>
                <DateForm v-model="endDate" />
                <div v-show="!allDay">
                    <TimeForm v-model="endTime" />
                </div>
                <CheckBox v-model="allDay" label="終日" />
            </DialogSection>
            <DialogSection icon="mdi-card-text-outline">
                <TextForm v-model="description" />
            </DialogSection>
            <!--追加-->
            <DialogSection icon="mdi-calendar">
                <CalendarSelectForm :value="calendar" @input="changeCalendar($event)" />
            </DialogSection>
            <!--ここまで-->
            <DialogSection icon="mdi-palette">
                <ColorForm v-model="color" />
            </DialogSection>
        </v-card-text>
        <v-card-actions class="d-flex justify-end">
            <v-btn @click="cancel">キャンセル</v-btn>
            <v-btn @click="submit">保存</v-btn>
        </v-card-actions>
    </v-card>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';
import DialogSection from '../pageParts/DialogSection';
import DateForm from '../form/DateForm';
import TimeForm from '../form/TimeForm';
import TextForm from '../form/TextForm';
import ColorForm from '../form/ColorForm';
import CheckBox from '../form/CheckBox';
import CalendarSelectForm from '../form/CalendarSelectForm'; // 追加

export default {
    name: 'EventFormDialog',
    components: {
        DialogSection,
        DateForm,
        TimeForm,
        TextForm,
        ColorForm,
        CheckBox,
        CalendarSelectForm, // 追加
    },
    data: () => ({
        name: '',
        startDate: null,
        startTime: null,
        endDate: null,
        endTime: null,
        description: '',
        color: '',
        allDay: false,
        calendar: null, // 追加
    }),
    computed: {
        ...mapGetters('events', ['event']),
    },
    created() {
        this.name = this.event.name;
        this.startDate = this.event.startDate;
        this.startTime = this.event.startTime;
        this.endDate = this.event.endDate;
        this.endTime = this.event.endTime;
        this.description = this.event.description;
        this.color = this.event.color;
        this.allDay = !this.event.timed;
        this.calendar = this.event.calendar; // 追加
    },
    methods: {
        ...mapActions('events', ['setEvent', 'setEditMode', 'createEvent', 'updateEvent']),
        closeDialog() {
            this.setEditMode(false);
            this.setEvent(null);
        },
        submit() {
            const params = {
                ...this.event,
                name: this.name,
                start: `${this.startDate} ${this.startTime || ''}`,
                end: `${this.endDate} ${this.endTime || ''}`,
                description: this.description,
                color: this.color,
                timed: !this.allDay,
                calendar_id: this.calendar.id, // 追加
            };
            if (params.id) {
                this.updateEvent(params);
            } else {
                this.createEvent(params);
            }
            this.closeDialog();
        },
        cancel() {
            this.setEditMode(false);
            if (!this.event.id) {
                this.setEvent(null);
            }
        },
        // 追加
        changeCalendar(calendar) {
            this.color = calendar.color;
            this.calendar = calendar;
        },
        // ここまで
    },
};
</script>

このままだと、カレンダーが取得できずにエラーになるので、API側を編集します。
/app/Http/Controllers/EventController.phpindexメソッドshowメソッドを次の様に編集します。

<?php

namespace App\Http\Controllers;

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

class EventController extends Controller
{
    /**
     * @return \Illuminate\Http\JsonResponse
     */
    public function index(): \Illuminate\Http\JsonResponse
    {
        return response()->json(Event::with('calendar')->get()); // 編集
    }

    /**
     * @param int $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function show(int $id): \Illuminate\Http\JsonResponse
    {
        return response()->json(Event::with('calendar')->find($id)); // 編集
    }
// ~ 省略 ~

Eloquentのwith関数を用いて、イベントを取得しつつ、合わせて関連するカレンダーの一覧も取得します。
今回はコントローラーで記載していますが、記載箇所は任意です。

公式ドキュメントに詳しい解説があります。
Laravel公式サイト Eloquent-rRelationships Eager-Lording

ここまで実装できたら、「npm run watch」でビルドして、ローカルサーバーを立ち上げ動作を確認して下さい。

画像の様に予定の選択ができる様になっていれば成功です。

併せて予定編集フォームの確認も行って下さい。
対象の予定のカレンダーが選択済みになっていることを確認してください。

予定詳細ダイアログにもカレンダーを表示

resources/js/components/events/EventFormDetialDialog.vueを次の様に編集して下さい。

<template>
    <v-card class="pb-12">
        <v-card-actions class="d-flex justify-end pa-2">
            <v-btn icon @click="editEvent">
                <v-icon size="20px">mdi-pencil-outline</v-icon>
            </v-btn>
            <v-btn icon @click="removeEvent">
                <v-icon size="20px">mdi-trash-can-outline</v-icon>
            </v-btn>
            <v-btn icon @click="closeDialog">
                <v-icon size="20px">mdi-close</v-icon>
            </v-btn>
        </v-card-actions>
        <v-card-title>
            <DialogSection icon="mdi-square" :color="event.color">
                {{ event.name }}
            </DialogSection>
        </v-card-title>
        <v-card-text>
            <DialogSection icon="mdi-clock-time-three-outline">
                {{ event.startDate }} {{ event.timed ? event.startTime : '' }} ~ {{ event.endDate }} {{ event.timed ? event.endTime : '' }}
            </DialogSection>
        </v-card-text>
        <v-card-text>
            <DialogSection icon="mdi-card-text-outline">
                {{ event.description || 'no description' }}
            </DialogSection>
        </v-card-text>

        <!--追加-->
        <v-card-text>
            <DialogSection icon="mdi-calendar">{{ event.calendar.name }}</DialogSection>
        </v-card-text>
        <!--ここまで-->
    </v-card>
</template>

<!--下記省略-->
予定種類が表示されていればOK

Laravelのコントローラーも同時に編集しているので、間違えない様に注意して下さい。

カレンダー作成機能の作成

表示部分が終わったので、次はカレンダーを作成する機能を作ります。
覚えておられる方も居るかとは思いますが、今回のチュートリアルでは、「ユーザー毎にカレンダーを管理する」事を考えて作っています。
ただ、今のままではログイン済みのユーザーを取得できません

ユーザー毎のカレンダー管理ができるように修正して行きます。
app/Http/Controllers/CalendarController.php_saveCalendarメソッドを次の様に編集して下さい。

/**
     * @param Request $request
     * @param $calendar
     * @return \Illuminate\Http\JsonResponse
     */
    private function _saveCalendar(Request $request, $calendar)
    {
        $calendar->name = $request->input('name');
        $calendar->color = $request->input('color');
        $calendar->visibility = $request->input('visibility');
        $calendar->user_id = $request->input('user_id'); // 編集

        if ($calendar->save()) {
            return response()->json($calendar);
        } else {
            return response()->json(['error' => 'Save Error']);
        }
    }

Save時にリクエストで送信される「user_id」を保存する様に変更します。
続いて、resources/views/home.blade.phpを開き次の様に編集して下さい。

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-12">
                <div class="card">
                    <div class="card-header text-center">My Scheduler</div>
                    <div class="card-body">
                        
        <!--この行を追加--> 
                        <input type="hidden" id="user_id" name="user_id" value="{{ Auth::id() }}" />
        <!--ここまで-->

                        <div id="app">
                            <home-component></home-component>
                        </div>
                        
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

propsで渡しても良いのですが、Home画面へ隠し項目でログインユーザーのIDを持たせて、submit時に渡す事にします。

今回、Laravelのルーティングをapi.phpへ記述している為に、この様な処理を行っています。(web.phpルーティングを記述すれば、Auth::id()をコントローラーで使用できる)

同一オリジンならweb.phpにルーティングを記述し、コントローラーでAuthを取得しても良いのですが、拡張性を考えapi.phpへ記述してあります。

Token認証などを作成すれば良いのですが、難しくなるので今回はこの方法で対応する事にしました。ご了承ください。

Laravelの認証について詳しく知りたい方は、下記をご覧ください。
Laravel 8.x 認証

カレンダーストアにカレンダーの作成アクションを追加

resources/js/store/modules/calendars.jsを次の様に編集します。

import axios from 'axios';
import { serializeCalendar } from '../../functions/serializers';

const state = {
    calendars: [],
    calendar: null, // 追加
};

const getters = {
    calendars: state => state.calendars.map(calendar => serializeCalendar(calendar)),
    calendar: state => serializeCalendar(state.calendar),
};

const mutations = {
    setCalendars: (state, calendars) => (state.calendars = calendars),
    appendCalendar: (state, calendar) => (state.calendars = [...state.calendars, calendar]), // 追加
    setCalendar: (state, calendar) => (state.calendar = calendar), // 追加
};

const actions = {
    async fetchCalendars({ commit }) {
        const response = await axios.get('/api/calendars');
        commit('setCalendars', response.data);
    },
    // 追加
    async createCalendar({ commit }, calendar) {
        const response = await axios.post('/api/calendars', calendar);
        commit('appendCalendar', response.data);
    },
    setCalendar({ commit }, calendar) {
        commit('setCalendar', calendar);
    },
    // ここまで
};

export default {
    namespaced: true,
    state,
    getters,
    mutations,
    actions,
};

説明は不要ですね。
続いてターミナルで次のコマンドを実行し、CalendarFormDialogコンポーネントを作成して下さい。

my-calendar $ touch resources/js/components/calendars/CalendarFormDialog.vue

resources/js/components/calendars/CalendarFormDialog.vueを開き次の様に編集して下さい。

<template>
    <v-card class="py-12">
        <v-card-text>
            <DialogSection icon="mdi-square" :color="color">
                <v-text-field v-model="name" label="カレンダー名"></v-text-field>
            </DialogSection>
            <DialogSection icon="mdi-palette">
                <ColorForm v-model="color" />
            </DialogSection>
        </v-card-text>
        <v-card-actions class="d-flex justify-end">
            <v-btn @click="close">キャンセル</v-btn>
            <v-btn @click="submit">保存</v-btn>
        </v-card-actions>
    </v-card>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';
import DialogSection from '../pageParts/DialogSection';
import ColorForm from '../form/ColorForm';

export default {
    name: 'CalendarFormDialog',
    components: { DialogSection, ColorForm },
    data: () => ({
        name: '',
        color: null,
    }),
    computed: {
        ...mapGetters('calendars', ['calendar']),
    },
    created() {
        this.name = this.calendar.name;
        this.color = this.calendar.color;
    },
    methods: {
        ...mapActions('calendars', ['createCalendar', 'setCalendar']),
        close() {
            this.setCalendar(null);
        },
        submit() {
            const user_id = document.getElementById('user_id').value; // bladeからuser_idを取得する
            
            const params = {
                ...this.calendar,
                name: this.name,
                color: this.color,
                user_id: user_id,
            };
            this.createCalendar(params);
            this.close();
        },
    },
};
</script>

const user_id = document.getElementById(‘user_id’).value; // bladeからuser_idを取得する

の行で、先ほどbladeへ作成した「user_id」を取得しています。

続いて、このコンポーネントをCalendarListコンポーネントへ表示します。
resources/js/components/calendars/CalendarList.vueを開き次の様に編集して下さい。

<template>
    <v-list dense>
        <!--編集して下さい-->
        <v-list-item>
            <v-list-item-content>
                <v-subheader>マイカレンダー</v-subheader>
            </v-list-item-content>
            <v-list-item-action>
                <v-btn icon @click="initCalendar">
                    <v-icon size="16px">mdi-plus</v-icon>
                </v-btn>
            </v-list-item-action>
        </v-list-item>
        <v-list-item-group :value="selectedItem">
            <v-list-item v-for="calendar in calendars" :key="calendar.id">
        <!--ここまで-->
                <v-list-item-content class="pa-1">
                    <v-checkbox
                        dense
                        v-model="calendar.visibility"
                        :color="calendar.color"
                        :label="calendar.name"
                        class="pb-2"
                        hide-details="true"
                    ></v-checkbox>
                </v-list-item-content>
            </v-list-item>
        </v-list-item-group>
        <!--追加-->
        <v-dialog :value="calendar !== null" @click:outside="closeDialog" width="600">
            <CalendarFormDialog v-if="calendar !== null" />
        </v-dialog>
        <!--ここまで-->
    </v-list>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'
import CalendarFormDialog from './CalendarFormDialog'; // 追加

export default {
    name: 'CalendarList',
    data: () => ({
        selectedItem: null,
    }),
    components: { CalendarFormDialog },
    computed: {
        ...mapGetters('calendars', ['calendars', 'calendar']), // 'calendar'追加
    },
    created() {
        this.fetchCalendars();
    },
    methods: {
        ...mapActions('calendars', ['fetchCalendars', 'setCalendar']), // 'setCalendar'追加
        // 追加
        initCalendar() {
            this.setCalendar({
                name: '',
                visibility: true,
            });
        },
        closeDialog() {
            this.setCalendar(null);
        },
        // ここまで
    },
};
</script>
カレンダーリスト上部のプラスをクリックするとフォームが開き登録が行えます。

適当なカレンダー名を入力し、カラーを選択して保存ボタンを押すとダイアログが閉じ、カレンダー一覧にそれが追加されます。

カレンダーの編集・削除機能を実装

どんどん行きましょう!
まず更新から作ります。

resources/js/store/modules/calendars.jsを次の様に編集します。

import axios from 'axios';
import { serializeCalendar } from '../../functions/serializers';

const state = {
    calendars: [],
    calendar: null,
};

const getters = {
    calendars: state => state.calendars.map(calendar => serializeCalendar(calendar)),
    calendar: state => serializeCalendar(state.calendar),
};

const mutations = {
    setCalendars: (state, calendars) => (state.calendars = calendars),
    appendCalendar: (state, calendar) => (state.calendars = [...state.calendars, calendar]),
    updateCalendar: (state, calendar) => (state.calendars = state.calendars.map(c => (c.id === calendar.id ? calendar : c))), // 追加
    setCalendar: (state, calendar) => (state.calendar = calendar),
};

const actions = {
    async fetchCalendars({ commit }) {
        const response = await axios.get('/api/calendars');
        commit('setCalendars', response.data);
    },
    async createCalendar({ commit }, calendar) {
        const response = await axios.post('/api/calendars', calendar);
        commit('appendCalendar', response.data);
    },
    // 追加
    async updateCalendar({ commit }, calendar) {
        const response = await axios.put(`/api/calendars/${calendar.id}`, calendar);
        commit('updateCalendar', response.data);
    },
    // ここまで
    setCalendar({ commit }, calendar) {
        commit('setCalendar', calendar);
    },
};

export default {
    namespaced: true,
    state,
    getters,
    mutations,
    actions,
};

続いて、resources/js/components/calendars/CalendarList.vueを開き次の様に編集して下さい。

<template>
    <v-list dense>
        <v-list-item>
            <v-list-item-content>
                <v-subheader>マイカレンダー</v-subheader>
            </v-list-item-content>
            <v-list-item-action>
                <v-btn icon @click="initCalendar">
                    <v-icon size="16px">mdi-plus</v-icon>
                </v-btn>
            </v-list-item-action>
        </v-list-item>
        <v-list-item-group :value="selectedItem">
            <v-list-item v-for="calendar in calendars" :key="calendar.id">
                <v-list-item-content class="pa-1">
                    <v-checkbox
                        dense
                        v-model="calendar.visibility"
                        :color="calendar.color"
                        :label="calendar.name"
                        class="pb-2"
                        hide-details="true"
                    ></v-checkbox>
                </v-list-item-content>
                <!--追加-->
                <v-list-item-action class="ma-0">
                    <v-menu transition="scale-transition" offset-y min-width="100px">
                        <template v-slot:activator="{ on }">
                            <v-btn icon v-on="on">
                                <v-icon size="12px">mdi-dots-vertical</v-icon>
                            </v-btn>
                        </template>
                        <v-list>
                            <v-list-item @click="editCalendar(calendar)">編集</v-list-item>
                        </v-list>
                    </v-menu>
                </v-list-item-action>
                <!--ここまで-->
            </v-list-item>
        </v-list-item-group>
        <v-dialog :value="calendar !== null" @click:outside="closeDialog" width="600">
            <CalendarFormDialog v-if="calendar !== null" />
        </v-dialog>
    </v-list>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'
import CalendarFormDialog from './CalendarFormDialog'; 

export default {
    name: 'CalendarList',
    data: () => ({
        selectedItem: null,
    }),
    components: { CalendarFormDialog },
    computed: {
        ...mapGetters('calendars', ['calendars', 'calendar']),
    },
    created() {
        this.fetchCalendars();
    },
    methods: {
        ...mapActions('calendars', ['fetchCalendars', 'setCalendar']), 
        initCalendar() {
            this.setCalendar({
                name: '',
                visibility: true,
            });
        },
        closeDialog() {
            this.setCalendar(null);
        },
        // 追加
        editCalendar(calendar) {
            this.setCalendar(calendar);
        },
        // ここまで
    },
};
</script>

resources/js/components/calendars/CalendarFormDialog.vueを開き次の様に編集して下さい。

<!--テンプレート省略-->

<script>
import { mapActions, mapGetters } from 'vuex';
import DialogSection from '../pageParts/DialogSection';
import ColorForm from '../form/ColorForm';

export default {
    name: 'CalendarFormDialog',
    components: { DialogSection, ColorForm },
    data: () => ({
        name: '',
        color: null,
    }),
    computed: {
        ...mapGetters('calendars', ['calendar']),
    },
    created() {
        this.name = this.calendar.name;
        this.color = this.calendar.color;
    },
    methods: {
        ...mapActions('calendars', ['createCalendar', 'updateCalendar', 'setCalendar']), //'updateCalendar'を追加
        close() {
            this.setCalendar(null);
        },
        submit() {
            const user_id = document.getElementById('user_id').value;

            const params = {
                ...this.calendar,
                name: this.name,
                color: this.color,
                user_id: user_id,
            };
            // 追加
            if (params.id) {
                this.updateCalendar(params);
            } else {
                this.createCalendar(params);
            }
            // ここまで
            this.close();
        },
    },
};
</script>

イベント管理機能を作成した時と全く同じ流れですね。
同じ流れなので、解説は割愛しますが、気になる方は前回の記事をご覧ください。

カレンダーの更新を行ってみて問題が無いか確認して下さい。

削除処理の実装

resources/js/store/modules/calendars.jsを次の様に編集します。

import axios from 'axios';
import { serializeCalendar } from '../../functions/serializers';

const state = {
    calendars: [],
    calendar: null, // 追加
};

const getters = {
    calendars: state => state.calendars.map(calendar => serializeCalendar(calendar)),
    calendar: state => serializeCalendar(state.calendar),
};

const mutations = {
    setCalendars: (state, calendars) => (state.calendars = calendars),
    appendCalendar: (state, calendar) => (state.calendars = [...state.calendars, calendar]),
    updateCalendar: (state, calendar) => (state.calendars = state.calendars.map(c => (c.id === calendar.id ? calendar : c))), // 追加
    removeCalendar: (state, calendar) => (state.calendars = state.calendars.filter(c => c.id !== calendar.id)), // 追加
    setCalendar: (state, calendar) => (state.calendar = calendar),
};

const actions = {
    async fetchCalendars({ commit }) {
        const response = await axios.get('/api/calendars');
        commit('setCalendars', response.data);
    },
    async createCalendar({ commit }, calendar) {
        const response = await axios.post('/api/calendars', calendar);
        commit('appendCalendar', response.data);
    },
    async updateCalendar({ commit }, calendar) {
        const response = await axios.put(`/api/calendars/${calendar.id}`, calendar);
        commit('updateCalendar', response.data);
    },
    // 追加
    async deleteCalendar({ commit }, id) {
        const response = await axios.delete(`/api/calendars/${id}`);
        commit('removeCalendar', response.data);
    },
    // ここまで
    setCalendar({ commit }, calendar) {
        commit('setCalendar', calendar);
    },
};

export default {
    namespaced: true,
    state,
    getters,
    mutations,
    actions,
};

続いて、resources/js/components/calendars/CalendarList.vueを開き次の様に編集して下さい。

<template>
    <v-list dense>
        <v-list-item>
            <v-list-item-content>
                <v-subheader>マイカレンダー</v-subheader>
            </v-list-item-content>
            <v-list-item-action>
                <v-btn icon @click="initCalendar">
                    <v-icon size="16px">mdi-plus</v-icon>
                </v-btn>
            </v-list-item-action>
        </v-list-item>
        <v-list-item-group :value="selectedItem">
            <v-list-item v-for="calendar in calendars" :key="calendar.id">
                <v-list-item-content class="pa-1">
                    <v-checkbox
                        dense
                        v-model="calendar.visibility"
                        :color="calendar.color"
                        :label="calendar.name"
                        class="pb-2"
                        hide-details="true"
                    ></v-checkbox>
                </v-list-item-content>
                <v-list-item-action class="ma-0">
                    <v-menu transition="scale-transition" offset-y min-width="100px">
                        <template v-slot:activator="{ on }">
                            <v-btn icon v-on="on">
                                <v-icon size="12px">mdi-dots-vertical</v-icon>
                            </v-btn>
                        </template>
                        <v-list>
                            <!--追加-->
                            <v-list-item @click="editCalendar(calendar)">編集</v-list-item>
                            <v-list-item @click="delCalendar(calendar)">削除</v-list-item>
                            <!--ここまで-->
                        </v-list>
                    </v-menu>
                </v-list-item-action>
            </v-list-item>
        </v-list-item-group>
        <v-dialog :value="calendar !== null" @click:outside="closeDialog" width="600">
            <CalendarFormDialog v-if="calendar !== null" />
        </v-dialog>
    </v-list>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'
import CalendarFormDialog from './CalendarFormDialog'; // 追加

export default {
    name: 'CalendarList',
    data: () => ({
        selectedItem: null,
    }),
    components: { CalendarFormDialog },
    computed: {
        ...mapGetters('calendars', ['calendars', 'calendar']), // 'calendar'追加
    },
    created() {
        this.fetchCalendars();
    },
    methods: {
        ...mapActions('calendars', ['fetchCalendars', 'deleteCalendar', 'setCalendar']), // 'deleteCalendar'追加
        initCalendar() {
            this.setCalendar({
                name: '',
                visibility: true,
            });
        },
        closeDialog() {
            this.setCalendar(null);
        },
        editCalendar(calendar) {
            this.setCalendar(calendar);
        },
        // 追加
        delCalendar(calendar) {
            const res = confirm(`「${calendar.name}」を削除してもよろしいですか?`);
            if(res) {
                this.deleteCalendar(calendar.id);
            }
        },
        // ここまで
    },
};
</script>

続いて、app/Http/Controllers/CalendarController.phpのdestroyメソッドを編集します。

// 省略

/**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function destroy(Request $request)
    {
        $calendar = Calendar::find($request->id);
        // カレンダータイプに紐づくイベントを削除
        $calendar->events()->each(function ($event) {
            $event->delete();
        });

        if ($calendar->delete()) {
            return response()->json($calendar);
        } else {
            return response()->json(['error', 'Delete Error']);
        }
    }

// 省略

カレンダーを削除したときに、カレンダーに紐づくイベントも同時に削除するようにしています。

モデルのdeletingメソッドに記述した方が綺麗ですが、今回は分かりやすくコントローラーに記述しています。

Eloquentのメソッドについて詳しく知りたい方は、こちらからご確認下さい。

削除の確認をしてみて、紐づいたイベントも同時に削除される事をご確認下さい。

フロントエンド編⑤のまとめ

お疲れ様でした!!
前回に続き非常に長い記事になってしまいました。

フロントエンド編は次回が最終回です。
次回の実装として次のものを考えています。

  • カレンダー横のチェックボックスのチェック状態でイベントのフィルタリング
  • カレンダーの月表示・週表示・日表示・タイムライン表示
  • ログインユーザーのカレンダーのみ表示するようにする

この3つは正直簡単に実装できるので、次回は軽めの記事になるかと思います。

フロント編の後に、「ステップアップ編」として

  • バリデーションの実装
  • データーベースのシーディング

などを解説したいなと思っています。

最後までお読みいただきありがとうございました!!次回をお楽しみに!!


手っ取り早く稼げるエンジニアになりたい方は、スクールを利用した学習がオススメです。
TechAcademyは、選抜された現役エンジニアから学べるオンラインに特化したスクールです。
どこかに通う必要なく、自宅でもプログラミングやアプリ開発を学ぶことができます。


フリーランスとして活躍したい方や、副業で稼ぎたい方は初期投資の負担はありますが、スクールで学ぶ事で最短距離で目標を達成できます。

TechAcademyには次の様なメリットがあります。

  • 自宅にいながらオンライン完結で勉強できる
  • 受講生に1人ずつ現役のプロのパーソナルメンターがつく
  • チャットで質問すればすぐに回答が返ってくる
  • オリジナルサービスやオリジナルアプリなどの開発までサポート

コロナ禍で自宅時間が増え、自分のスキルアップや収入アップを目指している方は是非、スクールを検討してみて下さい。


コメントを残す