LoginSignup
6
5

More than 5 years have passed since last update.

Vue.js で Recursive にコンポーネントを呼び出して、階層が深いメニュー的なものを作る

Last updated at Posted at 2018-10-18

Screen Shot 2018-10-18 at 2.23.16 PM.png

完成形
https://codesandbox.io/s/j295rk76j9

以下のような入れ子になったオブジェクトを展開して、上記画像のように、タイトルをクリックすると入れ子の中身が展開されるコンポーネントを作る。

まとめ

  • 配列の数だけコンポーネントを生成する時には v-for を使う
  • 表示非表示をするなら(正確にはコンポーネントの存在をコントロールする)、v-if を使う
  • データ構造をきっちり入れ子にして、コンポーネントも自分自身を再帰的に呼び出すようにすれば、無限に入れ子になる
data: () => ({
    tree: {
      contents: { label: '1' },
      nodes: [
        {
          contents: { label: '2.1' },
          nodes: [
            {
              contents: { label: '3.1' },
              nodes: [
                { contents: { label: '4.1' } },
                { contents: { label: '4.2' } }
              ]
            },
            { contents: { label: '3.2' } }
          ]
        },
        {
          contents: { label: '2.2' }
        }
      ]
    }
  })

完成系のソース

App.vue
// https://codepen.io/anthonygore/pen/PJKNqa を参考に作成

<template>
  <div id="app">
    <Recursive :node="tree" :depth="1"></Recursive>
  </div>
</template>

<script>
import Recursive from './components/Recursive'

export default {
  name: 'App',
  components: {
    Recursive
  },
  data: () => ({
    tree: {
      contents: { label: '1' },
      nodes: [
        {
          contents: { label: '2.1' },
          nodes: [
            {
              contents: { label: '3.1' },
              nodes: [
                { contents: { label: '4.1' } },
                { contents: { label: '4.2' } }
              ]
            },
            { contents: { label: '3.2' } }
          ]
        },
        {
          contents: { label: '2.2' }
        }
      ]
    }
  })
}
</script>

<style>
</style>

これがきもになる再帰的に自分を呼び出すコンポーネント

components/Recursive.vue
<template>
  <div>
    <h2 @click="showChildren = !showChildren">{{open}} {{node.contents.label}}</h2>
    <recursive
      v-if="showChildren"
      :key="index"
      v-for="(node, index) in node.nodes" 
      :node="node"
      :style="indent"
      :depth="depth + 1"
    ></recursive>
  </div>

</template>

<script>
import Recursive from '/src/components/Recursive'

export default {
  data: () => ({
    showChildren: false
  }),
  name: 'Recursive',
  props: ['node', 'depth'],
  components: { Recursive },
  computed: {
    indent() {
      return { transform: `translate(${this.depth * 50}px)` }
    },
    open() {
      if(!this.node.nodes) {
        return ''
      }

      if(this.node.nodes && !this.showChildren) {
        return '+'
      }

      return '-'
    }
  }
}
</script>

<style scoped>
</style>

ポイント

データ構造

以下のオブジェクトが最小単位となる。

{
  contents: {},
  nodes: []
}

さらに上記の nodes の配列に、最小単位のオブジェクトが、入れ子になって入っている。

{
  contents: {},
  nodes: [
    {
      contents: {},
      nodes: []
    },
    {
      contents: {},
      nodes: []
    }
  ]
}

そして、最小単位のオブジェクトが無限に入れ子になっていくことが可能。
入れ子にならない最後のオブジェクトは以下のようにコンテンツだけを持ち、nodes を持たなければいい。

{
  contents: {},
}

自分自身を呼び出すコンポーネント

Recursive.vue コンポーネントは、入れ子構造を作るために自分自身を呼び出す。

その際にどんなデータで呼び出すか、ということは、props としてうけとった node の中の nodes プロパティを参照する。
v-for="(node, index) in node.nodes"
結果として node.nodes にある配列の分だけ Recursive コンポーネントが呼び出されて並列に並べられる。

各 Recursive コンポーネントには、node がまた渡される。

こうして入れ子構造が生成される。

components/Recursive.vue
<template>
  <div>
    <h2 @click="showChildren = !showChildren">{{open}} {{node.contents.label}}</h2>
    <recursive
      v-if="showChildren"
      :key="index"
      v-for="(node, index) in node.nodes" 
      :node="node"
      :style="indent"
      :depth="depth + 1"
    ></recursive>
  </div>

</template>

<script>
import Recursive from '/src/components/Recursive'

export default {
  data: () => ({
    showChildren: false
  }),
  name: 'Recursive',
  props: ['node', 'depth'],
  components: { Recursive },
  computed: {
    indent() {
      return { transform: `translate(${this.depth * 50}px)` }
    },
    open() {
      if(!this.node.nodes) {
        return ''
      }

      if(this.node.nodes && !this.showChildren) {
        return '+'
      }

      return '-'
    }
  }
}
</script>

<style scoped>
</style>

クリックした時の表示非表示の挙動

各コンポーネントは、子要素を表示するかしないかを制御するための値、showChildren を持つ。
@click された時に、この値を反転させる。

v-if でこの値を参照しているので、true の時のみ、子コンポーネントが生成される。

開く前は +, 開いたら -, 子コンポーネントがない時はそもそも何もマークがつかないの切り替え

computed の open というメソッドが担当している。

open() {
      // 子node がそもそもなければ何も表示しない
      if(!this.node.nodes) {
        return ''
      }

      // 子node があり、show ではない時は開けるので + を表示する
      if(this.node.nodes && !this.showChildren) {
        return '+'
      }

      // それ以外は - 閉じるボタンを表示する
      return '-'
    }

インデントを下げたようなスタイリング

重要部分だけを以下にぬき出す。

ポイントは、:style に対して値を渡すことでスタイリングを行う。
その値はメソッドで定義する。

props として受け取った depth の値に基づいて、右にずらしている。
depth は、各コンポーネントから入れ子にする際に、1 深くしてから props として渡している。

<template>
  <div>
    <recursive
      :style="indent"
      :depth="depth + 1"
    ></recursive>
  </div>
</template>

<script>
import Recursive from '/src/components/Recursive'

export default {
  props: ['depth'],
  computed: {
    indent() {
      return { transform: `translate(${this.depth * 50}px)` }
    },
}
</script>
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5