DEV Community

Cover image for VueJS - Recursive Components
Pablo Veiga
Pablo Veiga

Posted on • Updated on

VueJS - Recursive Components

During your web developer career you will probably come across certain scenarios where recursion is needed. And there's more, you might even need to implement it in one of your VueJS components. But don't push yourself too hard! Being capable of identifying when recursion must (or not) be used is not that simple and it's an ability that comes with time and experience. I've also struggled to understand how recursion works. Keep cool and one day a light will be shed on it and you'll fully understand everything like magic.

TL;DR

In this article we define "recursion" and use this approach to create recursive VueJS components, which means that they use themselves within their own implementation.


Concept

Before we start to see Recursive Components, let's move a step backwards and talk about recursion as a programming resource. The term recursion has several definitions according to its context but they all mean basically the same.

The Cambridge Dictionary says that recursion is:

the practice of describing numbers, expressions, etc. in terms of the numbers, expressions, etc. that come before them in a series (within Mathematics and Computing contexts).

or even

the practice of putting a structure such as a phrase inside a structure of the same kind (within Language context).

But the best definition I've ever read is this one found on the University of Utah - School of Computing website:

Recursion is the process of defining a problem (or the solution to a problem) in terms of itself.

Source: https://www.cs.utah.edu/~germain/PPS/Topics/recursion.html

Nearly philosophical!

Recursive Components

Now that you understood (or almost!) what recursion stands for, let's think of some scenarios where it could be applied within a VueJS application.

  1. Imagine that you are building an application similar to a social network where people can comment on posts. Now, imagine that people might be able to comment on comments.

  2. Now, let's say you might need to display the whole structure of a file system containing folders within folders as something we call tree.

There might be other scenarios but these two above are enough for us to think about implementing recursive components.

In the first scenario, a comment might have comments, so we could implement a component that invokes itself in order to display comments within comments.

In the second scenario, we may need to implement a tree node component that might contain other tree nodes inside, which would be the case of folders (or files) inside folders.

Implementation

Let's grab the second example, which is commonly required.
We are going to implement a recursive component that is capable of displaying a tree structure.

There are two important things we must consider:

  • we want to build an extensible component in terms of layout because we might need to display different trees;

  • we must have a breaking point within the component to avoid loops. This will work exactly the same way it does within recursive functions to prevent it from calling itself endlessly;

Creating the project structure

First, let's create a basic VueJS project structure. If you like, you might check this other article of mine where I explain how I structure my own boilerplates to get a VueJS project up and running.

Once you've done that, you are ready to move to the next step.

Building a recursive component

First of all let's create the "shell" of our component. We want to display the label of each node.

Let's make use of the b-alert component from Bootstrap Vue just to avoid dealing too much with styling.

Initially we are going to display the node label and also an arrow that will help us show and hide the node children.

Create a TreeNode.vue file within components folder and implement it like the following:

src/components/TreeNode.vue
<template>
  <b-alert show class="d-flex justify-content-between mb-1">
     {{ node.label }}
     <span class="fas fa-angle-right" />
   </b-alert>
</template>
Enter fullscreen mode Exit fullscreen mode
export default {
  name: 'TreeNode',
  props: {
    node: {
      type: Object,
      required: true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's render this new component in Home.vue page.
As the TreeNode represents only one node, we need to use a v-for directive to render all of our nodes.
We're going to use a static set of data in order to see the components working.

This is how we do it:

src/views/Home.vue
<template>
  <div>
    <TreeNode v-for="node in nodes" :key="node.id" :node="node" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
import TreeNode from '@/components/TreeNode/TreeNode'

export default {
  name: 'HomeView',
  components: {
    TreeNode
  },
  computed: {
    nodes() {
      return [
        {
          id: 1,
          label: 'Foods',
          children: [
            {
              id: 2,
              label: 'Fruits',
              children: [
                {
                  id: 3,
                  label: 'Banana'
                },
                {
                  id: 4,
                  label: 'Apple'
                },
                {
                  id: 5,
                  label: 'Strawberry'
                }
              ]
            },
            {
              id: 6,
              label: 'Vegetables',
              children: [
                {
                  id: 7,
                  label: 'Carrot'
                },
                {
                  id: 8,
                  label: 'Lettuce'
                },
                {
                  id: 9,
                  label: 'Potato'
                }
              ]
            }
          ]
        },
        {
          id: 10,
          label: 'Drinks',
          children: [
            {
              id: 11,
              label: 'Beers',
              children: [
                {
                  id: 12,
                  label: 'Budweiser'
                },
                {
                  id: 13,
                  label: 'Heineken'
                }
              ]
            },
            {
              id: 14,
              label: 'Wines'
            },
            {
              id: 15,
              label: 'Whiskey'
            }
          ]
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This is how it will look like:

image

Now let's implement the magic.
The TreeNode component must be able to reference itself in order to display the children nodes of each node. As a tree structure may contain several levels, our component must support that.

In order to achieve that, we are going to implement a breaking point checking if the node has children or not. If it has, it will render a v-for loop of itself to display the nested nodes.

This is how it will look like:

<div>
    <b-alert show class="d-flex justify-content-between mb-1">
      {{ node.label }}
      <span class="fas fa-angle-right" />
    </b-alert>
    <div v-if="hasChildren">
      <TreeNode
        v-for="child in node.children"
        :key="child.id"
        :node="child"
      />
    </div>
  </div>
Enter fullscreen mode Exit fullscreen mode
export default {
  name: 'TreeNode',
  props: {
    node: {
      type: Object,
      required: true
    }
  },
  computed: {
    hasChildren() {
      const { children } = this.node
      return children && children.length > 0
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And this is how it should look like:

image

Now we can see all nodes of our tree structure, but it still doesn't look like a tree, right?!
We need to manipulate styling in order to achieve that.
So, what we do is: let's add a left margin to each node according to its level.

We're going to add a spacing prop to the TreeNode component and use it to set a margin-left style into the layout. We are also going to pass this prop to the children nodes incrementing it by 10 (which result in 10px when rendering).

Check this out:

<template>
  <div :style="nodeMargin">
    <b-alert show class="d-flex justify-content-between mb-1">
      {{ node.label }}
      <span class="fas fa-angle-right" />
    </b-alert>
    <div v-if="hasChildren">
      <TreeNode
        v-for="child in node.children"
        :key="child.id"
        :node="child"
        :spacing="spacing + 10"
      />
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
export default {
  name: 'TreeNode',
  props: {
    node: {
      type: Object,
      required: true
    },
    spacing: {
      type: Number,
      default: 0
    }
  },
  computed: {
    nodeMargin() {
      return {
        'margin-left': `${this.spacing}px`
      }
    },
    hasChildren() {
      const { children } = this.node
      return children && children.length > 0
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This is how it will look like:

image

We're almost there. Now, we just need to make sure that the right-side arrow will be displayed only if the node has children and also implement a method that will show and hide children nodes.

In order to do that we are going make the arrow icon dynamic using a computed property and also create a reactive variable to control if the children nodes of that respective node are being displayed or not and call it from the click event of the icon.

<template>
  <div :style="nodeMargin">
    <b-alert show class="d-flex justify-content-between mb-1">
      {{ node.label }}
      <span
        v-if="hasChildren"
        :class="toggleChildrenIcon"
        @click="toggleChildren"
        @keypress="toggleChildren"
      />
    </b-alert>
    <div v-if="hasChildren" v-show="showChildren">
      <TreeNode
        v-for="child in node.children"
        :key="child.id"
        :node="child"
        :spacing="spacing + 10"
      />
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
export default {
  name: 'TreeNode',
  props: {
    node: {
      type: Object,
      required: true
    },
    spacing: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      showChildren: false
    }
  },
  computed: {
    nodeMargin() {
      return {
        'margin-left': `${this.spacing}px`
      }
    },
    hasChildren() {
      const { children } = this.node
      return children && children.length > 0
    },
    toggleChildrenIcon() {
      return this.showChildren ? 'fas fa-angle-down' : 'fas fa-angle-right'
    }
  },
  methods: {
    toggleChildren() {
      this.showChildren = !this.showChildren
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And this is the result:
https://www.loom.com/share/233a8108a4ca43ecb5723d4e791ceb20

You may want to customise this base component to support your layout and business requirements by adding some more advanced functionality like:

  • close all children nodes when closing a parent node;
  • props to allow the user to customise classes;
  • node search;

Conclusion

When building web applications we need to be aware of which data structures we're going to deal with.
There are several scenarios where data must be displayed as a tree.

In this article, I explained one (of some) approaches to build a recursive component that will help you display data as a tree without much effort.

Extra

In the example above we assumed the data would be structured as tree using the children approach but this is not the only way of defining a tree in terms of data.
Sometimes all we have is a flat array with items that reference themselves to inform which node is the parent of which node.
Like the following:

[
  {
     id: 1,
     label: 'Root Node',
     parentId: null,
     order: 1
  },
  {
     id: 2,
     label: 'Child Node',
     parentId: 1,
     order: 1
  },
  {
     id: 3,
     label: 'Grandchild Node',
     parentId: 2,
     order: 2
  },
  {
     id: 4,
     label: 'Root Node 2',
     parentId: null,
     order: 2
  }
]
Enter fullscreen mode Exit fullscreen mode

This same structure can be displayed as a tree like this:

image

If this is your case, you might want to use this reduce function to help you transform data before passing it to your recently-learnt Tree Component. :)

You can find the full source code in this repository.

I hope you liked it!
Please, comment and share...

Oldest comments (4)

Collapse
 
imaginativeone profile image
Doug Franklin

Loved this article! I'm working on implementing this with the Composition API as well as Drag and Drop.

Collapse
 
simonardelima profile image
Simonarde Lima

Thanks! It helped me a lot!

Collapse
 
sbu profile image
Sbu

Hi, very nice component. I am totally taking it as inspiration for my own version :) Thanks!

There is a little issue with it though: left margins add up on each level.

Level 1 gets its margin-left set to 10px, then level 2 gets 10+20 = 30px, then level 3 would get 10 + 20 + 30 = 60px and so on. We can actually see this issue in your last screenshots.

In short, spacing props is simply not useful: you just have to set a margin-left: 10px on each TreeNode. Those margins naturally add up in each sub-level because each div already has its parent's margin.

Collapse
 
vcpablo profile image
Pablo Veiga

Thank you so much for the heads up @sbu!
I really appreciate your interest in reading the full article and even collaborating to make it better!!

I wish you the best of luck and success on your endeavor.
🙏