Vue.jsで再帰的なコンポーネントを作ってみた


Vueのコンポーネントを利用して、SNSでよくあるような、コメントに返信がぶら下がり、
さらにその返信への返信がぶら下がり...といったような一覧を作ってみました。

以下のようなイメージです。

環境

今回はNuxt.jsを使っています。

  • Node.js => v10.8.0
  • nuxt-edge => v2.0.0-25599455.3a0f094 (すごく中途半端なバージョンですみません)

構成

root
  ├ pages
  │   └ comments.vue
  ├ components
  │   └ MyComment.vue
  │ ...

pages/comments.vue

まずコンポーネントを呼び出す側のページです。
<my-comment>が再帰的なコメントコンポーネントになります。

pages/comments.vue

<template>
<section>
  <my-comment
    :comment="comment"
  />
</section>
</template>

<script>
import MyComment from '~/components/MyComment'

export default {
  components: {
    MyComment
  },
  data() {
    return {
      comment: {
        id: 0,
        content: '親コメント',
        replies: [
          {
            id: 1,
            content: '子コメント1',
            replies: []
          },
          {
            content: '子コメント2',
            replies: [
              {
                id: 2,
                content: '孫コメント1',
                replies: [
                  {
                    id: 4,
                    content: '曾孫コメント1',
                    replies: []
                  },
                  {
                    id: 5,
                    content: '曾孫コメント2',
                    replies: []
                  }
                ]
              },
              {
                id: 3,
                content: '孫コメント2',
                replies: []
              }
            ]
          }
        ]
      }
    }
  }
}
</script>

<style>
section {
  padding: 1.5rem;
}
</style>

ポイント

コンポーネントに渡すデータを再帰的な構造で定義する

commentオブジェクトの最小単位は以下のような形になります。

commentオブジェクト

{
  id: 'uniqueId',
  content: 'コメント文字列',
  replies: []
}

repliesの中に、commentオブジェクトを入れ子にしていきます。
commentオブジェクトが長さ1以上のrepliesをもつ限り、MyCommentが再帰的に呼び出され続けます。

idは、v-forを使って繰り返し描画する際のkeyとしてユニークな値が必要なため、付与しています。

components/MyComment.vue

こちらが再帰的に呼び出されるコンポーネントです。

components/MyComment.vue
<template>
<div>
  <p>{{ comment.content }}</p>
  <ul v-if="comment.replies.length > 0">
    <li
      class="reply"
      v-for="reply in comment.replies"
      :key="reply.id"
      >
      <my-comment
        :comment="reply"
      />
    </li>
  </ul>
</div>
</template>

<script>
import MyComment from '~/components/MyComment'

export default {
  name: 'my-comment', // or myComment / MyComment
  components: {
    MyComment
  },
  props: {
    comment: {
      type: Object,
      required: true
    }
  }
}
</script>

<style>
@charset 'utf8';

.reply {
  display: block;
  margin-left: 2rem;
  margin-top: 1rem;
}
</style>

ポイント

コンポーネントにnameオプションを定義する

引用: API — Vue.js オプション / その他 #name

テンプレート内でのコンポーネント自身の再帰呼び出しを許可します。コンポーネントは Vue.component() でグローバルに登録され、グローバル ID はその名前に自動的に設定される事に注意してください。

とあります。
nameに指定した名前でコンポーネントが登録されるようです。
名前は、ケバブケースキャメルケースアッパーキャメルケースどれでもいいようです。
コンポーネントはグローバルに登録されるという点だけ、念の為心に留めておいたほうがよさそうです。

自分自身をimportしてcomponentsに登録する

自分で自分を再帰的に呼び出します。

MyCommentコンポーネントの propscommentにはreplyを渡す

<li v-for="reply in comment.replies">

v-forで回しているreplyには、上のcommentオブジェクトと同じ形のデータが入っているので、これを渡してあげます。

入れ子のようなスタイル

li要素の上、左にマージンをとることで、上の要素との余白を確保しつつ、
入れ子になった際にどんどん右にインデントされていくスタイルを実現できます。

<style>
@charset 'utf8';

.reply {
  display: block;
  margin-left: 2rem;
  margin-top: 1rem;
}
</style>

感想

仕事で、画像のようなコメントのUIを作るときに
nameオプションを使ってコンポーネントを再帰的に呼び出せることを知ったので、
備忘録として残しておこうと思いました。

もし、この記事が、同じようなことをしようとしている方のお役に立てたなら幸いです。