Nuxt.js で Vue コンポーネントの利用とコンポーネントの相互呼出しをする

Nuxt.js を使うことで JAMstack なアプリを高速に構築できます。それに加えて Vue.js のフレームワークであるため Vue コンポーネントが使えるというメリットがあります。UI のパーツをコンポーネントとして切り出すことで実装をシンプルにし、また再利用性を高めることができます。

これまで、Nuxt.jsPWAwithTypeScriptとして、ベースとなるアプリを作ってきました。このままページを追加していくことで簡単にアプリ画面を増やしていけますが、その前に Nuxt.js (というか Vue.js 系フレームワーク) の、強力な機能である「Vue コンポーネント」を使い実装をシンプルにしたいと思います。

説明の都合により下記シリーズのソースを使いますが一般的な Vue コンポーネントの使い方ですので、下記 “開始時のアプリソース” ではなくても大丈夫です。

シリーズの記事

環境
本記事の開発環境は以下となります。

絵文字ボタンを Vue コンポーネントにする

以下はページ(/pages/index.vue) へ追加した絵文字ボタンに関連するコードです。絵文字に応じたtadaなどの文字列が異なるだけで、ほぼ同じコードの繰り返しとなっています。

/pages/index.vue(該当箇所のみ抜粋)

1
<div class="actions">
2
  <a @click="addTada" class="button--action">🎉 {{ tada }}</a>
3
  <a @click="addSparkles" class="button--action">✨ {{ sparkles }}</a>
4
  <a @click="addThumbsup" class="button--action">👍 {{ thumbsup }}</a>
5
  <a @click="addHeart" class="button--action">🧡 {{ heart }}</a>
6
</div>
7
8
<script lang="ts">
9
export default Vue.extend({
10
  data() {
11
    return {
12
      tada: 0,
13
      sparkles: 0,
14
      thumbsup: 0,
15
      heart: 0
16
    }
17
  },
18
  methods: {
19
    addTada: function(): void {
20
      this.tada++
21
    },
22
    addSparkles: function(): void {
23
      this.sparkles++
24
    },
25
    addThumbsup: function(): void {
26
      this.thumbsup++
27
    },
28
    addHeart: function(): void {
29
      this.heart++
30
    }
31
  }
32
})
33
</script>

これを Vue コンポーネント化して、再利用可能な絵文字ボタン・コンポーネントにします。
まず、ボタン1つとして表現される/components/EmojiButton.vueを作ります。ボタン1つ分なので<a>タグが1つ、汎用的なメソッド名、変数名にします。絵文字は、親コンポーネントから渡された絵文字を使いたいのでprops: { emoji: String }として受け取るようにします。

/components/EmojiButton.vue(コード全文)

1
<template>
2
  <a @click="add" class="button--action">{{ emoji }} {{ counter }}</a>
3
</template>
4
5
6
<script lang="ts">
7
import Vue from 'vue'
8
9
export default Vue.extend({
10
  props: {
11
    emoji: String
12
  },
13
  data() {
14
    return {
15
      counter: 0
16
    }
17
  },
18
  methods: {
19
    add: function(): void {
20
      this.counter++
21
    }
22
  }
23
})
24
</script>
25
26
27
<style scoped>
28
.button--action {
29
  display: inline-block;
30
  border-radius: 4px;
31
  border: 1px solid #3b70d0;
32
  color: #3b70d0;
33
  text-decoration: none;
34
  margin: 0 2px;
35
  padding: 10px 10px;
36
  cursor: pointer;
37
}
38
39
.button--action:hover {
40
  color: #fff;
41
  background-color: #3b70d0;
42
}
43
</style>

絵文字ボタンを利用するページ側(/pages/index.vue) では、ハードコードしていた絵文字ボタンをコンポーネントの利用に変更します。コンポーネントのタグは<emoji-button>で、data() { emojis }の配列を使った v-forの繰り返しで作ります。また古いハードコードしていた絵文字ボタンのコードは削除します。

/pages/index.vue(該当箇所のみ抜粋)

1
<template>
2
  <section class="container">
3
    <div>
4
      <div class="actions">
5
        <emoji-button v-for="e in emojis" :key="e" :emoji="e" />
6
        <a @click="clear" class="button--grey">Clear</a>
7
      </div>
8
    </div>
9
  </section>
10
</template>
11
12
13
<script lang="ts">
14
import Vue from 'vue'
15
import EmojiButton from '~/components/EmojiButton.vue'
16
import Logo from '~/components/Logo.vue'
17
18
export default Vue.extend({
19
  components: {
20
    Logo,
21
    EmojiButton
22
  },
23
  data() {
24
    return {
25
      emojis: [ '🎉', '✨', '👍', '🧡' ]
26
    }
27
  },
28
  computed: {
29
    counter(): number {
30
      return 0
31
    }
32
  },
33
  methods: {
34
    clear: function(): void {
35
    }
36
  }
37
})
38
</script>

これにより絵文字ボタンがコンポーネント化され、再利用可能なボタンとなりました。
それぞれのボタンに絵文字が表示され、クリックするとクリックされたボタンのカウンターが増えます。似たようなコードがなくなりスッキリしました。

子コンポーネントから、親コンポーネントにイベントを送る

絵文字ボタンのコンポーネントはできましたが、トータルのカウンターは機能しなくなりました。絵文字ボタン・コンポーネントのカウンターの合計だけロゴ(/components/Logo.vue) コンポーネントを回転する機能がありましたが、現在は回転しません。

これまではページ側(/pages/index.vue) に、すべての情報があったのでボタン・クリック数の合計をロゴ・コンポーネントに送れました。しかし各カウンターが絵文字ボタン・コンポーネントへ移動してしまったため、カウンターの合計がページ側では作れなくなりました。

そこで、絵文字ボタン・コンポーネント(子コンポーネント)はクリックされたことを、ページ(親コンポーネント)へ伝えるようにします。ページ側では、その各絵文字ボタン・コンポーネントから伝えられたクリックの回数をカウントして合計としてロゴ・コンポーネントへ送るようにします。

そのクリックされたことを伝えるために「イベント」を使います。絵文字ボタン・コンポーネント(子コンポーネント)から、ページ(親コンポーネント)へ「イベント通知」をすることで、クリックされたことを伝えます。

子コンポーネント「絵文字ボタン・コンポーネント」を修正

絵文字ボタン・コンポーネントのカウントアップ処理にイベント通知this.$emit('countup')を追加します。

/components/EmojiButton.vue(該当箇所のみ抜粋)

1
<script lang="ts">
2
import Vue from 'vue'
3
4
export default Vue.extend({
5
  methods: {
6
    add: function(): void {
7
      this.counter++
8
      this.$emit('countup')
9
    }
10
  }
11
})
12
</script>

$emitは、Vue コンポーネント間でイベントを通知するための仕組みです。今回は絵文字ボタン・コンポーネントが子コンポーネントなので、親コンポーネントであるページへイベントを通知します。

イベント通知は$emit('イベント名', [...引数])の書式です。引数は可変長配列で、イベントの通知先へ渡されます。今回はcountupという名前のイベント名で引数無しの通知を行っています。

参考情報

親コンポーネント「ページ」を修正

親コンポーネントであるページ側で、子コンポーネントである絵文字ボタン・コンポーネントからのイベント通知を受け取ります。

/pages/index.vue(該当箇所のみ抜粋)

1
<template>
2
  <section class="container">
3
    <div>
4
      <div class="actions">
5
        <emoji-button v-for="e in emojis" :key="e" :emoji="e" v-on:countup="add" />
6
        <a @click="clear" class="button--grey">Clear</a>
7
      </div>
8
    </div>
9
  </section>
10
</template>
11
12
13
<script lang="ts">
14
export default Vue.extend({
15
  data() {
16
    return {
17
      total: 0,
18
      emojis: [ '🎉', '✨', '👍', '🧡' ]
19
    }
20
  },
21
  computed: {
22
    counter(): number {
23
      return this.total
24
    }
25
  },
26
  methods: {
27
    add: function(): void {
28
      this.total++
29
    },
30
  }
31
})
32
</script>

<emoji-button>タグにv-on:countup="add"属性を追加し、絵文字ボタン・コンポーネントのcountupイベントを受け取ります。属性値addは、メソッド呼出しでadd()メソッドを呼び出すという定義です。これにより子コンポーネントのイベントを受け取って、親コンポーネントで処理をするという形が作れます。

上記定義で呼び出されるメソッドを<script>に追加します。これは通常のメソッド定義と同様です。今回はdata() { total }をインクリメントするメソッドとします。

これにより各絵文字ボタン・コンポーネントがクリックされると、ページ側にcountupイベントが伝わります。そのイベントの回数をtotalにインクリメントしていくことで、全絵文字ボタン・コンポーネントのカウンター合計が作れます。

そしてtotalcomputed: { counter() }につなぐことで、これまで機能していたロゴの回転が復旧します。

参考情報

親コンポーネントから、子コンポーネントにイベントを送る

最後に [Clear] ボタンを復旧します。[Clear] ボタンは絵文字ボタンと異なり1つだけですし、すべての絵文字ボタン・コンポーネントのカウンターをリセットする特別な役割があります。そのため親コンポーネントに残しました。

[Clear] ボタンの機能であるリセットですが、親コンポーネント内のtotal0にリセットすることはできます。それによりロゴの回転を止めて、カウンターのリセットもできます。

しかしながら子コンポーネントのカウンターは直接リセットできません。こちらもイベント通知を使いリセットします。先ほどと逆なり、親コンポーネントから子コンポーネントへイベントを通知します。

親コンポーネント「ページ」を修正

ページ側はイベントを通知する先の子コンポーネントである、全絵文字ボタン・コンポーネントを特定してイベント通知します。今回は絵文字ボタン・コンポーネントしかないので(正確には、ロゴ・コンポーネントも子コンポーネントですが)、送り先を意識しにくいですが多数のコンポーネントを抱えている親コンポーネントであるからこそ、どのコンポーネントへイベント通知するか考える必要があります。

/pages/index.vue(該当箇所のみ抜粋)

1
<template>
2
  <section class="container">
3
    <div>
4
      <div class="actions">
5
        <emoji-button v-for="e in emojis" :key="e" :emoji="e" v-on:countup="add" ref="EmojiButtons" />
6
        <a @click="clear" class="button--grey">Clear</a>
7
      </div>
8
    </div>
9
  </section>
10
</template>
11
12
13
<script lang="ts">
14
export default Vue.extend({
15
  methods: {
16
    clear: function(): void {
17
      this.total = 0;
18
      (this.$refs.EmojiButtons as Vue[]).forEach((value: Vue) => value.$emit('clear'))
19
    }
20
  }
21
})
22
</script>

<emoji-button>タグのref="EmojiButtons"属性は、EmojiButtonsの文字列でタグを参照されるための設定です。ref属性は通常の HTML タグと、子の Vue コンポーネント・タグのどちらにも使用できます。

参照する側は$refs.[ref 属性の値]で行います。今回はEmojiButtonsで登録したので$refs.EmojiButtonsでアクセスします。clear()メソッドではEmojiButtonsで登録された全絵文字ボタン・コンポーネントを取得し、$emit('clear')clearというイベント名で引数無しの通知を送っています。$emit()の使い方は、子コンポーネントからイベントを送った時と同じです。

参考情報

子コンポーネント「絵文字ボタン・コンポーネント」を修正

絵文字ボタン・コンポーネントで clearイベントの通知を受けます。

/components/EmojiButton.vue(該当箇所のみ抜粋)

1
<script lang="ts">
2
import Vue from 'vue'
3
4
export default Vue.extend({
5
  created: function() {
6
    this.$on('clear', () => {
7
      this.counter = 0;
8
    })
9
  },
10
})
11
</script>

Vue コンポーネントのライフサイクルcreatedで、$onを使い、イベント受け取りの登録をしておきます。今回は、イベント名clearで引数無しです。イベントを受けたらコンポーネント内のcounterをリセットします。これにより、各絵文字ボタン・コンポーネントのカウンターがクリアできます。

参考情報

ソースコード

今回作成した部分までのソースを GitHub へアップしました。(Tag:0.0.5)

GitHub Pages にホスティングもしました。動作している状態を確認する場合は、こちらへアクセスしてください。
(公開サイトは1つのため記事公開に合わせて変わり、本記事の内容とは異なることがあります)


似たようなコードをコンポーネント化し再利用できるようにしました。Vue コンポーネントを使うことで、画面内のコードもスッキリさせることができます。(同じようなものは集約してプログラムで処理したいところですよね、0.0.4 までのソースはサンプル用とはいえ心苦しかった)

一方でコンポーネント間の連携は意外と難しいところがあります。まずは親子間のイベント通知が基本です。

今回のサンプルは、スタータープロジェクトにボタンを付けたり、ロゴを回したりと地味な感じではありますが、2種類の子コンポーネント間を親コンポーネントがつなぐ形となっています。まさにコンポーネント間連携の基本なので、実装を確認いただければと思います。

[Clear] ボタンが子コンポーネントになると、兄弟間の連携となり、また一歩踏み込むことになりますが次の機会に!