第13回 Vue.js $emit 子コンポーネントから親コンポーネントへのデータの受け渡し

プログラミング
世代 受け渡し

こんにちは、コンスキです。

データの受け渡しには、「親コンポーネントから子コンポーネント」と「子コンポーネントから親コンポーネント」という2つの方向がありましたね。

前回はそのうちpropsプロパティを使った「親コンポーネントから子コンポーネント」の方を説明しました。

今回は、もう一つの$emitを使った「子コンポーネントから親コンポーネント」へのデータの受け渡しを解説します。

$emitとは関係ありませんが、これからの説明の中で、アプリケーションに対してコンポーネントが1つだけしか使われていない場合、Vueインスタンス生成に引数に渡すオブジェクトの中身のことを親コンポーネントと呼ぶことがあります。一方で、Vue.component APIの引数に渡す、オブジェクトの中身のことを子コンポーネントと呼ぶことがあります。

このようによ部理由は、親コンポーネントの表示を変える設定がVueインスタンスにわたすオブジェクトの中身であり、子コンポーネントの表示を変える設定たVue.component APIに渡すオブジェクトの中身であるためです。

親 → 子のデータ受け渡しの必要性

子コンポーネントから親コンポーネントへのデータの受け渡しはどんなときにする必要があるのでしょうか。

例えば、通販サイトを例に考えてみましょう。商品ごとの購入個数と購入総数を別々に表示したいとします。まず、各商品において、商品ごとの購入個数を計算するためのボタン要素と購入個数を表示する要素が必要です。

この2つの要素は、複数の商品で使う必要があるため、この2要素を使ったコンポーネント作ったとします。ここで、そのコンポーネントがをボタンを押された回数から商品ごとの購入個数を求めることは比較的簡単にできることがわかると思います。

それでは、異なる商品の購入個数から全体での商品の購入数を求めた後に、そのコンポーネントが使われている親コンポーネントの方で、異なる商品の購入個数から求めた購入商品の総数を使わなければいけなくなったときにどうすればよいでしょうか。

ボタン要素と購入個数を表示するための要素コンポーネントを使っていなければ、次のように1つの値をデータに作って、その値をボタンが押されるたびに増やしていけば実現することができます。

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <title>$emit</title>
  <script src="https://unpkg.com/vue"></script>
</head>

<body>
  <div id="app">
    <div>
      <button v-on:click="counter1">カートに追加する</button>
      <div>購入個数:{{ count1 }}</div>
    </div>
    <div>
      <button v-on:click="counter2">カートに追加する</button>
      <div>購入個数:{{ count2 }}</div>
    </div>
    <div>
      <button v-on:click="counter3">カートに追加する</button>
      <div>購入個数:{{ count3 }}</div>
    </div>
    <div>
      <button v-on:click="counter4">カートに追加する</button>
      <div>購入個数:{{ count4 }}</div>
    </div>
    <div>購入総数:{{ total }}</div>
  </div>
  <script>
    var vm = new Vue({
      data: {
        count1: 0,
        count2: 0,
        count3: 0,
        count4: 0
      },
      methods: {
        counter1:function () {
          this.count1++
        },
        counter2:function () {
          this.count2++
        },
        counter3:function () {
          this.count3++
        },
        counter4:function () {
          this.count4++
        }
      }
    })
  </script>
</body>
</html>>

しかし、コンポーネントを使っていた場合は、各商品のボタンのために作った子コンポーネントで計算した値を、親コンポーネント側で単純に使うことはできません。

ここで子コンポーネントでボタンを押したということを、親コンポーネントに伝える必要が出てきました。

つまり、子コンポーネントで発生したイベントと言うデータを親コンポーネントへ受け渡さなければいけません。

そこで、$emitというものを使います

$emitを使ったデータの受け渡し方

$emitとは、インスタンスメソッドと呼ばれるものです。

このインスタンスメソッドを使うことで、子コンポーネントで起こったイベントを親コンポーネントに渡すことができます。

子コンポーネントで発生したクリックイベントを、親コンポーネントへ渡す場合の基本的な書き方は次のようになります。この場合、子コンポーネントはVue

html側

<div id="app">
  <子コンポーネント名 v-on:this.$emmitの引数="処理または親コンポーネント内のメソッドのキー"></子コンポーネント名>
</div>

Javascript側↓

Vue.component("子コンポーネントの名", {
  template: "<要素名 v-on:click="$emitが使われるメソッドのキー"></要素名>",
  methods: {
    キー: function () {
      //要素がクリックさたときの子コンポーネント内での処理を書きます
      this.$emit("イベントの名前(自分の好きな名前でOKです)")
    }
  }
})
var vm = new Vue({
  el: "#app",
  methods: {
    キー: function () {
        //ここに子要素のthis.$emitのイベントを受け取ったら行う処理を書きます
    }
  }
})

実際のコードの例として、子コンポーネントで起こったクリックイベントを親コンポーネントへ受け渡す際のコードを下に書きました。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>$emit</title>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
  <div id="app">
    <div v-for="item in items" v-bind:key="item.id">
      {{ item.name }}<item-component v-on:counter="totalCounter()"></item-component>
      <br>
    </div>
    <br>  
    <div>購入総数:{{ total }}</div>
  </div>  
  <script>
    Vue.component("itemComponent", {
      template: `
        <div>
          <button v-on:click="counter">カートに追加する</button>
          <div>購入個数:{{ count }}</div>
        </div>
      `,
      data: function () {
        return {
          count: 0
        }
      },
      methods: {
        counter: function () {
          this.count++
          this.$emit("counter")
        }
      }
    })
    var vm = new Vue({
      el: "#app",
      data: {
        items: [
          {
            name: "商品1",
            id: 1
          },
          {
            name: "商品2",
            id: 2
          },
          {
            name: "商品3",
            id: 3
          },
          {
            name: "商品4",
            id: 4
          }
        ],
        total: 0      
      },
      methods: {
        totalCounter: function () {
          this.total++
        }
      }
    })
  </script>
</body>
</html>

ブラウザに表示すると次のようになると思います。

商品1から商品4までの「カートに追加する」のボタンを押すことで、それぞれの商品の購入個数が増加するだけではなく、それに合わせて購入総数まで増えている事がわかると思います。

これは、「カートに追加する」ボタンを押したことで発生した子コンポーネントのイベントが親コンポーネントに渡されていることによってできるようになっています。

雲孫コンポーネントのイベントを親コンポーネントに渡す

ここまでで、子コンポーネントから親コンポーネントへのイベントの受け渡しできることがわかりました。

それでは、孫コンポーネントから子コンポーネントへイベントを渡した後、さらにそのイベントを親コンポーネントまで渡すことはできるのでしょうか。

どうせなら、もっと次の世代のコンポーネントから親コンポーネントまでリレーをするようにイベントを受け取れるかを検証したかったため、孫の次は何か調べました。

子、孫、ひ孫。ひ孫の子どもは「玄孫(やしゃご)」となり、ここまでは何とかたどり着けそうです。問題はその後。「来孫(らいそん)」「昆孫(こんそん)」「仍孫(じょうそん)」「雲孫(うんそん)」とつづきます。雲孫以降は特に決まった呼び名があるわけではないようです。

子、孫、ひ孫、玄孫、その次は何? | 介護のほんねニュース

ということで、「子→孫→ひ孫→玄孫→来孫→昆孫→仍孫→雲孫」という順番だったため、一番最後の雲孫コンポーネントから親コンポーネントまでクリックイベントをリレーしてみようと思います。

わかりやすいように、イベントを受け渡すたびに言葉を付け加えて、親コンポーネントまで渡されたところで文が完成するようにします。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>$emit</title>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
  <div id="app">
    <ko-component v-on:ko="appendWord"></ko-component>
    <p>親:{{ word }}</p>
  </div>  
  <script>
    Vue.component("unsonComponent", {
      template: `
        <div>
          <p style="display:inline">雲孫:</p><input type="text" v-model="firstWord">
          <button v-on:click="emitWord">親まで渡す</button>
        </div>
      `,
      data: function () {
        return {
          firstWord: "次回"
        }
      },
      methods: {
        emitWord: function () {
          this.$emit("unson", this.firstWord)
        }
      }
    })
    Vue.component("jousonComponent", {
      template: `
        <div>
          <unson-component v-on:unson="appendWord"></unson-component>
          <p>仍孫:{{ word }}</p>
        </div>
      `,
      data: function () {
        return {
          word:"",
          jousonWord: "は",
        }
      },
      methods: {
        appendWord: function (word) {
          this.word = word + this.jousonWord
          this.$emit("jouson", this.word)        
        }
      }
    })
    Vue.component("konsonComponent", {
      template: `
        <div>
          <jouson-component v-on:jouson="appendWord"></jouson-component>
          <p>昆孫:{{ word }}</p>
        </div>
      `,
      data: function () {
        return {
          word:"",
          konsonWord: "スロット",
        }
      },
      methods: {
        appendWord: function (word) {
          this.word = word + this.konsonWord
          this.$emit("konson", this.word)        
        }
      }
    })
    Vue.component("raisonComponent", {
      template: `
        <div>
          <konson-component v-on:konson="appendWord"></konson-component>
          <p>来孫:{{ word }}</p>
        </div>
      `,
      data: function () {
        return {
          word:"",
          raisonWord: "コンテンツ",
        }
      },
      methods: {
        appendWord: function (word) {
          this.word = word + this.raisonWord
          this.$emit("raison", this.word)        
        }
      }
    })
    Vue.component("yashagoComponent", {
      template: `
        <div>
          <raison-component v-on:raison="appendWord"></raison-component>
          <p>玄孫:{{ word }}</p>
        </div>
      `,
      data: function () {
        return {
          word:"",
          yashagoWord: "に",
        }
      },
      methods: {
        appendWord: function (word) {
          this.word = word + this.yashagoWord
          this.$emit("yashago", this.word)        
        }
      }
    })
    Vue.component("himagoComponent", {
      template: `
        <div>
          <yashago-component v-on:yashago="appendWord"></yashago-component>
          <p>ひ孫:{{ word }}</p>
        </div>
      `,
      data: function () {
        return {
          word:"",
          himagoWord: "ついて",
        }
      },
      methods: {
        appendWord: function (word) {
          this.word = word + this.himagoWord
          this.$emit("himago", this.word)        
        }
      }
    })
    Vue.component("magoComponent", {
      template: `
        <div>
          <himago-component v-on:himago="appendWord"></himago-component>
          <p>孫:{{ word }}</p>
        </div>
      `,
      data: function () {
        return {
          word:"",
          magoWord: "説明",
        }
      },
      methods: {
        appendWord: function (word) {
          this.word = word + this.magoWord
          this.$emit("mago", this.word)        
        }
      }
    })
    Vue.component("koComponent", {
      template: `
        <div>
          <mago-component v-on:mago="appendWord"></mago-component>
          <p>子:{{ word }}</p>
        </div>
      `,
      data: function () {
        return {
          word:"",
          koWord: "します",
        }
      },
      methods: {
        appendWord: function (word) {
          this.word = word + this.koWord
          this.$emit("ko", this.word)        
        }
      }
    })
    var vm = new Vue({
      el: "#app",
      data: {
        word: "",
        oyaWord: "!"
      },
      methods: {
        appendWord: function (word) {
          this.word = word + this.oyaWord
        }
      }
    })
  </script>
</body>
</html>

上のプログラムをブラウザに表示すると次のようになります。

「親まで渡す」と書かれているボタンをクリックして見て下さい。

雲孫コンポーネントのボタンがクリックされたことが親まで伝わりました。文も完成することができました。

ということで次回は、スロットコンテンツというものについて説明します。

最後まで読んでいただきありがとうございました。

まとめ

  • $emitというインスタンスメソッドを使うと子から親へイベントを渡すことができる
  • $emitは1世代だけでなく何世代にも渡ってイベントを渡すことができる

コメント

タイトルとURLをコピーしました