Vue Component で和暦から西暦に変換するセレクトボックスをつくる

さてさて、前回の記事「Vue Component で2枚の画像を切り替えるチェックボックスをつくってみよう」では独自のコンポーネント(≒疑似HTMLタグ)を作って利用規約の同意で使ってみました。

そして、この記事を書いている時に、その昔クライアントさんからご要望があった「ちょっとめんどうな」セレクトボックスがあったことを思い出しました。

それが、元号 + 年で生年月日を入力するセレクトボックスです。

もちろん西暦ならセレクトボックスひとつで問題なのですが、これまで長い間お客さんのデータを和暦で管理してきた関係上、入力は和暦にしたいとのご要望でした。

ということで、今回は前回に引き続きVue Componenntの話題で、和暦 + 年で連携するセレクトボックスをつくってみます。

ぜひ学習の役に立ててくださいね。(最後に教材ソースコードをダウンロードできます。)

やりたいこと

まずセレクトボックスは次の2つです。

  • 元号

そして、「元号」を選択すると自動的に「年」の中身が元年〜その元号の最大年に変化するようにします。

では、実際に開発を進めていきましょう!

Vueの基本形をつくる

では、Vueが使えるように基本形を作り、ここにコンポーネントをつくっていきます。

<html>
<body>
    <div id="app">
        <!-- ここのVueコンポーネント -->
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
    <script>

        new Vue({
            el: '#app'
        });

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

※ Vue本体はcdnから読み込んでいます。

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

今回のコンポーネントは元号と年のセレクトボックスが必要になり、さらに各セレクトボックスに様々なコードが必要になってくるので、構造は次のようにします。

  • v-wareki-input ・・・ 親コンポーネント
  • v-wareki-gengo-input ・・・ 元号の選択 (子コンポーネント – 1)
  • v-wareki-year-input ・・・ 年の選択 (子コンポーネント – 2)

つまり、v-wareki-inputというコンポーネントの中に、v-wareki-gengo-inputv-wareki-year-inputという2つの子コンポーネントが含まれている形になります。

ではひとつずつ開発していきましょう。

元号を選択するセレクトボックス

まずは次のように元号を選択できるセレクトボックスを自動生成するコンポーネントです。

Vue.component('v-wareki-gengo-input', {
    props: {
        value: {
            type: String,
            default: 0
        }
    },
    template:
        '<select v-model="value">'+
            '<option></option>'+
            '<option v-for="(gengo,index) in $parent.gengoData" :value="index" v-text="gengo.text"></option>'+
        '</div>',
    watch: {
        value: function(value) {

            this.$emit('input', value);

        }
    }

});

では重要な部分をひとつずつみていきましょう。

props

valuev-modelの中身が入ってくる変数で、選択中された元号のインデックス番号です。つまり、後でつくる親コンポーネントから次のように利用します。

<v-wareki-gengo-input v-model="gengoIndex"></v-wareki-gengo-input>

template

重要なのは、v-forを使って元号の選択肢を作っている部分です。
なお、$parent.gengoDataはこれからつくる親コンポーネントの変数gengoDataからデータをとってくるという意味になります。

v-for="(gengo,index) in $parent.gengoData"

watch

valueが変更されたらinputイベントを送出します。
これによって親コンポーネントに値を伝えることができるようになります。

年を選択するセレクトボックス

続いて、年を選択するセレクトボックスですが、これは元号によって選択肢が自動的に変更になるようにします。(明治: 45年/大正: 15年/昭和: 63年/平成: 30年)

Vue.component('v-wareki-year-input', {
    props: {
        max: {
            type: Number,
            default: 0
        },
        value: {
            type: Number,
            default: 0
        }
    },
    template:
        '<select v-model="value">'+
            '<option></option>'+
            '<option v-for="i in max" :value="i" v-text="year(i)"></option>'+
        '</select>',
    methods: {
        year: function(i) {

            if(i == 1) {

                return '元年';

            }

            return i +'年';

        }
    },
    watch: {
        value: function(value) {

            this.$emit('input', value);

        }
    }

});

では、ひとつずつみていきます。

props

このコンポーネントに必要なデータは、

  • 選択した年
  • 選択した元号に対応する最大年数

の2つです。

そのため、親コンポーネント内では、次のように2つの変数を格納するようにします。

<v-wareki-year-input :max="maxGengoYear" v-model="gengoYear"></v-wareki-year-input>

つまり、maxGengoYearは先ほどの元号セレクトボックスが選択されたらすぐ自動的に切り替わる必要があります。(これについては親コンポーネントのcomputedで説明します)

template

まず、v-forではmax、つまり最大年数分の選択肢を作成しています。

v-for="i in max"

また、表示するテキストは「1年」の場合は「元年」とするべきですので、year()というメソッドをつくり、この中で値が1の場合のみ「元年」を返すようにしています。

watch

このコンポーネントでも、valueが変更されたらinputイベントを送出し、親コンポーネントへ値を伝えています。

親コンポーネント

では最後に親コンポーネントです。
2つの子コンポーネントを連携させるので複雑な部分もありますが、できるだけわかりやすく説明していきます。

Vue.component('v-wareki-input', {
    props: {
        value: {
            type: Number,
            default: 0
        }
    },
    template: '<span>'+
                '<v-wareki-gengo-input v-model="gengoIndex"></v-wareki-gengo-input>&nbsp;'+
                '<v-wareki-year-input :max="maxGengoYear" v-model="gengoYear"></v-wareki-year-input>&nbsp;'+
                '</span>',
    data: function() {
        return {
            gengoIndex: '',
            gengoYear: 0,
            gengoData: [
                {text: '明治', base: 1867, max: 45},
                {text: '大正', base: 1911, max: 15},
                {text: '昭和', base: 1925, max: 63},
                {text: '平成', base: 1988, max: 30},
            ],
            initialized: false
        }
    },
    methods: {
        calcYear: function() {

            var year = null;

            if(this.hasGengo && this.gengoYear > 0) {

                var gengo = this.gengoData[this.gengoIndex];
                year = this.gengoYear + gengo.base;

            }

            this.$emit('input', year);

        }
    },
    computed: {
        hasGengo: function() {

            return (this.gengoData[this.gengoIndex] != undefined);

        },
        selectingGengo: function() {

            return (this.hasGengo)
                ? this.gengoData[this.gengoIndex]
                : null;

        },
        maxGengoYear: function() {

            return (this.hasGengo)
                ? this.selectingGengo.max
                : 0;

        }
    },
    mounted: function() {

        if(this.value > 0) {

            for(var i in this.gengoData) {

                var gengo = this.gengoData[i];

                if(this.value > gengo.base) {

                    this.gengoIndex = i;
                    this.gengoYear = this.value - gengo.base;

                }

            }

        }

        var self = this;

        Vue.nextTick(function(){

            self.initialized = true;

        });

    },
    watch: {
        gengoIndex: function() {

            if(this.initialized && this.hasGengo) {

                this.gengoYear = 0;

            }

            this.calcYear();

        },
        gengoYear: function() {

            this.calcYear();

        }
    }
});

では、ひとつずつ見ていきます。(わかりやすく説明したいので順不同です)

props

valueはv-modelに対応する部分です。
例えば、次のように初期値を西暦で入力すると、自動的に和暦に変換するようにします。

<v-wareki-input v-model="2000"></v-wareki-input>

data

変数は次のとおりです。

  • gengoIndex ・・・ 選択された元号のインデックス
  • gengoYear ・・・ 選択された年
  • gengoData ・・・ 元号のマスターデータ。ここで一元管理したいので、子コンポーネントからは$parentとして呼び出しています。もし新元号になったらここにデータを追加するだけでOKです。
  • initialized ・・・ ページ表示されてすぐかどうかかが分かる変数。true or false

※ なお、Vueコンポーネントは関数の返り値として設定する必要があります。そのため、functionを使っています。

computed

computedの内容は次のとおりです。

  • hasGengo() ・・・ 元号が選択されているかどうか。true or false
  • selectingGengo() ・・・ 現在選択されている元号(だけ)のデータ
  • maxGengoYear() ・・・ 現在選択されている元号の最大年数

template

2つのこのコンポーネントをテンプレートとして呼び出し、それぞれ変数gengoIndexgengoYearをバインディングしています。

重要なのは、maxGengoYearの部分で、これはcomputedmaxGengoYear()の結果が自動的に入るようにしています。

methods

calcYear()は、元号と年の両方が選択された状態なら、それが西暦何年かを計算し、結果を$emitで呼び出し元に伝えています。つまり、次の場合、変数yearが自動的に更新されます。

<v-wareki-input v-model="year"></v-wareki-input>

※ なお、もし元号や年が選択されていないときはnullを返すようにしています。

mounted

value(西暦)に初期値が入っていたら、計算して元号と年をセットするようにしています。また、最後にinitializedtrueに変更しています。(この部分は詳しくwatchで説明します)

watchs

変数gengoIndexgengoYearに変更があったらcalcYear()で西暦を計算するようになっています。

そして、ここで重要なのがgengoIndexの次の部分です。

if(this.initialized && this.hasGengo) {

    this.gengoYear = 0;

}

なぜページが表示されてすぐの場合はgengoYear0にはしないかというと、例えば、初期値を2000にした場合、mountedの中で

  • 元号 ・・・ 平成
  • 年 ・・・ 12

と和暦への変換が行われます。そして、それぞれgengoIndexgengoYearにその値が格納されることになるのですが、もしinitializedで守っていないとgengoIndexを変更した時点で、watchによって120に上書きされてしまうのです。

mounted内でnextTick()を使っているのもそのためです。

var self = this;

Vue.nextTick(function(){

    self.initialized = true;

});

nextTick()は一旦レンダリングが完了してから中身を実行するのですが、こうしないと同じくタイミング的にgengoYear0に上書きされてしまいます。

デモを用意しました

実際のキャプチャ画像は次のとおりですが、画像だけでは分かりにくいかもしれませんので、実際に体験できるデモページを用意しました。ぜひ触ってみてください。

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

今回開発したソースコード一式(Vue Componentを含む)を以下からダウンロードできます。cdnVueを読み込んでいるので展開してすぐ実行することができます。

和暦を入力するVue Component: ソースコード一式