👻

CDN版Vueで親と子のコンポーネント間で通信して値を同期させてみる

2022/09/02に公開

共用レンタルサーバーでVueを動かす必要があるので色々と試行錯誤中です。

親と子のコンポーネント間で通信

まずは親と子のコンポーネント間で通信する方法について。
Vueではコンポーネントを呼び出すと自動的に親子関係になります。
なぜ親子関係を作るかというとコードが増えていき見通しが悪くなるからです。

なのでコンポーネントという部品ごとに切り分けて、その中で完結させた方がコードのメンテナンス性が高くなり、また共通コンポーネントとして使い回しやすくなります。

で、問題になるのがコンポーネントの機能をコンポーネント自身に持たせた場合、その処理結果がクローズであることです。
例えばログイン後にユーザーアイコンがヘッダーに表示されるような画面の場合、ヘッダーをコンポーネントにしてログインデータを持たせるとします。
ユーザーのログイン状態をあとで使いたい場合、そのデータはヘッダーのコンポーネントにあるため直接アクセスできません。

別のコンポーネントで別途ユーザーのログイン状態を取得するという手もありますが、それも無駄です。
そこで、なんとか親と子のコンポーネントで通信できないか? と思い描くわけです。
もちろん可能です。しかし、クセがあるという話。

親から子はprops 子から親へは$emit

Vueでは親から子にデータを渡す場合は「props」を使います。
子から親にデータを渡す場合は「$emit」を使います。
とにかく使います。異論は認めません。

この辺は調べればすぐに出てくるのですが、問題は親から子へデータをpropsで受け取ったとして、子ではpropsの値を直接変更してはいけない、というルールがあることです。
親から借りてきているデータなので、借りてきたものを自分勝手にいじってはいけませんよ、ということらしい。
「私ぃ、親からもらった身体を傷つけるなんてできな~~い」というアレです。
単に親からのデータを子で表示する場合は問題はない。

例えば、タイトル文字列を受け取って、ダイアログのタイトルに表示するだけという場合は借りるだけでOK。
ところが、親からのデータをテキストボックスに表示させて、なおかつその変更をユーザーから可能にして、かつ親側のデータも書き換える場合がよく分からない。
そう、同期(リアクティブ)にしたいわけです。

まずはコードを載せます。

index.html
<html>
    <head>
        <meta charset="UTF-8" />
        <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
        <script src="//unpkg.com/http-vue-loader"></script>
        <title>親子間の通信</title>
    </head>
    <body>

    <div id="app">
        <div>
            親:<input type="text" v-model="input">親のデータ「{{input}}</div>
        <vc-child v-bind:test="input" v-on:childs-event="parentMethod"></vc-child>
    </div>

  </body>

<script>

const vue = new Vue(
{
    el                  : '#app',
    components          : 
    {
        'vc-child'      : httpVueLoader( 'vc-child.vue' )
    },
    methods             :
    {
        parentMethod    : function( value )
        {
            alert( `イベントキャッチ。\n親の値を「${value}」で書き換えます` );

            //親のデータを書き換え
            this.input = value;
        },
    },
    data                : () => 
    ({
        input           : null ,
    }),

});

</script>

</html>
vc-child.vue
<template>
    <div>
        <div>
            子:<input type="text" v-model="alt_test">親からのデータ「{{test}}</div>
        <input type="button" v-on:click="handleClick" value="親に通知">
    </div>
</template>

<script>
module.exports =
{
    methods             :
    {
        //親に通知する
        handleClick     : function()
        {
            //$emitで親にイベントで知らせる
            this.$emit( 'childs-event' , this.alt_test );
        },
    },
    watch               :
    {
        //親からのpropsであるtestを監視する
        test            : function( newVal , oldVal )
        {
            //値が書き換わったらtestをalt_testに代入する
            this.alt_test = this.test;
        },
    },
    data                : () => 
    ({
        alt_test        : null ,
    }),
    props               : [ 'test' ] ,

}
</script>

コードの説明

親からは渡すデータをカスタタグに「v-bind」で紐づけます。
と、同時に子から「$emit」でイベントで返ってきたデータを処理するメソッドをカスタタグに「v-on」で紐づけます。
ポイントは、親から子へはプロパティ(props=属性)で渡して、子から親へはイベントで渡すことです。

index.html
<vc-child v-bind:test="input" v-on:childs-event="parentMethod"></vc-child>

一方で、子の方は親からの属性を受け取るpropsを追加します。

vc-child.vue
props               : [ 'test' ]

これで子側では「this.test」として扱うことができます。
※複数のpropsがある場合は配列なのでカンマで区切って指定します。
子のテキストボックスに「v-model」でバインドすれば親のデータと同期します。

vc-child.vue
子:<input type="text" v-model="test">親からのデータ「{{test}}

しかし、前述したように「this.test」を直接いじると怒られます。
テキストボックスは値を変更できるので値を書き換えるとエラーとなります。

テキストでは直接いじらずに算出プロパティ(computed)を使うべしとあるのだが、算出プロパティも編集するとエラーで怒られるんだよね。
算出プロパティではgetとsetで値を変更できるということらしいが、なんかそれも面倒だ。

で、どうしようかと思って「watch」を使ったらうまくいった。
「watch」なんて使う場面がないと思っていたらようやく使うことができました。

vc-child.vue
watch               :
{
    //親からのpropsであるtestを監視する
    test            : function( newVal , oldVal )
    {
        //値が書き換わったらtestをalt_testに代入する
        this.alt_test = this.test;
    },
},

watchを使う

「watch」は値を指定しておくと監視してくれて、変更があれば起動するというもの。
「this.test」を監視して変更があれば都度「this.alt_test」に反映する。
テキストボックスも以下のようにすれば問題なし。

vc-child.vue
子:<input type="text" v-model="alt_test">親からのデータ「{{test}}

これで親のテキストボックスを変更すると、都度、子のテキストボックスも同期して変更されるようになる。
次に、子側のテキストボックスの値を変更して、それを親に伝える方法。
流れとしては以下の通り。

  1. まず親側のテキストボックスに「おはよう」と入力する。
  2. 子側のテキストボックスにも「おはよう」と表示される。
  3. 子側のテキストボックスを「おやすみ」に変更する。
  4. [ 親に通知 ] ボタンを押すと親側のテキストボックスが「おやすみ」に変更される。

[ 親に通知 ] ボタンには「v-on」でメソッドを割り当てておく。

vc-child.vue
<input type="button" v-on:click="handleClick" value="親に通知">

「handleClick」メソッドでは「$emit」を使って親側で紐づけた「childs-event」を発火させると、親側の「parentMethod」メソッドが続けて発火する。

vc-child.vue
methods             :
{
    //親に通知する
    handleClick     : function()
    {
        //$emitで親にイベントで知らせる
        this.$emit( 'childs-event' , this.alt_test );
    },
},

親側の「parentMethod」で受け取った値で「this.input」を上書きすれば、子から親への値の受け渡しは完了。

index.html
methods             :
{
    parentMethod    : function( value )
    {
        alert( `イベントキャッチ。\n親の値を「${value}」で書き換えます` );

        //親のデータを書き換え
        this.input = value;
    },
},

因みに子側のテキストボックスに「v-on:change="handleClick"」を追加するとわざわざ[ 親に通知 ] ボタンを押さなくても変更する度に自動で親にイベントが渡る。

vc-child.vue
子:<input type="text" v-model="alt_test" v-on:change="handleClick">親からのデータ「{{test}}

Discussion