Recursive components in Vue 3

Michael Verschoof
6 min readJan 28, 2022
Photo by Julia Kadel on Unsplash

A while ago I was working on a project where I needed to display a nested directory structure of files and folders. Often these are displayed in lists with sub-lists in the list-items, such as the below screenshot.

Common view of nested files and folders

But I wanted to do it differently…

As this was for an application that was used on desktops, we had a wide view in which to display these items. That triggered me to try something different and show these side-by-side as columns.

After some messing around, I realized that Vue has the possibility to have a component call itself. This allows us to create the entire file structure, no matter how deep, with only one component!

The setup

You probably have your own project that you’re currently working on, but for this example I’m using a basic Vue 3 project using my default settings from create Vue app. I will try to keep this example as simple as possible and use the least amount of different files so we can keep an overview on the actual changes.

Creating the component

Alright, let’s get started on the actual fun part. We’ll create a new Vue component called RecursiveComponent.vue in the components directory. This is the same directory as HelloWorld.vue, but feel free to place and name it however you want.

Adding the template

We’ll start with the template part. I’ll describe what is happening soon so don’t worry if something is confusing 😄

<template>
<ul class="character-list">
<template v-for="character of characters" :key="character.name">
<li :class="{ selected: isSelected(character) }"
@click="select(character, level)"
>
<div class="name">{{ character.name }}</div>

<template v-if="isSelected(character)">
<teleport to="#container">
<recursive-component
v-if="character.children"
:characters="character.children"
:selected="selected"
:level="level + 1"
@selected="select"
/>
</teleport>
</template>
</li>
</template>
</ul>
</template>

As you can see the template starts with an <ul> tag as its starting point. Basically every level of items will consist of this tag and its direct items are <li> tags. These contain a div that displays the item’s name.

Next is a <template> tag where a check is performed on if the item has any children. If it does, a new <recursive-component>is added. This is the main part of the magic as this is the component we’re already working on!

You’ve probably also noted the <teleport> tag that is right above it. This is a bit of Vue magic that enables us to move the elements inside it to another place in the DOM, while maintaining the same parent-child connections from a logic standpoint. Effectively we’re using the to="" attribute to tell the teleport where to move the elements inside it to. In our case this will be the element loading the component initially, but we’ll see that later on.

Adding the functionality

Next we’ll add the actual functionality to make the component work. Let’s add the following script tag to the component.

<script setup lang="ts">
const props = defineProps({
characters: {
type: Array,
required: true
},
selected: {
type: Array,
required: false,
default: []
},
level: {
type: Number,
required: false,
default: 0
}
});

const emit = defineEmits(['selected']);

const select = (character, level) => {
emit('selected', character, level);
};

const isSelected = (character) => {
if (!props.selected
|| !props.selected.length
|| !props.selected.some((char) => char.name === character.name))
{
return false;
}

return props.selected[props.level]?.name === character.name;
};
</script>

This does a few things. It first declares the properties that the component uses from its parent. These consist of an array of characters to display, an array of characters that are currently selected and the current level of nesting that we are in.

After this an emit is declared which alerts its parent if an element in this level is selected. This is followed by a function that calls the emit function once an element is selected. This emits the selected character and the level it’s on.

Lastly, a function is added on which to determine if an item is selected or not. This is used in the template to add the selected class to the list-item so we can highlight the item.

It is also used to determine if we show any children it may have. This shows another recursive component (itself) if the selected character has children. If so, another instance of this component is created where the children of the current component are passed to and the value of the level, plus one, is added.

Adding some styling

In order to make our list look somewhat decent, we’re adding some styling. This is completely optional though, but it makes it look a little cleaner 😉

<style scoped>
.character-list {
border-right: 1px solid #41b883;
min-height: 25vh;
width: 200px;
margin: 0;
padding: 0;
}

.character-list li {
padding: 0.5em 1em;
}

.character-list li:hover {
cursor: pointer;
}

.character-list li:not(.selected):hover {
background-color: rgba(65, 184, 131, 0.05);
}

.character-list li.selected {
background-color: rgba(65, 184, 131, 0.1);
}
</style>

Actually using the component

Now that we have our component completed, we can add it to our page. I’ve chosen to use the HomeView.vue component as the parent. Basically I’ve emptied the file and we’ll add the new parts in the following steps.

The script

This time we’ll start with the script. We need a few things to get it to work. We need a variable that keeps an array of strings for the items that are selected so we can pass that to the component for styling.

We’ll also add a function that is called when an item is selected. This modifies the list of selected items. It also removes all items after the just selected item so that the list is clean and correct.

And, at the top, we’ve imported our recursive component in order to be able to use it 😄

<script setup>
import RecursiveComponent from '@/components/RecursiveComponent.vue';
import { ref } from 'vue';

const selected = ref([]);

const select = (character, level) => {
selected.value[level] = character;
selected.value.splice(level + 1);
};
</script>

The demo data

In order to have data to display with several levels, we’ll use a short list of LotR characters as mock data. In the script tag that we’ve just created we’ll add the following:

<script setup>
// JavaScript we just added

const characters = [
{ name: 'Gandalf' },
{
name: 'Thranduil',
children: [{ name: 'Legolas' }]
},
{
name: 'Groin son of Farin',
children: [
{
name: 'Gloin son of Groin',
children: [{ name: 'Gimli son of Gloin' }]
}
]
}
];
</script>

The template

Next up is the template. This will actually call the newly created component and pass the variables and such to the component.

<template>
<main>
<h1>Hello recursive components</h1>

<div id="container">
<recursive-component
:characters="characters"
:selected="selected"
:level="0"
@selected="select"
/>
</div>
</main>
</template>

Note that an element with the ID “container” is added here. This is the target of the <teleport> we’ve used in our recursive component.

Some styling

Let’s add a tad of styling so we can see our component a bit better. It has a 2px border in Vue-green so we can see that everything inside it is our recursive component.

<style>
#container {
border: 2px solid #41b883;
display: flex;
margin: 0 auto;
max-width: 800px;
}

ul,
li {
list-style-type: none;
}
</style>

The result

Now we’ve added a bunch of stuff it would be nice to see the result of our work. We can start the development server (if you haven’t already) by going to the main directory of this project using a terminal and running npm run dev. This will start up the server which is available on http://localhost:3000/ by default.

When going to that url, we’ll see a basic screen with our container and the green border. Now we can click on items to select them which “automagically” shows their children, if they have any.

Look at the cool selected characters and their children!

Conclusion

That’s it! We’ve created a component that is able to call itself and, with a bit of styling, displays everything neatly side by side 😁

Getting the code

You can get the result of this tutorial at my GitHub. This contains the exact same files and code as used here.

--

--

Michael Verschoof

Ex-Java developer turned Javascript / Typescript and Vue developer at BTC Direct Europe B.V.