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

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

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

前回はVuetifyを利用してカレンダーを表示しました。
また、APIサーバーへリクエストを送り、イベントをカレンダーへ表示するところまでをやりました。

axiosを利用して、Laravelのコントローラーを呼び出し、イベントデータを取得しましたね!!

今回は、Vuexというものを利用し、状態管理を導入していきたいと思います。

Vuexとは

VuexはVue.jsアプリケーションで最もよく使われている状態管理ライブラリです。
多くのアプリケーションは、サーバから取得したデータやユーザ入力から得たデータ、見た目を変化させるための値などによってページの”状態”が変化します。
こういったデータは1つのページ、1つのコンポーネントだけでなく、他のページやコンポーネントでも使い回せるように共有する必要があります。
コンポーネント間で状態を共有するための仕組みが状態管理です。

状態管理を行わない場合でも、あるコンポーネントからあるコンポーネントに値を受け渡すことでデータを共有することができます。
しかし、例えばあるデータの値の変更によって多くのコンポーネントの見た目を変更したい場合、値の受け渡しが複雑になってしまいます。

公式サイトの説明がとても丁寧で参考になります。
全てを解説することはできないので、是非公式ドキュメントをご一読ください。

Vuex – Vuexとは何か?

上の図はVuexでの状態管理の流れを表しています。
状態管理しているデータの値を更新する際の処理の流れを順に説明します。

  • 管理しているデータは「State」が保持しており、コンポーネントはこのStateを参照して(renderして)値を取得する
  • Stateで管理しているデータの値を変更する場合は、直接Stateを更新するのではなく、「Action」を呼び出す(dispatchする)ことで指示を与える
  • ActionはAPIを呼び出してデータを取得するなどの非同期処理を行う
    • ActionではまだStateを変更せず、「Mutation」の呼び出し(commit)を行う
  • MutationではActionから値を受け取り、Stateの更新(mutate)を行う
    • Stateの値を更新できるのはこのMutationのみ

このように、状態を変化させる処理を「Action」「Mutation」「State」に役割分担し、データを循環させるように管理する仕組みとなっています。

Vuexを定義するには「state」「getters」「mutations」「actions」の4つが必要です。

  • state
    • 一元管理するデータの状態
  • getters
    • stateを取得する(getterを使わないrenderもある)
  • mutations
    • stateを更新する、更新は同期的に行います
  • actions
    • データの加工や、WebAPI呼び出しを非同期的に行います

今回は、複数のコンポーネントでイベントデータを共有して利用したいので、Vuexのstoreを定義してフロントの開発を進めます。

Vuexを使ってイベントデータを操作する

では、早速作っていきましょう!
Vuexのインストール自体はフロントエンドの準備編で終わっています。
まだの方は、下のリンクから準備編をご覧ください。

https://www.yuu-progra.com/2021/09/14/laravel-vue-cal-4/

まず、Vuexのモジュールを格納するディレクトリやファイルを作成します。
ターミナルで下記の様にコマンドを実行してください。

my-calendar $ mkdir -p resources/js/store/modules
my-calendar $ touch resources/js/store/index.js
my-calendar $ touch resources/js/store/modules/events.js

store/index.jsを編集します。

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

Vue.use(Vuex);

export default new Vuex.Store({
    modules: {
        events,
    }
});

Vuex.Storeの中のmodulesに、これから作成するevents.jsを入れることで、eventsストアが利用可能になります。
それでは、store/modules/events.jsを次の様に編集してください。

import axios from 'axios';

const state = {
    events: []
};

const getters = {
    events: state => state.events.map(event => {
        return {
            ...event,
            start: new Date(event.start),
            end: new Date(event.end)
        };
    }),
}

const mutations = {
    setEvents: (state, events) => (state.events = events)
}

const actions = {
    async fetchEvents({ commit }) {
        const response = await axios.get('/api/events');
        commit('setEvents', response.data);
    }
}

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

gettersのreturn内にある「...」スプレッド構文と言います。
returnで新しいオブジェクトを作って返すのですが、その新しいオブジェクトの要素として、eventオブジェクトの要素を展開して入れています。

続いて、resources/js/app.jsを下記の様に変更してください。

import Vue from 'vue'
import Vuex from "vuex"; // vuexライブラリーをimport
import Vuetify from 'vuetify';
import Home from "./components/pages/HomeComponent";
import store from "./store/index"; // vuexストアを読み込む

require('./bootstrap');

Vue.component('HomeComponent', require('./components/pages/HomeComponent.vue').default);
Vue.use(Vuex);  // Vuexを使用する事を宣言
Vue.use(Vuetify);

new Vue({
    el: '#app',
    vuetify: new Vuetify(),
    store: store, // 追記
    components: {
        Home
    }
});

Vuexのストアを使用する事を記述しておきます。

Vuexについての補足

VuexとはVue.jsのためのデータ保存ツールです。

すべてのコンポーネントからアクセスでき、さらに意図しないデータの変更を防げます。

例えば、ログイン中のユーザーに関するデータはどのコンポーネントからも参照できたほうが便利ですよね。

Vuexはデータを1箇所に集中させて管理を容易にします。

Vuexがない環境ではコンポーネント間のデータの受け渡しには、propsや$emitによるイベントを利用して行います。しかし、コンポーネント間でのデータ受け渡しが頻繁に行われたり階層が増えてくるとporpsや$emitでのデータ管理が難しくなります。

propsは親コンポーネントから子コンポーネントへデータを渡すのに利用します。$emitは子コンポーネントから子コンポーネントで発生した処理、データを親に通知するために利用します。通知を受け取った親コンポーネントでは子コンポーネントの通知に対応する処理を実行します。

Vuexでは、処理の流れは以下のように一方通行でなければなりません。

ActoionsとMutationsが何で分けられてるのか疑問を持たれる方もいると思います。
私がそうでした(笑)

公式には次の様に解説があります。

状態変更を非同期に組み合わせることは、プログラムの動きを予測することを非常に困難にします。例えば、状態を変更する非同期コールバックを持った 2 つのメソッドを両方呼び出すとき、それらがいつ呼び出されたか、どちらが先に呼び出されたかを、どうやって知ればよいのでしょう?これがまさに、状態変更と非同期の 2 つの概念を分離したいという理由です。Vuex では全てのミューテーションは同期的に行うという作法になっています

非同期の場合、いつ実行されるかが保証されないため、(Mutations を)同期的にすることで状態の変化を予測することができるようになります。
こうした理由から明示的に分けるために分けて考えられています。

  • Actions: 非同期
  • Mutations: 同期(状態変更)

役割に応じて明示的に記述法が変わるため、見通しが良くなるように設計されています。

更に詳細を知りたい方は、公式ドキュメントをご覧ください。

Vuex – Vuexとは何か?

ちょっと補足が長くなり過ぎました。
Vuexはとても便利なので、是非使ってください。

Vuexをコンポーネントで使用する

では、カレンダーコンポーネントからイベントStoreを利用してみましょう!!
resources/js/components/pageParts/Calendar.vueを次の様に編集してください。
<script></script>の中身を変えていただければ大丈夫です。

<template>
    <div>
        <v-sheet height="100vh">
            <v-calendar
                v-model="value"
                :events="events"
                @change="fetchEvents"
            ></v-calendar>
        </v-sheet>
    </div>
</template>

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

export default {
    name: 'Calendar',
    data: () => ({
        value: new Date('2021/09/01'),  // 表示する月を指定
    }),
    computed: {
        ...mapGetters('events', ['events']),
    },
    methods: {
        ...mapActions('events', ['fetchEvents'])
    }
};
</script>

:eventsにはストアから取得したイベントデータを、@changeにはストアのfetchEventsアクションを指定します。

map〇〇に関する補足

Vuexのヘルパーについて、最低限の補足です。
大きく分けて以下2つに分けて覚えればOKです。

1.状態を呼び出す
  • mapState
  • mapGetters
2.状態を変化させる
  • mapMutations
  • mapActions
使用するメリット

コードを見ていただくとわかる通り、とてもシンプルな記述になります。
もし使用せずに書くと、「$store.getters.getters1」の様に冗長な記述になります。

mapStatemapGettersはcomputedに記述します。

mapMutationsmapActionsはmethods内に記述します。

map〇〇に関する補足

Vuexのヘルパーについて、最低限の補足です。
大きく分けて以下2つに分けて覚えればOKです。

1.状態を呼び出す
  • mapState
  • mapGetters
2.状態を変化させる
  • mapMutations
  • mapActions
使用するメリット

コードを見ていただくとわかる通り、とてもシンプルな記述になります。
もし使用せずに書くと、「$store.getters.getters1」の様に冗長な記述になります。

mapStatemapGettersはcomputedに記述します。

mapMutationsmapActionsはmethods内に記述します。


それでは、npm run devを実行して、php artisan serveでローカルサーバーを立ち上げてブラウザーで確認してみて下さい。

こんな感じでイベントデータが取得できていれば問題ありません。
まだ、Vuexのメリットを感じられる要素はありませんが、これから複数のコンポーネントを作成していく中で、便利さが実感できます。

カレンダーに「月の移動ボタン」を追加する

現在のカレンダーは、コード内に記述した月のデータを表示しているだけなので、他の月のイベントデータを見る事ができません。

また、何月のデータを表示しているのかも分からない状態です。
より、便利に使用できるようにそれらの機能を作って行きましょう!!

月のタイトルを動的に表示できる様にする

resources/js/components/pageParts/Calendar.vueを次の様に編集してください。
まずはテンプレートの方のみ記述します。

<template>
    <div>
<!--ここから編集-->
        <v-sheet height="6vh" class="d-flex align-center">
            <v-toolbar-title>2021年 9月</v-toolbar-title>
        </v-sheet>
        <v-sheet height="94vh">
<!--ここまで編集-->
            <v-calendar
                v-model="value"
                :events="events"
                @change="fetchEvents"
            ></v-calendar>
        </v-sheet>
    </div>
</template>

~以下省略~

<v-sheet>でメニューを表示する部分とカレンダーを表示する部分の2つの領域に分けます。
<v-toolbar-title>で年月を表示します。

これまで、Vue.jsのビルドにnpm run devを毎回使用していましたが、npm run watchを使用すると、変更を監視し、変更があると自動でビルドしてくれます。

今後Vue.jsの変更が多くなるので、npm run watchコマンドとphp artisan serveコマンドをそれぞれターミナルで実行した状態にしておくと開発がスムーズです。

表示する月の指定はvalue変数で管理しているため、value変数の値を表示用の文字列に変換して表示してみます。

JavaScriptは日付の処理が少々面倒なので、ライブラリーを利用します。
チュートリアル通りに進んでいれば導入済みの「date-fns」というライブラリーを使用します。

date-fnsの使い方などについて詳しく知りたい方は、公式ドキュメントをご覧ください。
JavaScriptの日付処理は罠が多く、バグの温床になりやすいので素直にライブラリーを使った方が良いです。

Modern JavaScript date utility library

resources/js/components/pageParts/Calendar.vueを次の様に編集してください。

<template>
    <div>
        <v-sheet height="6vh" class="d-flex align-center">
<!--直打ちしていた月の欄をcomputedで作成するタイトルで置き換える-->
            <v-toolbar-title>{{ title }}</v-toolbar-title>
        </v-sheet>
        <v-sheet height="94vh">
            <v-calendar
                v-model="value"
                :events="events"
                @change="fetchEvents"
            ></v-calendar>
        </v-sheet>
    </div>
</template>

<script>
import { format } from 'date-fns'; // この行を追加
import { mapGetters, mapActions } from 'vuex';

export default {
    name: 'Calendar',
    data: () => ({
        value: new Date('2021/09/01'),  // 表示する月の初期値
    }),
    computed: {
        ...mapGetters('events', ['events']),
// 下記追加
        title () {
            return format(this.value, 'yyyy年M月');
        },
// ここまで
    },
    methods: {
        ...mapActions('events', ['fetchEvents'])
    }
};
</script>

表示は変わりませんが、エラーが出ていなければOKです。

月の進む/戻るボタンを追加する

まずは、テンプレートを編集しボタンを作りましょう!

resources/js/components/pageParts/Calendar.vueを次の様に編集してください。
テンプレートの方のみ記述します。

<template>
    <div>
        <v-sheet height="6vh" class="d-flex align-center">
<!--ここから追加-->
            <v-btn icon>
                <v-icon>mdi-chevron-left</v-icon>
            </v-btn>
            <v-btn icon>
                <v-icon>mdi-chevron-right</v-icon>
            </v-btn>
<!--ここまで-->
            <v-toolbar-title>{{ title }}</v-toolbar-title>
        </v-sheet>
        <v-sheet height="94vh">
            <v-calendar
                v-model="value"
                :events="events"
                @change="fetchEvents"
            ></v-calendar>
        </v-sheet>
    </div>
</template>

ビルドしてブラウザーで確認して下さい。

ボタンが表示されていればOKです。
続いてボタンをクリックした時のイベントを記述します。

resources/js/components/pageParts/Calendar.vueを次の様に編集してください。

<template>
    <div>
        <v-sheet height="6vh" class="d-flex align-center">
            <v-btn icon @click="$refs.calendar.prev()"> <!-ここを編集-->
                <v-icon>mdi-chevron-left</v-icon>
            </v-btn>
            <v-btn icon @click="$refs.calendar.next()"> <!-ここを編集-->
                <v-icon>mdi-chevron-right</v-icon>
            </v-btn>
            <v-toolbar-title>{{ title }}</v-toolbar-title>
        </v-sheet>
        <v-sheet height="94vh">
<!-v-calendarのref属性に"calendar"を指定-->
            <v-calendar
                ref="calendar"
                v-model="value"
                :events="events"
                @change="fetchEvents"
            ></v-calendar>
        </v-sheet>
    </div>
</template>

<script>
import { format } from 'date-fns';
import { mapGetters, mapActions } from 'vuex';

export default {
    name: 'Calendar',
    data: () => ({
        value: format(new Date(), 'yyyy/MM/dd'),  // ここを編集(初期値を今日の月にする)
    }),
    computed: {
        ...mapGetters('events', ['events']),
        title () {
            return format(new Date(this.value), 'yyyy年 M月'); // ここを編集
        },
    },
    methods: {
        ...mapActions('events', ['fetchEvents'])
    }
};
</script>

戻るボタンがクリックされた時、$refs.calendar.prev()を進むボタンをクリックしたら$refs.calendar.prev()を実行するようにしました。
<v-calendar>ref="calendarを指定することで、$refs.calendar.メソッド()<v-calendar>で定義されているメソッドを呼び出すことができます。
prev()メソッド及びnext()メソッドはVuetifyのカレンダーコンポーネントで定義されているメソッドで、valueの値を前の月の最終日/次の月の初日に変更します。

Vuetifyのコンポーネントは凄く便利な機能が多いです。
カレンダーコンポーネントのメソッドなどは下記をご覧ください。

v-calendar コンポーネント

ブラウザーで動作を確認してみて下さい。
前後の月へスムーズに遷移でき、更に移動した月のイベントデータも取得・表示できているかと思います。
すごいですよね!!

今日の月に戻るボタンを追加する

月を沢山動かすと、今月のデータを確認するのに、何度もボタンを押さなくてはなりません。
ちょっと、ナンセンスですよね。

ナンセンスな部分を解消するのに、「当月に戻る」ボタンを実装します。
resources/js/components/pageParts/Calendar.vueを次の様に編集してください。

<template>
    <div>
        <v-sheet height="6vh" class="d-flex align-center">
            <v-btn outlined small class="ma-4" @click="setToday">TODAY</v-btn> <!--「TODAY」ボタンを追加-->
            <v-btn icon @click="$refs.calendar.prev()">
                <v-icon>mdi-chevron-left</v-icon>
            </v-btn>
            <v-btn icon @click="$refs.calendar.next()">
                <v-icon>mdi-chevron-right</v-icon>
            </v-btn>
            <v-toolbar-title>{{ title }}</v-toolbar-title>
        </v-sheet>
        <v-sheet height="94vh">
            <v-calendar
                ref="calendar"
                v-model="value"
                :events="events"
                @change="fetchEvents"
            ></v-calendar>
        </v-sheet>
    </div>
</template>

<script>
import { format } from 'date-fns';
import { mapGetters, mapActions } from 'vuex';

export default {
    name: 'Calendar',
    data: () => ({
        value: format(new Date(), 'yyyy/MM/dd'),  // 初期値を今日の月にする
    }),
    computed: {
        ...mapGetters('events', ['events']),
        title () {
            return format(new Date(this.value), 'yyyy年 M月');
        },
    },
    methods: {
        ...mapActions('events', ['fetchEvents']),
// メソッドを追加
        setToday() {
            this.value = format(new Date(), 'yyyy/MM/dd')
        },
    }
};
</script>

動作を確認して下さい。

何ヶ月か先に進み、「TODAY」ボタンを押してみて下さい。
当月に遷移すれば成功です!!

表記を日本語にする

カレンダーの表示を日本語にします。
次のように<v-calendar>の属性を追加します。

resources/js/components/pageParts/Calendar.vueを次の様に編集してください。
テンプレートの方のみ記述します。

<template>
    <div>
        <v-sheet height="6vh" class="d-flex align-center">
            <v-btn outlined small class="ma-4" @click="setToday">TODAY</v-btn>
            <v-btn icon @click="$refs.calendar.prev()">
                <v-icon>mdi-chevron-left</v-icon>
            </v-btn>
            <v-btn icon @click="$refs.calendar.next()">
                <v-icon>mdi-chevron-right</v-icon>
            </v-btn>
            <v-toolbar-title>{{ title }}</v-toolbar-title>
        </v-sheet>
        <v-sheet height="94vh">
<!--カレンダーの属性を編集-->
            <v-calendar
                ref="calendar"
                v-model="value"
                :events="events"
                @change="fetchEvents"
                locale="ja-jp"
                :day-format="(timestamp) => new Date(timestamp.date).getDate()"
                :month-format="(timestamp) => (new Date(timestamp.date).getMonth() + 1)"
            ></v-calendar>
<!--ここまで-->
        </v-sheet>
    </div>
</template>

~以下省略~

分かりにくいですが、曜日や月の表記が日本語になっていることが確認できるかと思います。

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

お疲れ様でした!!
今回は、かなり長くなってしまいましたが、ブラウザーで動きを確認出来るので楽しかったのでは無いでしょうか?

Vuetifyのコンポーネントは、とても美しいパーツを使用する事ができ、更に便利なメソッドも多く定義されています。

このブログで全てを解説することは難しいので、気になった箇所があればVuetifyの公式ドキュメントをご参照ください。
基本的にスタイルなどはVuetifyのUIコンポーネントを使って整えて行きます。

次回は、イベントの詳細データを表示するコンポーネントを作成やイベントの追加などを同時にできれば良いなと考えています。

フロントエンド編は少し長くなりますが、最後まで一緒に頑張りましょう!!

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

以前紹介しましたが、下記の書籍で「Vue.jsを利用してGoogleカレンダーのクローン」を作成しています。

Vue.jsの書籍で現在、最もおすすめできる書籍です。
よければ読んでみてください!


このチュートリアルでは、最近のJavaScriptの構文を使用しています。
JavaScriptも年々進化していて、情報をキャッチするのが難しく、なんとなく書いている方も多いかと思います。

書籍で体系だった知識を身につけることで、変化に対応する力を身につけて下さい。
下記の書籍は最近の書籍ではおすすめです。

コメントを残す