Vue.jsでページ離脱を阻止する(ダウンロード可)

さてさて、気がつけば今年も上半期が終了し、さらに令和の時代になってから2ヵ月が過ぎてしまい時間のスピードに驚く今日この頃です。

そして、さらに時間の流れを感じるのが「Vueの次期バージョン3.0」のニュースをちらほらと見るようになってきたことです。(どうやら次期バージョンの仕様で不満が出たりもしているようですが…)

私が3.0のニュースははじめに見たのが2018年の年末でしたので、やはりすでに半年がビューーン!と過ぎてしまったことになります。

正直なことを言うとこのブログでもVueの記事をたくさん公開してきましたが、最近サボってますね(すみません。Laravel or その他が面白くてちょっとした浮気・・・😅)

でもたまにはVueも使っていないと、スキルが低下していくことは間違いないので、ここいらでその昔きちんと実行可能かどうかをチェックしておきたかった「ページ離脱時の挙動」を紹介したいと思います。

ぜひ皆さんのお役に立てると嬉しいです!
(最後でソースコードをダウンロードすることができます)

実行環境: Google Chrome 75、Firefox 68、IE 11

やりたいこと

わかりやすい例で言うと、wordpressで記事を書いていて保存していないのに閉じようとすると「まだ記事が保存されてないけどホントに大丈夫!?」と警告を出してくれますが、この機能をVueを使って実装してみたいと思います。(つまり、何も入力がなければ警告は出さないようにします)

なお、今回は以下の2バージョンを作ってみることにしました。

  1. シンプルバージョン
  2. 厳密バージョン

また、「ページ離脱の警告」が実行されるのは以下の3パターンです。

  1. リンクから他のページへ移動しようとした
  2. ページが再読み込みされた
  3. タブを閉じようとした

なお、擬似的に「保存する」ボタンをクリックしたら警告は出さないようにしますが、さらに入力データされてページ離脱しようとすると警告が出るようにします。

では、ひとつずつ見ていきましょう。

ページ離脱をキャッチして警告を表示するコードをつくる

シンプルバージョン

シンプルバージョンは、入力データに変更があった時点で「ページ離脱の警告」を有効にする、シンプルな内容になっています。

<!DOCTYPE html>
<html>
<head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div id="app" class="container">
        <span class="badge badge-primary">サンプル - 1</span>
        <h3>入力内容を変更するとページ移動する前にアラートが出ます。</h3>
        <div class="row">
            <div class="col-md-6 form-group">
                <label>名前</label>
                <input class="form-control" type="text" v-model="params.name">
            </div>
            <div class="col-md-6 form-group">
                <label>かな</label>
                <input class="form-control" type="text" v-model="params.name_kana">
            </div>
        </div>
        <div v-if="dirty">&#x26A0; 保存されていないデータがあります</div>
        <hr>
        <ul>
            <li>
                パターン1: <a href="https://yahoo.co.jp">他のページへ移動します</a>
            </li>
            <li>
                パターン2: <a href="#" @click.prevent="location.reload()">リロードします</a>
            </li>
            <li>
                パターン3: タブを閉じる(閉じるボタンをクリックしてください)
            </li>
        </ul>
        <br>
        <button type="button" class="btn btn-warning" @click="save">保存する</button>(実際には保存はしません)
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                params: {
                    name: '',
                    name_kana: ''
                },
                dirty: false
            },
            methods: {
                save: function() {

                    // ここで保存するコード

                    this.dirty = false;

                }
            },
            mounted: function() {

                var self = this;

                window.onbeforeunload = function() {

                    if(self.dirty) {

                        return '未保存のデータがあります。処理を実行しますか?';

                    }

                };

            },
            watch: {
                params: {
                    handler: function() {

                        this.dirty = true;

                    },
                    deep: true
                }
            }
        });

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

実際に表示はこちら↓↓↓

watchについて

この中で重要なのが、watchの部分です。

このメソッドはVueの変数paramsに変更があったときに実行されるようになっていますが、おそらく通常使っているwatchメソッドとは違っていると思います。

なぜなら、paramsは単体のデータを持つ通常の変数ではなく、オブジェクト(今回ではname, namae_kanaの2つデータを持っています)だからです。

つまり、watchにオブジェクトのデータを設定するには、deepオプションをtrueにし、さらにメソッドはhandler()を指定する必要があるのです。

そして、この中では変数dirtytrueにして、データ変更があったかどうかをチェックするようにしています。

onbeforeunloadについて

次にonbeforeunloadですが、以下ように値をreturnで返していますが実際にはGoogle ChromeFirefoxではこの文字列が影響されることはなく、IEのみが有効になっています。

if(self.dirty) {

    return '未保存のデータがあります。処理を実行しますか?';

}

※ ただし、必要ないからといって何も返さないようにするとページ離脱の警告が実行されなくなってしまうので気をつけてください。

save()について

save()内ではAjax通信などでデータ送信することを想定しています。そのため、もしaxiosを使った場合は以下のようになります。

var self = this;
axios.post('/url')
    .then(function(response){

        self.dirty = false;

    });

※ IEではPromiseに対応していないため、axiosがそのままでは動きません。その場合はes6-promiseを使ってください。

厳密バージョン

続いて厳密バージョンです。

先ほどのシンプルバージョンでは、入力データに変更があった場合に必ず警告を有効にしますが、この場合、もし一旦変更して元に戻した場合でも警告が表示されてしまう可能性がでてきます。

そのため、厳密バージョンでは「当初のデータと今のデータが同じかどうか」をチェックするようにしています。

<!DOCTYPE html>
<html>
<head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div id="app" class="container">
        <span class="badge badge-primary">サンプル - 2</span>
        <h3>入力内容を変更するとページ移動する前にアラートが出ます。</h3>
        <div class="row">
            <div class="col-md-6 form-group">
                <label>名前</label>
                <input class="form-control" type="text" v-model="params.name">
            </div>
            <div class="col-md-6 form-group">
                <label>かな</label>
                <input class="form-control" type="text" v-model="params.name_kana">
            </div>
        </div>
        <div v-if="dirty">&#x26A0; 保存されていないデータがあります</div>
        <hr>
        <ul>
            <li>
                パターン1: <a href="https://yahoo.co.jp">他のページへ移動します</a>
            </li>
            <li>
                パターン2: <a href="#" @click.prevent="location.reload()">リロードします</a>
            </li>
            <li>
                パターン3: タブを閉じる(閉じるボタンをクリックしてください)
            </li>
        </ul>
        <br>
        <button type="button" class="btn btn-warning" @click="save">保存する</button>(実際には保存はしません)
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                params: {
                    name: '',
                    name_kana: ''
                },
                originalParams: {}
            },
            methods: {
                copyParams: function() {

                    this.originalParams = JSON.parse(JSON.stringify(this.params));

                },
                save: function() {

                    // ここで保存するコード

                    this.copyParams();

                }
            },
            mounted: function() {

                var self = this;

                window.onbeforeunload = function() {

                    if(self.dirty) {

                        return '未保存のデータがあります。処理を実行しますか?';

                    }

                };

                this.originalParams = JSON.parse(JSON.stringify(this.params));

            },
            computed: {
                dirty: function() {

                    return (JSON.stringify(this.params) !== JSON.stringify(this.originalParams));

                }
            }
        });

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

先ほどのシンプルバージョンとの大きな違いは、watchがなくなり代わりにcomputedが登場しているところです。

つまり、dirty「当初のデータと今のデータが同じかどうか」をチェックしてtrue/falseを返すのですが、この比較をJSON形式で行うと文字列の比較になるので、よりシンプルに実現できます。

そして、同じくデータのコピーを作成するのも一旦JSON形式にして、さらに元に戻しています。

なぜなら、そのまま変数に入れてしまうと「同じもの」とされてしまい、paramsが変更された場合、同じくoriginalParamsの中身まで変更されてしまうからです。(つまり、この場合ですと常にdirtyfalseを返すようになってしまいます)

あとは、save()で再び現在のparamsをコピーしてoriginalParamsとして状態をリセットします。

お疲れ様でした!

ソースコードをダウンロードする

以下から今回開発したソースコード一式をダウンロードすることができます。
VueBootstrapcdnから呼び出しているのですぐ確認することができます!

Vue.jsでページ離脱を阻止する

おわりに

ということで、今回はVueを使ってページ離脱を防ぐ方法を紹介しました。

ちなみに私は業務管理のウェブシステムを請け負うことが結構あるのですが、ユーザビリティとして「保存忘れ」をなくすためににこのテクニックを使うこともあります。

なお、たまにデータ入力するものは一切ないのに、ページ離脱しようとした瞬間に「ホントにいいの!?損しちゃうよ??」みたいなカンジで引き止めに入ろうとする場合がありますが、あれってちょっと印象悪かったりしますよね😭

なので、やっぱり何でも適材適所なのかなと思ったりもします。
ぜひ皆さんも適材適所で今回のテクニックを活用してみてくださいね。

ではではー!