connecteev's avatar

Nested Vue components with counts of direct children and nested children

I am trying to implement nested comments in vue.js and nuxt.js.

  • Each comment can have one or more children comments.
  • Each child comment, can again, have one or more children comments.
  • Unlimited levels of nested comments is possible.

As you can see in the diagram I have attached, I would like each comment to "know" (for the sake of simplicity, to display) the following information:

  1. The depth of the comment (I have this working already). Example, all of the "top-level" comments are at depth=0, all their children are at depth=1, and so on.
  2. The number of direct children
  3. the number of children (including nested children, unlimited levels deep)

enter image description here

I came across this question on StackOverflow but it doesn't quite do the trick. Or maybe I am doing something wrong.

In case you want to take a look at my (very messy) code, here it is. However, I'm willing to start over, so appreciate any pointers on how to pass the data up / down the chain of nested comments (vue components). Some sample code would be great.

components/PostComment.vue:

<template>
<div>


  <div class="tw-flex tw-flex-wrap tw-justify-end">
    <div :class="indent" class="tw-w-full tw-flex">

      <div class="tw-font-bold tw-p-4 tw-border-gray-400 tw-border tw-rounded tw-text-right">
        <div class="kb-card-section">
          <div class="kb-card-section-content tw-flex tw-flex-wrap tw-items-center tw-text-left">

            <div class="tw-flex tw-w-full">
              <div class="tw-hidden md:tw-block md:tw-w-2/12 tw-text-right tw-my-auto">
                <div class="tw-flex">
                  <p class="tw-w-full tw-text-xs tw-text-gray-600 tw-text-right">children: {{ numNestedChildComments }}, depth: {{depth}}</p>
                </div>
              </div>
            </div>

          </div>

        </div>

      </div>

    </div>

    <div class="tw-w-full" v-if="commentData.nested_comments" v-for="nestedComment in commentData.nested_comments">
      <post-comment 
        :commentData="nestedComment"
        :depth="depth + 1"
        :numChildCommentsOfParent=numNestedChildComments
      />
    </div>

  </div>


</div>
</template>

<script>

export default {
  name: 'post-comment', // necessary for recursive components / nested comments to work
  props: {
    depth: {
      type: Number,
      required: true
    },
    postAuthorData: {
      type: Object,
      required: true
    },
    commentAuthorData: {
      type: Object,
      required: true
    },
    commentData: {
      type: Object,
      required: true
    },
    numChildCommentsOfParent: {
      type: Number,
      required: true
    },
  },
  data() {
    return {
      numNestedChildComments: this.numChildCommentsOfParent,
    }
  },
  mounted() {
    this.incrementNumParentComments();
  },
  methods: {
    incrementNumParentComments() {
      this.numNestedChildComments++;
      this.$emit('incrementNumParentComments');
    },
  },
  computed: {
    indent() {
      switch (this.depth) {
        case 0:
          return "tw-ml-0 tw-mt-1";
        case 1:
          return "tw-ml-4 tw-mt-1";
        case 2:
          return "tw-ml-8 tw-mt-1";
        case 3:
        default:
          return "tw-ml-12 tw-mt-1";
      }
    },
  },
}

</script>
0 likes
8 replies
fylzero's avatar

You pass data up the chain by using $emit...

Child:

<input @input="$emit('dothing', 'Simple String Value Here')">

Parent:

<MyComponent @dothing="executeThisMethod"/>

methods: {
    executeThisMethod(simpleStringValue) {
        console.log(simpleStringValue);
    }
}

Alternately, you pass data down using props.

1 like
fylzero's avatar

If you're wanting to build this counting thing... I would strongly recommend watch The Net Ninja's YouTube video series on VueX and implementing a store you can simply increment each thing... then have access to it wherever you need.

https://www.youtube.com/watch?v=BGAu__J4xoc

2 likes
connecteev's avatar

@fylzero I'm trying to do it when the components are initialized instead of using VueX.

  • Child initialized (mounted hook) = increment counter and bubble up to the parent.
  • Parent =increment it's own counter and bubble up to it's parent recursively..
  • and so on.

I haven't been able to get it to work though.

Lelectrolux's avatar

I have no solution to propose on the vue side, as I never had this problem, but if you have control over the backend, the "initial" counting problem might be better solved server side, in the database query (or at the backend application layer if you really want to...). Aggregates are a problem solved more than efficiently in SQL. Depth could also be computed backend side.

Then the problem in the front end is way simpler, as you only have to keep in sync the counts, which happens to be easy to bubble up (just re-emit it on the deleted/added event handler at each level).

I realise this isn't a direct solution for what you want to achieve, but it might be an alternative worth exploring, depending on your sql table structure and proefficiency.

1 like
jlrdw's avatar

Have you looked at any of the nested set packages for this sort of thing.

I usually like restricting levels to so many, code can really get out of hand.

One example I wrote a pedigree program, some of the dogs that went way back, their progeny report ( reverse pedigree) on just 6 Generations were in the hundreds of pages if you were going to print it out.

Later I changed it over to Just 4 generation and if more detail was needed made name clickable to pull up a four generation on that dog.

So good logic and organization in this nesting really comes in handy.

1 like
fylzero's avatar

@connecteev You should count them on initialize and pass them to a VueX store. It will make wiring this much easier.

That said... you can take what I posted about emits and props and use that to wire your data. You already know where to count the items at. All you have to do is wire the data.

Go forth and good luck!

2 likes
rodrigo.pedra's avatar
Level 56

I made a simplified version to propose a solution:

Here as a parent component calling the tree roots:

<template>
    <div>
        <MyTree v-for="item in records" :key="item.id" :item="item" />
    </div>
</template>

<script>
import MyTree from './MyTree';

const FIXTURE = [
    {
        id: 1,
        children: [
            {
                id: 2,
                children: [{id: 3}, {id: 4}, {id: 5}],
            },
            {
                id: 6,
                children: [
                    {id: 7},
                    {id: 8, children: [{id: 9}, {id: 10}]},
                ],
            },
        ],
    },
    {
        id: 11,
        children: [
            {id: 12, children: [{id: 13}, {id: 14}, {id: 15}]},
            {id: 16, children: [{id: 17}]},
            {id: 18},
        ],
    },
];

export default {
    components: {MyTree},

    data() {
        return {
            records: FIXTURE,
        };
    },
};
</script>

And here is the tree component:

<template>
    <div>
        <div style="border: 1px solid black; padding: 5px;" :style="offset">
            id: {{ item.id }}
            // depth: {{ depth }}
            // direct: {{ direct }}
            // children: {{ childrenCount }}
        </div>

        <template v-if="item.children">
            <MyTree
                v-for="record in item.children"
                :key="record.id"
                :item="record"
                :depth="depth + 1"
                @born="handleBorn()" />
        </template>
    </div>
</template>

<script>
const COLORS = [
    'white',
    'lightgray',
    'lightblue',
    'lightcyan',
    'lightskyblue',
    'lightpink',
];

export default {
    // MUST give a name in recursive components
    // https://vuejs.org/v2/guide/components-edge-cases.html#Recursive-Components
    name: 'MyTree',

    props: {
        item: {type: Object, required: true},
        depth: {type: Number, default: 0},
    },

    data() {
        return {
            childrenCount: 0,
        };
    },

    computed: {
        direct() {
            if (Array.isArray(this.item.children)) {
                return this.item.children.length;
            }

            return 0;
        },

        offset() {
            return {
                'margin-left': (this.depth * 20) + 'px',
                'background-color': COLORS[this.depth % COLORS.length],
            };
        },
    },

    mounted() {
        this.$emit('born');
    },

    methods: {
        handleBorn() {
            this.childrenCount++;
            this.$emit('born');
        },
    },
};
</script>

EDIT

Added screenshot:

nested tree output

3 likes
connecteev's avatar

@fylzero @jlrdw @lelectrolux @rodrigo.pedra thank you so much for sharing your thoughts...

I spent a lot of time thinking about all your answers and experimenting....but I really wanted a front-end solution (to save the burden of having to maintain separate counts on the back-end side).

There's no denying that @rodrigo.pedra 's answer nailed it! Thank you!

2 likes

Please or to participate in this conversation.