Рекурсивный компонент в Vue.js является один , который вызывает себя , например:
Vue.component('recursive-component', {
template: `<!--Invoking myself!-->
<recursive-component></recursive-component>`
});
Рекурсивные компоненты полезны для отображения комментариев в блоге, вложенных меню или в целом, где родитель и потомок одинаковы, хотя и с разным содержанием. Например:
Чтобы продемонстрировать, как эффективно использовать рекурсивные компоненты, я расскажу о шагах по созданию расширяемого / сжимаемого древовидного меню.
Структура данных
Дерево рекурсивных компонентов пользовательского интерфейса будет визуальным представлением некоторой рекурсивной структуры данных. В этом уроке мы будем использовать древовидную структуру, где каждый узел является объектом с:
label
Собственность.- Если у него есть дочерние элементы,
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>
Вот как это выглядит до сих пор:
вдавливание
Было бы неплохо визуально определить «глубину» дочернего компонента, чтобы пользователь получил представление о структуре данных из пользовательского интерфейса. Давайте все больше отступать от каждого уровня детей для достижения этой цели.
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
thedepth
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.