Recursive components in Vue 3

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 setup using Vite. 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 at 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.

Lastly we’ve added a div that shows a text that the selected element has no children. This is just for visual confirmation and testing.

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(0, 0, 0, 0.05);
}

.character-list li.selected {
background-color: rgba(0, 0, 0, 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 HelloWorld 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 './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>
// Code we've 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>
<h1>Hello recursive components</h1>

<div id="container">
<recursive-component
:characters="characters"
:selected="selected"
:level="0"
@selected="select"
/>
</div>
</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 scoped>
#container {
border: 2px solid #41b883;
display: flex;
margin: 0 auto;
max-width: 800px;
}
</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 its 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 :D

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.

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

--

--

--

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

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Angular Template Outlets as a Workaround for Component Placement Restrictions

You don’t always need to not reinvent the wheel

Your First Elm Application

Technical Breakdown of expriment #304

i18n Phone Number Validation in Angular

Angular P1

Commit conventions

Adding Audio To My JavaScript SPA

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Michael Verschoof

Michael Verschoof

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

More from Medium

How to validate a custom form component in Vue 3?

Vue 3 Programmatic Component Design

Vue 3 — Google reCaptcha implemented in just 2 minutes.

Good bye Nuxt 2, hello Nuxt 3