前回記事はこちら→SES営業のぼくでもできた!Google FirebaseでセルフDX(6)

皆さん、こんにちは!
株式会社TSOneでSES営業を担当しているHYです。

弊社が得意としているVue.jsとFireBaseを利用したアプリ開発を素人がどこまでできるのか?ブログで紹介していきたいと思います♪

SES営業がエンジニアとつながりたくて作ったアプリ「SalesPromotionalTools(営業推進ツール)」(※以降SPTs)

今回は実装で苦しんだ部分を抜粋して紹介したいと思います。今後Vue.jsとFirebaseで同じ悩みを抱えた人の少しでも癒しになればと思います。本当に困ったら弊社までご連絡ください(笑)

はじめに

我がSPTsも成長して、いろいろな機能が実装され、メニューから各種機能へリンク(遷移)できるようになりました。今回、メニューの実装するにあたりちょっとだけ苦労したことについて共有したいと思います。

メニュー画面

[メニュー画面]の実装
まずはサイドメニューとして表示するメニュー画面の実装です。
今回もプログラムをcomponetsフォルダに作成していきたいと思います。
ファイルは NavigationMenu.vue という名前にしました。

src/componets/NavigationMenu.vue

<template>
  <! – この中にコンテンツ -->
  <v-col>
    <v-list-item>
      <v-list-item-title class="title"> Menu </v-list-item-title>
    </v-list-item>
    <v-divider />
    <v-list nav>        ①
      <v-list-item
        v-for="menu in menus"
        :key="menu.title"
        @click="go(menu.url)"
      >
        <v-list-item-icon>
          <v-icon>{{ menu.icon }}</v-icon>
        </v-list-item-icon>
        <v-list-item-content>
          <v-list-item-title>{{ menu.title }}</v-list-item-title>
        </v-list-item-content>
      </v-list-item>
    </v-list>
  </v-col>
</template>
<style>
</style>
<script>
export default {
  data: () => ({
    drawer: false,
    menus: [],
    masterMenus: [],
  }),

  methods: {
    go: function (url) {
      this.$router.push(url).catch((err) => {
        // 既にいる画面に対してpushした際のエラー処理
        if (err.name === "NavigationDuplicated") {        ③
          return;
        }
      });
    },
  },
  mounted() {
    const administrator = this.$store.xxxxx.xxxxx.administrator;        ②
    if (administrator) {
      this.menus = [
        { title: "案件情報", icon: "work", url: "/ProjList" },
        { title: "要員情報", icon: "person_search", url: "/EngineerList" },
        { title: "各種事務依頼", icon: "loyalty", url: "/RequestMaster" },
        {
          title: "プロジェクト報告",
          icon: "pending_actions",
          url: "/ProjectReports",
        },
        {
          title: "お客様アンケート",
          icon: "dashboard",
          url: "/AssessmentMaster",
        },

        { title: "顧客管理", icon: "insert_emoticon", url: "/CustomerMaster" },
        { title: "利用者管理", icon: "group_add", url: "/UserMaster" },
      ];
     } else {
      this.menus = [
        { title: "案件情報", icon: "work", url: "/ProjList" },
        {
          title: "プロジェクト報告",
          icon: "pending_actions",
          url: "/ProjectReports",
        },
      ];
     }
  },
};
</script>

<templete>側の画面描画についてはタブを利用して、メニューを実装します。<script>側で定義したメニューの情報を保持する配列(menus)の要素数分 v-for でループしてメニュー情報を作成します。配列の変数 menus には、メニューで表示するタイトル(title)、アイコン(icon)と、選択(クリック)された際に呼び出す画面(url)を定義します。

②今回、権限によってメニューの表示内容を切り替えたかったので、mounted()内で権限による条件分岐を実装し、メニューの要素(menus)を定義しています。メニューの内容が固定であればdata()内で定義してしまって問題ありません。※実装のサンプル内で記載している変数administratorはSPTs独自の仕様です。Vue.jsが提供しているものではないのでご注意ください。

③$router.pushで画面遷移(描画)する際に、今の表示している画面と同じ画面(パス)を指定された際にエラーとなります。放っておいても良かったのですが、コンソールにエラーログが出力され続けるので、ここではメニューから同じ画面(パス)を指定した際にエラー(NavigationDuplicated)が発生しても無視する実装としています。

ここまででメニュー(NavigationMenu.vue)を作成しました。
尚、作成したNavigationMenu.vueはVue Routerに定義しないと表示されないので忘れずに。以下、NavigationMenu.vueの定義追加部分のみ抜粋

src/router/index.js※抜粋

import NavigationMenu from '@/components/NavigationMenu.vue'

const routes = [
 {
    path: '/NavigationMenu',
    name: 'NavigationMenu',
    component: NavigationMenu
  },
]

App.vueに[メニュー]を組み込む

いよいよ、作成したメニューを部品としてApp.vueに組み込むことで完成します。メニューは画面全体の構成に関わる為、App.vue内で実装します。

src/App.vue

<template>
  <div id="App">
    <v-app>
        <v-navigation-drawer app  class="blue darken-1" dark>        ③
          <Menu  />
        </v-navigation-drawer>
      <v-app-bar app color="#C5DFEA" dense dark height="70">

        <v-card-title class="font-weight-bold"
          ><v-img src="@/assets/sptslogo.png"></v-img
        ></v-card-title>
      </v-app-bar>

      <v-main>
        <router-view     />
      </v-main>
    </v-app>
  </div>
</template>
<script>
import Menu from "@/components/NavigationMenu";        ①

export default {
  name: "App",
  components: {
    Menu,        ②
  },
  data: () => ({
  }),
};
</script>

①②作成したNavigationMenuを Menu というコンポーネント名で利用することを宣言しています。
componetsに定義することで、<Menu>タグとして<templete>領域で利用できるようになります。
③実際のメニューの表示はVuetifyの機能タグを利用します。
今回作成した<Menu>タグ(NavigationMenu)を<v-navigation-drawer>の要素として突っ込みます。

早速SPTsを起動して画面表示を見てみると。

ログイン画面

メニューがでてます!
でも何か違和感が、、いきなりログイン画面でメニューが見えてるって、なんか変。

↑みたいにログイン以降の画面からメニューを出すようにできないものか?

emit発火!?

やりたいことで悩んだだら、まずは弊社のDX推進部にきいてみる。
ぼく「App.vueで表示している内容を、途中で切り替えたりとか制御したいんだけどなんか方法ある?」
研究開発部「あー、それならemit使って発火させたらいいっすねー」
ぼく「emit?発火?」

早速、「emit 発火」でググってみると、結構な情報量で実装方法が投稿されている。軽く眺めてみて、どーしても理解できなかったのが「親」「子」というキーワード。”emit使えば、「子」から「親」へイベント通知できる”みたいな投稿がでてくるのですが、そもそも何故「親」、「子」という表現なのか?何が「子」で、何が「親」なのか、ぼくにはちょっとよくわかんなかった。。

「親」と「子」の正しい理解についてはまた今度として、今回は以下で割り切って開発したいと思います。
親:App.vue
子:Login.vue

[やりたいこと]
Login.vue でのログイン処理が完了したタイミングで、親App.vueに仕込んで置いたファンクションを実行(発火)し、メニューを表示させます。
実装としては以下2ステップを今回の目標とします。

  1. App.vueにメニューの表示制御を実装し、子Login.vueから呼べる(発火する)ファンクションとして定義する
  2. Login.vueのログイン処理が正常終了した後に、親App.vueに仕込まれたファンクションを発火し、メニューを表示する

まずはステップ1.の実装を開始します。
1. 親App.vueにメニューの表示制御を実装し、子Login.vueから呼べる(発火する)ファンクションとして定義する

src/App.vue

<template>
  <div id="App">
    <v-app>
      <div v-if="isMenuDrawer">        ②
        <v-navigation-drawer app class="blue darken-4" dark>
          <Menu  />
        </v-navigation-drawer>
      </div>
      <v-app-bar app color="#C5DFEA" dense dark height="70">

        <v-card-title class="font-weight-bold"
          ><v-img src="@/assets/sptslogo.png"></v-img
        ></v-card-title>
      </v-app-bar>

      <v-main>
        <router-view  @menuDrawer="funcMenuDrawer"    />        ④
      </v-main>
    </v-app>
  </div>
</template>
<script>
import Menu from "@/components/NavigationMenu";

export default {
  name: "App",
  components: {
    Menu,
  },
  data: () => ({
     isMenuDrawer: false,         ①
  }),

  methods: {
    /**
     * menu表示をtrueにする。
     * ログイン後にemitで発行される。
     *
     * */
    funcMenuDrawer: function (arg) {        ③
      this.isMenuDrawer = arg;
    },
  },

};
</script>

①メニューを表示する、しないを保持する変数isMenuDrawerを定義します。
 初期値はfalse(表示しない)としておきます。
②ここでは v-if を使用し、<div>~</div>で括られた内容の表示制御をしています。
 isMenuDrawerの値が true の場合のみ<Menu />タグが表示する制御となります。
③メニューの表示可否を保持する変数isMenuDrawerの値に、変数argの値を設定するファンクションを定義します。
これによって子Login.vueから、親App.vueの変数isMenuDrawerへの値を操作可能としています。
④menuDrawerの部分がemitで呼び出されるファンクションの定義。子Login.vueからの呼び出し方法は後ほど記載しますが、
子からmenuDrawerが呼び出されると、@menuDrawer="funcMenuDrawer" での定義の通り③のfuncMenuDrawerが実行される流れになります。

次はステップ②子Login.vueの実装です。ここでemit発火を実装します。
②子Login.vueのログイン処理が正常終了した後に、親App.vueに仕込まれたファンクションを実行し、メニューを表示する。

src/componets/Login.vue

<template>
  <! – この中にコンテンツ -->
  <div id="Login">

      <v-card class="mx-auto">
        <v-container fluid>
          <v-row>
            <v-col>
              <v-card>
                <v-card-text>
                  <v-row>
                    <v-col>
                      <v-text-field
                        label="メールアドレス"
                        v-model="emailAddr"
                        suffix="@tsone.co.jp"
                        :rules="[required]"
                        type="text"
                      />
                    </v-col>
                  </v-row>
                  <v-row>
                    <v-col>
                      <v-text-field
                        label="password"
                        v-model="pass"
                        :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'"
                        :rules="[required]"
                        :type="show ? 'text' : 'password'"
                        name="input-10-2"
                        class="input-group--focused"
                        @click:append="show = !show"
                      />
                    </v-col>
                  </v-row>
                  <v-card-actions>
                    <v-btn
                      x-large
                      block
                      color="indigo darken-4"
                      dark
                      @click="login"
                      >ログイン</v-btn
                    >
                  </v-card-actions>
                </v-card-text>
              </v-card>
            </v-col>
          </v-row>

        </v-container>
      </v-card>
    </v-form>
  </div>
</template>
<script>
export default {
  name: "Login",
  data() {
    return {

    };
  },
  methods: {
    /**
     * 認証処理を行い初期画面へ遷移する。
     */
    async login() {
      // firebaseの認証処理


      /**ユーザ存在確認 */


      // 親(App.vue)のメニュー表示制御を発行
      this.$emit("menuDrawer", true);         ①

      /**遷移先メニュー制御 */

    },
};
</script>

①細かい実装は省略させていただきましたが、login()の処理が正常終了したら、ここでemit発火です。
this.$emit()の第1引数に、親App.vueに定義したファンクション名menuDrawerを指定します。前述のApp.vueの説明での④部分となります。
第2引数では値:trueを渡しています。
ここで渡された値:true がemitとして渡され、親App.vueの変数 isMenuDrawerにtrueが設定されることでメニューが表示されるようになります。

ここまででemit発火でのメニュー制御の実装完了です。
いろいろと疑問はあるんですけど、一旦はDon’t think. FEEL!」で乗り切ります。

早速SPTsを起動して画面表示を見てみると。
ログイン画面はいままで通りメニューは表示されていません。

ログイン画面

次にログインしてみます。

ログイン後の案件情報一覧画面

おお!ログイン後の画面でメニューがでました!
無事emit発火できたようですー。

おまけ

メニューが出っ放しでも良いのですが、折角なのでハンバーガーメニューにしたいと思います。今回もApp.vueでの実装となります。

src/App.vue

<template>
  <div id="App">
    <v-app>
      <div v-if="isMenuDrawer">
        <v-navigation-drawer app v-model="drawer" class="blue darken-4" dark>         ②
          <Menu />
        </v-navigation-drawer>
      </div>

      <v-app-bar app color="#C5DFEA" dense dark height="70">
        <div v-if="isMenuDrawer">
          <v-app-bar-nav-icon @click.stop="drawer = !drawer" />         ③
        </div>

</template>
<script>
import Menu from "@/components/NavigationMenu";

export default {
  name: "App",
  components: {
    Menu,
  },
  data: () => ({
     isMenuDrawer: false, 
  drawer: true,         ①
  }),


};
</script>

<v-navigation-drawer>(メニュー部分)※の表示制御を保持する変数drawerを定義します。
<v-navigation-drawer>(メニュー部分)※の表示制御として、drawerの値が true だと表示、falseだと非表示となります。
<v-navigation-drawer>が表示、非表示されることで、含まれる要素<Menu>も同様に表示制御されます。
③ハンバーガーのアイコン。クリックされるたびに変数drawerの値をtrue/falseに入れ替えてます。

画面をみてみると。
ヘッダー部分にハンバーガーのアイコンがでているのが確認できますね。

ハンバーガーのアイコンをクリックすると、メニューが閉じました!

もう一回ハンバーガーのアイコンをクリックすると、

メニューが表示されましたー
ここまでで、メニュー制御が完成です。

最後に

いかがでしたか?
今回はメニューの実装と、emitでの発火について投稿してみました。
みなさんの開発に少しでもお役にたてたら嬉しいです。
我がSPTsもメニューがあると大分立派にみえる気がします。作ってよかったー♪