Search code examples
javascriptvue.jstreevuejs3treeview

Vue js 3 - My Json or Object tree viewer have a duplicate key bug when multiple values are similar


I have a problem with this code: I'm trying to make a tree viewer from a object and it work great until when a value is exactly the same as another value it clone de key and it replace the same key where the value is the same.

On CodePen: https://codepen.io/onigetoc/pen/rNPeQag?editors=1010

This object:

[
    {
        "userId": 1,
        "id": 1,
        "title": "delectus aut autem",
        "description": "delectus aut autem"
    },
    {
        "userId": 1,
        "id": 2,
        "title": "quis ut nam facilis et officia qui",
        "description": "et porro tempora"
    },
]

Give me in my tree view that:

[-]
userId:1
userId:1
title:delectus aut autem
title:delectus aut autem
[-]
userId:1
id:2
title:quis ut nam facilis et officia qui
description:et porro tempora

We can see that the keys userId and title are duplicated because they have the same values. The second part is ok because there's no identical values.

HTML part:

<!-- item template -->
<script type="text/x-template" id="item-template">
  <li>
    <span :class="{bold: isFolder}" @click="toggle">
      <span class="key" v-if="!isFolder" @click="toggleKey">{{ key }}:</span>
      <span class="value" v-if="!isFolder">{{ value }}</span>
      <span v-if="isFolder">[{{ isOpen ? '-' : '+' }}]</span>
    </span>
    <ul v-show="isOpen" v-if="isFolder">
      <tree-item
        class="item"
        v-for="(child, index) in item"
        :key="index"
        :item="child"
      ></tree-item>
    </ul>
  </li>
</script>

<p>(You can double click on an item to turn it into a folder.)</p>

<!-- the demo root element -->
<ul class="view-list" id="connect-list">
  <tree-item class="item" :item="treeData" @make-folder="makeFolder" @add-item="addItem"></tree-item>
</ul>

Javascript part:

<!-- demo data -->
// https://jsonplaceholder.typicode.com/users
var treeData = [
    {
        "userId": 1,
        "id": 1,
        "title": "delectus aut autem",
        "description": "delectus aut autem"
    },
    {
        "userId": 1,
        "id": 2,
        "title": "quis ut nam facilis et officia qui",
        "description": "et porro tempora"
    },
];

const app = Vue.createApp({
  data: function() {
    return {
      treeData: treeData
    }
  },

methods: {
  makeFolder: function(item) {
    Vue.set(item, 'newFolder', {});
    this.addItem(item.newFolder);
  },
  addItem: function(item) {
    Vue.set(item, 'newItem', 'new stuff');
  }
}
})

app.component("tree-item", {
  template: '#item-template',
  props: {
    item: Object
  },
  data: function() {
    return {
      isOpen: true // default was false
    };
  },
  computed: {
    isFolder: function() {
      return typeof this.item === 'object' && this.item !== null;
    },
    key: function() {
      if (typeof this.item === 'object' && this.item !== null) {
        return '';
      } else {
        return Object.keys(this.$parent.item).find(key => this.$parent.item[key] === this.item);
      }
    },
    value: function() {
      if (typeof this.item === 'object' && this.item !== null) {
        return '';
      } else {
        return this.item;
      }
    }
  },
  methods: {
    toggle: function() {
      if (this.isFolder) {
        this.isOpen = !this.isOpen;
      }
    },
    toggleKey: function(event) {
      event.target.classList.toggle("bgcolor");
    }
  }
})

  app.mount('#connect-list')

Solution

  • The issue is that Object.keys(this.$parent.item).find(key => this.$parent.item[key] === this.item); will find the first key with a matching value

    Simple fix really, you need to pass the "index" as a prop (since the index in an object is a string, and item is an Object in tree-item)

    I use name as the prop, but I don't really like that :p can't think of a better name for this prop - but, that's not really something to worry about

      <tree-item
        class="item"
        v-for="(child, index) in item"
        :key="index"
        :name="index"
        :item="child"
      ></tree-item>
    

    note: keep the :key="index" as that is vital for the v-for

    and return this.name in the key: computed

    key: function() {
          if (typeof this.item === 'object' && this.item !== null) {
            return '';
          } else {
            return this.name;
          }
        },