Статьи

Создайте меню разборного дерева с помощью рекурсивных компонентов Vue.js

Рекурсивный компонент в Vue.js является один , который вызывает себя , например:

Vue.component('recursive-component', {
  template: `<!--Invoking myself!-->
             <recursive-component></recursive-component>`
});

Рекурсивные компоненты полезны для отображения комментариев в блоге, вложенных меню или в целом, где родитель и потомок одинаковы, хотя и с разным содержанием. Например:

recursive_components_01.png

Чтобы продемонстрировать, как эффективно использовать рекурсивные компоненты, я расскажу о шагах по созданию расширяемого / сжимаемого древовидного меню.

Структура данных

Дерево рекурсивных компонентов пользовательского интерфейса будет визуальным представлением некоторой рекурсивной структуры данных. В этом уроке мы будем использовать древовидную структуру, где каждый узел является объектом с:

  1. labelСобственность.
  2. Если у него есть дочерние элементы, nodesсвойство, представляющее собой массив из одного или нескольких узлов.

Как и все древовидные структуры, он должен иметь один корневой узел, но может быть бесконечно глубоким.

let tree = {
  label: 'root',
  nodes: [
    {
      label: 'item1',
      nodes: [
        {
          label: 'item1.1'
        },
        {
          label: 'item1.2',
          nodes: [
            {
              label: 'item1.2.1'
            }
          ]
        }
      ]
    }, 
    {
      label: 'item2'  
    }
  ]
}

Рекурсивный компонент

Давайте создадим рекурсивный компонент для отображения нашей структуры данных с именем TreeMenu. Все, что он делает, это отображает метку текущего узла и вызывает себя для отображения любых дочерних элементов.

TreeMenu.vue

<template>
  <div class="tree-menu">
    <div>{{ label }}</div>
    <tree-menu 
      v-for="node in nodes" 
      :nodes="node.nodes" 
      :label="node.label"
    >
    </tree-menu>
  </div>
</template>
<script>
  export default { 
    props: [ 'label', 'nodes' ],
    name: 'tree-menu'
  }
</script>

Если вы используете компонент рекурсивно, вы должны либо зарегистрировать его глобально Vue.component, либо присвоить ему nameсвойство. В противном случае любые дочерние элементы компонента не смогут разрешить дальнейшие вызовы, и вы получите неопределенную ошибку компонента.

Базовый вариант

Как и для любой рекурсивной функции, для завершения рекурсии необходим базовый случай , в противном случае рендеринг будет продолжаться бесконечно, и вы получите переполнение стека.

В нашем древовидном меню мы хотим остановить рекурсию всякий раз, когда достигаем узла, у которого нет дочерних элементов. Вы могли бы сделать это с помощью a v-if, но наши v-forнеявно сделают это за нас; если nodesмассив не определен, дальнейшие tree-menuкомпоненты не будут вызваны.

<template>
  <div class="tree-menu">
    ...
    <!--If `nodes` is undefined this will not render-->
    <tree-menu v-for="node in nodes"></tree-menu>
</template>

использование

Как мы теперь используем этот компонент? Для начала мы объявляем экземпляр Vue, который имеет структуру данных как dataсвойство и регистрирует TreeMenuкомпонент.

app.js

import TreeMenu from './TreeMenu.vue'

let tree = {
  ...
}

new Vue({
  el: '#app',
  data: {
    tree
  },
  components: {
    TreeMenu
  }
})

Помните, что наша структура данных имеет один корневой узел. Чтобы начать рекурсию, мы вызываем TreeMenuкомпонент в нашем основном шаблоне, используя корневые nodesсвойства для реквизита:

index.html

<div id="app">
  <tree-menu :label="tree.label" :nodes="tree.nodes"></tree-menu>
</div>

Вот как это выглядит до сих пор:

recursive_components_02.png

вдавливание

Было бы неплохо визуально определить «глубину» дочернего компонента, чтобы пользователь получил представление о структуре данных из пользовательского интерфейса. Давайте все больше отступать от каждого уровня детей для достижения этой цели.

recursive_components_03.png

This is implemented by adding a depth prop to TreeMenu. We’ll use this value to dynamically bind inline style with a transform: translate CSS rule to each node’s label, thus creating the indentation.

<template>
  <div class="tree-menu">
    <div :style="indent">{{ label }}</div>
    <tree-menu 
      v-for="node in nodes" 
      :nodes="node.nodes" 
      :label="node.label"
      :depth="depth + 1"
    >
    </tree-menu>
  </div>
</template>
<script>
  export default { 
    props: [ 'label', 'nodes', 'depth' ],
    name: 'tree-menu',
    computed: {
      indent() {
        return { transform: `translate(${this.depth * 50}px)` }
      }
    }
  }
</script>

The depth prop will start at zero in the main template. In the component template above you can see that this value will be incremented each time it is passed to any child nodes.

<div id="app">
  <tree-menu 
    :label="tree.label" 
    :nodes="tree.nodes"
    :depth="0"
  ></tree-menu>
</div>

Remember to v-bind the depth value to ensure it’s a JavaScript number rather than a string.

Expansion/Contraction

Since recursive data structures can be large, a good UI trick for displaying them is to hide all but the root node so the user can expand/contract nodes as needed.

To do this, we’ll add a local state property showChildren. If false, child nodes will not be rendered. This value should be toggled by clicking the node, so we’ll need a click event listener method toggleChildren to manage this.

<template>
  <div class="tree-menu">
    <div :style="indent" @click="toggleChildren">{{ label }}</div>
    <tree-menu 
      v-if="showChildren"
      v-for="node in nodes" 
      :nodes="node.nodes" 
      :label="node.label"
      :depth="depth + 1"
    >
    </tree-menu>
  </div>
</template>
<script>
  export default { 
    props: [ 'label', 'nodes', 'depth' ],
    data() {
      return { showChildren: false }
    },
    name: 'tree-menu',
    computed: {
      indent() {
        return { transform: `translate(${this.depth * 50}px)` }
      }
    },
    methods: {
      toggleChildren() {
        this.showChildren = !this.showChildren;
      }
    }
  }
</script>

Wrap Up

With that, we’ve got a working tree menu. As a nice finishing touch, you can add a plus/minus icon to make the UI even more obvious. I did this with Font Awesome and a computed property based on showChildren.

Inspect the CodePen to see how I implemented it.

recursive_components_04.png