Vue Plugin でコンポーネント、ミックスイン、ディレクティブをひとまとめにする

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、この間公開した Vue 3 でクリップボードにコピーできるボタンをつくる という記事が予想していたよりも多くアクセスいしていただき、(テンションが上がったので)今回もVueに関する記事をお届けします。😂

とはいえ、Vueでまだ記事として書いていない話題となるとそれほど数が多くはないのですが、「ひとつやり残している」ことがありました。

それが・・・・・・

Vue プラグイン

です。

Vueプラグイン」とは、シンプルに言うと「Vue本体を改造することができる」機能のことですが、今回は以下のものを作ってみたいと思います。

それは・・・・・・

お金に関する操作を総合的に引き受けてくれる

プラグインです。

例えば、通常数字はケタが多くなると見にくくなるため、3ケタずつ「カンマ(,)」をつけます。

例: 1,000,000円(👈百万円です。つまり、月収7桁といえば100万円以上稼いでるということになりますね。ウラヤマシイ😂)

また、お金を取り扱う場合、誤解がないように「¥」をつけたり「●●円」と表記する場合もあります。

今回はそういった「お金」に関する操作を以下3つの機能で総合的につくり、プラグインとしてひとまとめにしてみます。

  • 数字をカンマ区切りにする「ミックスイン」
  • 入力するときは数字だけど、フォーカスが外れるとカンマが表示される「コンポーネント」
  • 表示する数字を「カンマつき数字」+「円」で表示する「ディレクティブ」

ぜひ「Vueプラグイン」のエッセンスを体験していただけると嬉しいです。😊✨

「結局ファイナンシャルプランナーの資格、
使わなかったな・・・😫」

開発環境: Vue 3

作業の流れ

作業の流れとしては、まずは3つの機能をひとつずつ作り、最終的にそれらを「プラグイン」でひとまとめにします。

では実際にやっていきましょう❗

数字を3ケタずつでカンマ区切りにする「ミックスイン」をつくる

ではまずは、数字を入れてあげたら「3ケタずつカンマ区切りになった文字列」がゲットできる「ミックスイン」をつくっていきます。

ミックスインとは、「ある特定の部分だけを分離できる」機能のことで、目的としては「あっちでも使うし、こっちでも使う。じゃあ、共通化しとけば、使い回しできてラクできるよね👍」というものです。

実際のコードはこちらです。

const priceMixin = {
    methods: {
        commify(price) {

            return this.getPriceAsNumber(price).toLocaleString() +'円'

        },
        getPriceAsNumber(price) { // 文字列ではなく、数字として金額を取得

            price = price.toString().replace(/[^0-9]+/g, '') // 数字以外を除去
            return parseInt(price);

        }
    }
};

この中のメインはもちろんcommify()です。
replace()を使ってカンマつき数字にすることもできますが、シンプルなので、今回はtoLocaleString()を使いました。

また、補助的なgetPriceAsNumber()は、「数字以外が含まれているかもしれない値から数字だけを抜き出す」というメソッドです。

ちなみに、この部分は元々commify()ひとつだけで実装していましたが、他の場所で同じような処理が必要と分かり、わざわざ分割しました。

こうすることで、同じコードを重複させることがなくなるのでコード量が減り、さらに、もし何かあってもここだけ修正すればOK(=つまりラク)ということになります 👍✨

使い方は次のとおりです。

Vue.createApp({
    mounted() {

        console.log(this.commify(1000000)); // 1,000,000円

    }
})
.mixin(priceMixin) // 👈 ここでセットしています。
.mount('#app');

入力するときは数字だけど、フォーカスが外れるとカンマが表示される「コンポーネント」をつくる

では、続いてコンポーネントです。

コンポーネントの条件は次の2つです。

  • フォーカスがあたると type=”number” の数字入力になる
  • フォーカスが外れると3ケタのカンマ区切りになる

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

const priceInputComponent = {
    mixins: [priceMixin],
    props: {
        modelValue: Number
    },
    data() {
        return {
            price: '',
            inputType: 'text',
            isFocusing: false
        }
    },
    methods: {
        onInput() {

            this.$emit('update:modelValue', this.numberPrice); // 変更した値を呼び出し元のVueに伝える

        }
    },
    watch: {
        modelValue: {
            immediate: true,
            handler(value) {

                this.price = value;

            }
        },
        isFocusing: {
            immediate: true,
            handler(value) {

                this.inputType = (value === true)
                    ? 'number'
                    : 'text';

                Vue.nextTick(() => {

                    this.price = (value === true)
                        ? this.numberPrice
                        : this.commify(this.numberPrice);

                });
            }
        }
    },
    computed: {
        numberPrice() {

            return this.getPriceAsNumber(this.price);

        }
    },
    template: `
        <input
            :type="inputType"
            v-model="price"
            @focus="isFocusing=true"
            @blur="isFocusing=false"
            @input="onInput">
    `
};

この中で重要なのは、データが入ってくるpropsと出ていくthis.$emit()の部分です。なぜなら、直接propsを変更するとエラーが出てしまうからです。

詳しくは以下のページをご覧ください。

📝 参考ページ: Vueの定番エラー「Avoid mutating a prop directly…」の原因と対処方法

使い方は次のとおりです。

Vue.createApp({
    data() {
        return {
            price: 10000 // 👈 ここが初期値になります
        }
    },

    // 省略

})
.component('v-price-input', priceInputComponent) // 👈 コンポーネントをセットしてます
.mount('#app');

そしてHTMLはこうなります。

<div id="app">

    <v-price-input v-model="price"></v-price-input>
    <br>
    現在の内部的な値: <span v-text="price"></span>

</div>

表示する数字を「カンマつき数字」+「円」で表示する「ディレクティブ」をつくる

最後にディレクティブです。

ディレクティブは、よく使うv-ifv-forのようにHTMLタグにつけていろいろな操作ができる機能です。今回は数字だけを表示している部分を自動的に「カンマつき数字」にしてみます。

実際のコードはこちら。

const priceDirective = {
    updated(el, binding) {

        el.innerText = binding.instance.commify(el.innerText);

    },
    mounted(el, binding) {

        el.innerText = binding.instance.commify(el.innerText);

    }
};

この中のbinding.instanceは呼び出し元(つまりVueの)インスタンスになります。

そのため、ミックスインで使えるようにしたcommify()にアクセスすることができるわけです。

使い方はこうなります。

Vue.createApp({
    data() {
        return {
            price: 10000,
            price2: 10000, // 👈 ここを追加しました。
        }
    },
    
    // 省略

})
// 省略
.directive('price', priceDirective) // 👈 ここを追加しました
.mount('#app');

そして、HTML側はこうなります。

<div v-text="price2" v-price></div>
<button @click="price2=500000">値を変更する</button>

updatedを設定しているので、ボタンをクリックしてprice2の中身が変更になっても同じく「カンマつき数字」で表示することができます。

Vue プラグインをつくる

では、ここまででつくってきた3つの機能をひとまとめにしてプラグイン化してみましょう。

実際のコードはこちらです。

const pricePlugin = {
    install(app) {

        // ミックスイン
        app.mixin({
            methods: {
                commify(price) {

                    return this.getPriceAsNumber(price).toLocaleString() +'円'

                },
                getPriceAsNumber(price) {

                    price = price.toString().replace(/[^0-9]+/g, '')
                    return parseInt(price);

                }
            }
        });

        // コンポーネント
        app.component('v-price-input', {

            // 👈 ここの mixins: [priceMixin], は不要になりました

            props: {
                modelValue: Number
            },
            data() {
                return {
                    price: '',
                    inputType: 'text',
                    isFocusing: false
                }
            },
            methods: {
                onInput() {

                    this.$emit('update:modelValue', this.numberPrice);

                }
            },
            watch: {
                modelValue: {
                    immediate: true,
                    handler(value) {

                        this.price = value;

                    }
                },
                isFocusing: {
                    immediate: true,
                    handler(value) {

                        this.inputType = (value === true)
                            ? 'number'
                            : 'text';

                        Vue.nextTick(() => {

                            this.price = (value === true)
                                ? this.numberPrice
                                : this.commify(this.numberPrice);

                        });
                    }
                }
            },
            computed: {
                numberPrice() {

                    return this.getPriceAsNumber(this.price);

                }
            },
            template: `
                <input
                    :type="inputType"
                    v-model="price"
                    @focus="isFocusing=true"
                    @blur="isFocusing=false"
                    @input="onInput">
            `
        });

        // ディレクティブ
        app.directive('price', {
            updated(el, binding) {

                el.innerText = binding.instance.commify(el.innerText);

            },
            mounted(el, binding) {

                el.innerText = binding.instance.commify(el.innerText);

            }
        });

    }
};

正直なところ、これまでの3つをほぼコピー&ペーストしているだけですが、少しだけイレギュラーな部分もあります。

それが、以下の「コンポーネント内でのミックスイン呼び出し」が不要になったことです。

mixins: [priceMixin]

なぜ不要になるかというと、app.mixin()Vue本体だけでなく、コンポーネントでも有効になるからなんですね。

使い方はこうなります。

Vue.createApp({

    // 省略

})
.use(pricePlugin) // 👈 ここを追加しました
.mount('#app');

はい❗

ずいぶんスッキリしました。
これもプラグインのいいところですね。

ちなみに: プラグインのオプション

実はプラグインにはoptionsというパラメータを指定することができます。

そのため、今回は「」を「ドル」や「ユーロ」など自由に変更できるようにしてみましょう。

と言ってもコードは以下の部分を変更するだけでOKです。

const pricePlugin = {
    install(app, options) {

        const currency = options.currency || '円'; // 指定がない場合は「円」をつかう

        app.mixin({
            methods: {
                commify(price) {

                    return this.getPriceAsNumber(price).toLocaleString() + currency

                },

// 省略

そして、使い方はこうなります。

Vue.createApp({

    // 省略

})
.use(pricePlugin, {
    currency: 'ドル' // 👈 ここを追加しました
})
.mount('#app');

これで、全てドル表記になります。

デモページを用意しました

今回のプラグインで実装した内容をデモとして公開しました。
ぜひ参考にしてみてください。

📝 デモページ

企業様へのご提案

プラグイン化の最大のメリットは「開発効率の向上」です。

繰り返し使うコードをプラグイン化しておけば、複数のプロジェクトで共通してつかえるため、作業する部分を減らすことができるからです。

また、共通する部分を少しずつ追加していけばプロジェクトを重ねるごとに「開発する必要がない」部分が増えていくことになります。

たとえば、請求書番号など会社独自のフォーマットを「ourCompanyPlugin」といった名前でプラグイン化しておけば、次回からはプラグインをコピーして呼び出すだけで使えるようになります。(プラグインをgitで管理しておくとさらに使い勝手をよくすることもできます)

また、今回はVueの話題でしたが、これはPHPPythonなど別言語でもあっても同様です。

もし何かそういったご希望がございましたら、いつでもお気軽にご相談ください。ぜひよろしくお願いいたします。m(_ _)m

おわりに

ということで、今回はVueプラグインをつくることで、「ミックスイン」「コンポーネント」「ディレクティブ」の3つをひとまとめにしてみました。

もちろん、それぞれ単体で用意しておけば使い回しができるので、必ずプラグイン化する必要はありませんが、似た内容のものが複数存在している場合は、ひとまとめにしておけば管理がラクになると思います。

ちなみに今回は触れませんでしたが、プラグインを使うとdata()の「初期変数」を可変でセットすることもできます。

例えばこんなカンジです。

const dynamicDataPlugin = {
    install(app) {

        let dynamicData = {};

        if(Date.now() % 2 === 0) {

            dynamicData = {
                key1: '',
            };

        } else {

            dynamicData = {
                key2: '',
            };

        }

        app.mixin({
            data() {
                return dynamicData; // ここは固定ではなくなる
            }
        })

    }
};

こんなカンジでVue本体をいろいろと改造することができますので、ぜひ皆さんもやってみてくださいね。

ではでは〜❗

「お客さんにいただいた
お中元のビール
ウマウマです🍺✨」

開発のご依頼お待ちしております 😊✨ お問い合わせ
また、こちらもお待ちしております。
  • 実案件の開発サポート: 詳細
  • ツイッターのフォロー: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly