【Laravel + Vue】吹き出しで登録後の「クイックツアー」をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、このあいだ開発の関係でとあるサイトに登録をしたのですが、その際に前からつくってみたい機能があったことを思い出しました。

それが・・・・・・

クイックツアー(吹き出しポップアップ)

です。

これは、例えば以下のように「検索はここでできますよ!」というように、どこに何があるかを分かりやすく教えてあげる機能のことです。

そこで❗

今回は、この「吹き出しポップアップを使った、クイックツアー」をLaravel + Vueで実装してみることにしました。

※ なお、これまで「シンプルを求めて」記事では、LivewireSveltealpine.jsと体験してきましたが、やはりVueが一番記事にするには向いていると感じましたので、今後は特別な理由がなければVueで記事を公開していこうと考えています。(個人的には、知名度が上がればapline.jsもありかなというカンジです)

📝 関連記事: 

※ ちなみに、今回はVue 3Composition APIで実装しています。

ぜひ何かの参考になりましたら嬉しいです。😄✨

「最近何度行っても
マックスバリュに
アールグレイが置いてない😭」

開発環境: Vue 3、Laravel 9.x

CSS で吹き出し部分をつくる

まずは、Bubbly というページを参考にして以下のような「吹き出しポップアップ」が表示できるようにしてみます。

ちなみに、今回は「とんがり部分」が4つ出せるようCSSをつくってみました。

  • 左上
  • 左下
  • 右上
  • 右下

では、実際のコードです。

public/css/speech-bubble.css

/* メイン */
.speech-bubble {
    position: fixed;
    background: #00aabb;
    border-radius: .4em;
    width: 300px;
    min-height: 100px;
    padding: 0.5rem 0.8rem;
    color: #f9f9f9;
    z-index: 10000;
}

/* とんがり部分 */
.speech-bubble:after {
    content: '';
    position: absolute;
    width: 0;
    height: 0;
    border: 8px solid transparent;
    margin-top: -8px;
}

/* 左上 */
.speech-bubble-left-top:after {
    border-right-color: #00aabb;
    margin-left: -8px;
    border-left: 0;
    left: 0;
    top: 25%;
}

/* 左下 */
.speech-bubble-left-bottom:after {
    border-right-color: #00aabb;
    margin-left: -8px;
    border-left: 0;
    left: 0;
    bottom: 25%;
}

/* 右上 */
.speech-bubble-right-top:after {
    border-left-color: #00aabb;
    margin-right: -8px;
    border-right: 0;
    right: 0;
    top: 25%;
}

/* 右下 */
.speech-bubble-right-bottom:after {
    border-left-color: #00aabb;
    margin-right: -8px;
    border-right: 0;
    right: 0;
    bottom: 25%;
}

/* ボタン */
.speech-bubble .close-button {
    position: absolute;
    right: 10px;
    bottom: 10px;
}

※ なお、少し長くなったので、外部ファイルとして保存しています。

そして、以下のようなHTMLをつくります。

<div class="speech-bubble speech-bubble-left-top" style="top:50px;left:50px;">
    <div>吹き出しポップアップ</div>
    <div class="close-button">
        <button type="button" class="btn btn-light btn-sm" @click="$emit('close')">OK</button>
    </div>
</div>

これを実際に4つ表示してみると次のようになりました 👍

Vue コンポーネントをつくる

では、先ほどのCSSクラスで「吹き出しポップアップ」のコンポーネントをつくっていきます。

public/js/vue/components/speech-bubble.js

const speechBubbleComponent = {
    props: {
        type: { // とんがり部分の位置
            type: String,
            default: ''
        },
        text: { // 表示するテキスト
            type: String,
            default: ''
        },
        active: { // コンポーネントを表示するかどうか
            type: Boolean,
            default: false
        }
    },
    emits: ['click-ok'],
    setup(props) {

        const classNames = Vue.computed(() => {

            const type = props.type;
            const validTypes = [
                'left-top',
                'left-bottom',
                'right-top',
                'right-bottom'
            ];

            if(validTypes.includes(type)) {

                return [
                    'speech-bubble',
                    `speech-bubble-${type}`
                ]

            }

            return [];

        });

        return {
            classNames,
        };

    },
    template: `
        <div :class="classNames" v-if="active">
            <div v-html="text"></div>
            <div class="close-button">
                <button type="button" class="btn btn-light btn-sm" @click="$emit('click-ok')">OK</button>
            </div>
        </div>
    `
};

※ なお、メインのコードが少し長いので、こちらも外部ファイルにしています。

とてもシンプルなコンポーネントですが、`speech-bubble-${props.type}`の部分で該当するCSSクラス名を「動的に(可変できるように)」つくっていることに注目してください。

例えば、type="left-top"の場合、クラス名はspeech-bubble-left-topになります。

なお、予期せぬ文字列が入ってこないよう、validTypes.includes(type){ ... }で中身をチェックしています。(こういうとき、JavaScriptでもEnumがほしいですね 😭)

Vue でクイックツアーをつくる

では本題の「クイックツアー」をつくる部分です。
実際のコードは次のようになりました。

resources/views/quick_tour.blade.php

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="/css/speech-bubble.css">
</head>
<body>
    <div id="app">

        <!-- Vue コンポーネント -->
        <v-speech-bubble
            :type="currentMessage.type"
            :text="currentMessage.text"
            :style="currentMessage.style"
            :active="isSpeechBubbleActive"
            @click-ok="onClickOk"></v-speech-bubble>

        <nav class="navbar navbar-expand-lg navbar-dark bg-info">
            <div class="container-fluid">
                <a class="navbar-brand" href="#">【クイックツアーのサンプル】</a>
                <div class="collapse navbar-collapse" id="navbarColor02">
                    <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                        <li class="nav-item">
                            <a class="nav-link active" aria-current="page" href="#">ホーム</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">投稿</a>
                        </li>
                    </ul>
                    <form class="d-flex">
                        <input class="form-control me-2" type="search">
                        <button class="btn btn-outline-light text-nowrap" type="submit">検索</button>
                    </form>
                </div>
            </div>
        </nav>
        <nav class="navbar fixed-bottom navbar-light bg-info">
            <div class="container-fluid p-4">
                <a href="#" class="text-light">ログアウト</a>
                <a href="#" class="text-light">お問い合わせ</a>
            </div>
        </nav>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.min.js"></script>
    <script src="https://unpkg.com/vue@3.1.1/dist/vue.global.prod.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
    <script src="/js/vue/components/speech-bubble.js"></script>
    <script>

        /*  ここは、Laravel の Blade で埋め込むことを想定しています */
        const isInit = true; // 初回かどうかを判別する値
        const messages = [ // 表示するメッセージ
            {
                type: 'left-top',
                text: 'ここから投稿できます。&#x1F60A;&#x2728;',
                style: 'top:12px;left:460px;'
            },
            {
                type: 'right-top',
                text: 'ここで検索できます。&#x1F50E;&#x2728;',
                style: 'top:4px;right:280px;'
            },
            {
                type: 'left-bottom',
                text: 'ログアウトはこちら。&#x1F6B6;&#x2728;',
                style: 'bottom:8px;left:140px;'
            },
            {
                type: 'right-bottom',
                text: '分からないことがあったらお気軽にお問い合わせください!&#x1F44D;&#x2728;',
                style: 'bottom:8px;right:140px;'
            }
        ];

        Vue.createApp({
            setup() {

                // 表示 or 非表示
                const isSpeechBubbleActive = Vue.ref(isInit);

                // メッセージ部分
                const messageIndex = Vue.ref(0);
                const onClickOk = () => {

                    const messageLength = messages.length;

                    if(messageIndex.value < messageLength - 1) { // まだクイックツアーのメッセージが残っている場合

                        messageIndex.value++;

                    } else {

                        isSpeechBubbleActive.value = false;

                    }

                };
                const currentMessage = Vue.computed(() => {

                    const index = messageIndex.value;
                    return messages[index];

                });

                return {
                    isSpeechBubbleActive,
                    onClickOk,
                    currentMessage
                };

            }
        })
        .component('v-speech-bubble', speechBubbleComponent)
        .mount('#app');

    </script>
</body>
</html>

この中で重要なのが、setup()の中で各機能ごとにコードを書いている部分です。

具体的には、

  • 表示 or 非表示
  • メッセージ部分

の2つですね。

Composition APIはこのように、「何をするのか?」でブロックごとに分けてコードがかけるのがメリットです。

逆に昔のOptions APIは、各場所が役割ごとで決まっているので、今回の場合だと「同じメッセージ用のコードなのに、書く場所がバラバラになっちゃう…😭」となるので、規模が大きくなるほどコードの理解が難しくなります。

※ ただ、今回ぐらいシンプルなコードだとOptions APIで問題ないとは思いますが、初学者がチームにいる場合はどちらかに統一してあげるのが親切かもしれませんね。

また、isInitmessagesの部分は、LaravelBladeで埋め込むことを想定しています。もしAjaxで取得するなら、onMounted()の中で実行するといいでしょう。

テストしてみる

では、実際にテストしてみましょう。
まずは、アクセスした直後の全体スクショです。

では、ここからはボタンをクリックしながら進めます。

まず、こちらが1つ目のメッセージです。

そして、2つ目です。

続いて、3つ目。

最後に4つ目。

そして、最後の「OKボタン」をクリックすると・・・・・・??

はい❗メッセージが消えました。

クイックツアー、成功です😄✨

デモ・ページをつくりました

今回は実際に体験してもらった方が分かりやすいかな、と思ったのでデモ・ページをつくりました。興味がある方は以下から試してみてください。

📝 デモページ: クイック・ツアーのサンプル

企業様へのご提案

今回のように「ユーザー登録した直後の人たち」へ「どこで何ができるのか?」を分かりやすく表示してあげると、サイト利用がしやすくなることは間違いないと考えています。(目的地に行く時、一度でも地図を見ていたほうが行動しやすいのと同じですね)

また、この「地図」があるのとないのでは、その後の定着率も変わってくるのではないでしょうか。

もし、こういったサイト改善をしたい場合は、ぜひお問い合わせからご依頼ください。お待ちしております。😄✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravel + Vueでクイックツアーを作ってみました。

今回はテストということで、シンプルに表示しましたが、「1/4件」みたいな表示をつけてあげると、途中経過が分かりやすいですし、途中で終了したいかもしれないので、「終了する」ボタンを用意してもいいかもしれません。

また、今回記事を書くきっかけになったサイトでは、黒い半透明のバックスクリーンを使ってより吹き出しポップアップが目立つような工夫をしていました。

やはり有名サイトは「神は細部に宿る」を実践しているな、と感じました。

ぜひ皆さんも工夫したクイックツアーを考えてみてくださいね。

ではでは〜❗

「家に帰ってきたら、
自転車のサイドスタンドが
消えてました…😭」

このエントリーをはてなブックマークに追加       follow us in feedly