20181026

How to make a reactive, recursive tree navigation with Vue.js

In my last post I showed how to make your portlets reactive with Vue.js and Axios. Today I want to present my solution for a recursive tree naviagtion, that you can use as a component for the former. You can find a live demo on codesandbox.io. Here is the tree.jsp:
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@taglib prefix="portlet" uri="http://java.sun.com/portlet_2_0"%><portlet:defineObjects />

<div id="<portlet:namespace />tree">
  <div class="body">
    <div class="tree">
      <tree :items="items[1]"></tree>
    </div>
    <div v-if="openItem">
      <p>{{ openItem.text }}</p>
    </div>
  </div>
</div>

<script>
Vue.component('tree', {
  template: `\
    <ol>\
      <li v-for="key in Object.keys(items)" :key="key">\
        <a :class="{open : isOpen(key)}" @click="open(key)">{{ key }}. {{ items[key].heading }}</a>\
      <tree v-if="getSubitems(key) && isOpen(key)" :items="getSubitems(key)"></tree>\
    </li>\
    </ol>\
  `,
  props: {
    items: Object
  },
  methods: {
    getSubitems: function(key) {
      var levels = key.split(".");
      var sublevel = this.$root.$data.items[levels.length+1];
      if (sublevel) {
        return sublevel[key];
      }
    },
    isOpen: function(key) {
      if (this.$root.$data.openKey.startsWith(key)) {
        return true;
      } else {
        return false;
      }
    },
    open: function(key) {
      this.$root.$data.openKey = key;
    }
  }
});

var app = new Vue({
  el: '#<portlet:namespace />tree',
  data: function() {
    return {
      items : {
        "1" : {
          "1": {
            "heading": "Item A",
            "text" : "cold"
          },
          "2": {
            "heading": "Item B",
            "text" : "warm"
          },
          "3": {
            "heading": "Item C",
            "text" : "cold"
          },
          "4": {
            "heading": "Item D",
            "text" : "very cold"
          }
        },
        "2" : {
          "1": {
            "1.1": {
              "heading": "Subitem A 1",
              "text" : "colder"
            },
            "1.2": {
              "heading": "Subitem A 2",
              "text" : "very cold"
            }
          },
          "2": {
            "2.1": {
              "heading": "Subitem B 1",
              "text" : "colder"
            },
            "2.2": {
              "heading": "Subitem B 2",
              "text" : "warmer"
            }
          },
          "3": {
            "3.1": {
              "heading": "Subitem C 1",
              "text" : "very cold"
            }
          }
        },
        "3" : {
          "2.2" : {
            "2.2.1" : {
              "heading": "Deep Subitem",
              "text": "very hot"
            }
          }
        },
        "4" : {
          "2.2.1" : {
            "2.2.1.1" : {
              "heading" : "Another Deep Subitem",
              "text" : "Congratulations, you made it so deep!"
            }
          }
        }
      },
      openKey: ''
    }
  },
  computed: {
    openItem: function() {
      if (this.openKey) {
        var level = this.openKey.split(".").length;
        var levelItems = this.items[level];
        if (level == 1) {
          levelItems = levelItems[this.openKey];
        } else {
          var lastDot = this.openKey.lastIndexOf(".");
          var parent = this.openKey.substring(0, lastDot);
          levelItems = levelItems[parent][this.openKey];
        }
        return levelItems;
      }
    }
  }
})
</script>
<style>
#<portlet:namespace />tree {
  display: flex;
  flex-flow: column nowrap;
  height: 100%;
}
#<portlet:namespace />tree * {
  box-sizing: border-box;
}
#<portlet:namespace />tree ol {
  list-style-type: none;
}
#<portlet:namespace />tree .body {
  display: flex;
  flex-flow: row nowrap;
  height: 100%;
  width: 100%;
}
#<portlet:namespace />tree .body .tree {
  max-width: 50%;
  min-width: 50%;
  overflow-y: scroll;
}
#<portlet:namespace />tree .body ol {
  box-sizing: content-box;
  padding: 0;
}
#<portlet:namespace />tree .body a.open+ol>li {
  padding-left: 10px;
}
#<portlet:namespace />tree .body li {
  background: white;
  line-height: 2em;
}
#<portlet:namespace />tree .body li:hover {
  cursor: pointer;
}
#<portlet:namespace />tree .body li a {
  background: none;
  display: block;
  outline: none;
}
#<portlet:namespace />tree .body li a:hover {
  text-decoration: none;
}
#<portlet:namespace />tree .body li a.open {
  background: #f7ed4d;
}
</style>

We use a flexible layout and use no list-style counting, because this would constrain us. Instead we display the key as the "real" key from our items. As a data structure we use some kind of a multi-dimensional hash-list. Each navigation hierarchy is located on the first level of our items. Why we don't use a nested hierarchy instead, like the following?
"2" : {
  "heading" : "Item B",
  "text" : "warm",
  "2.2"  : {
    "heading" : "Subitem B",
    "text" : "warmer",
    "2.2.1" : {
      "heading" : "Another Deep subitem",
      "text" : "very hot",
      "2.2.1.1" : {
        "heading" : "Another Deep subitem",
        "text" : "Congratulations, you made it so deep!"
      }
    }
  }
}

Wouldn't this be more intuitive? Maybe from the viewpoint of creating the datastructure itself, but this wouldn't serve us well, when it comes to the operations, we want to apply on the items, and we don't want "big" items.

So, the list is being created breadth first, not depth first in the for loop.

With this we don't need to parse the keys of each item to see, if it is a subtree or not, then remember it for later after we displayed the siblings first then go back display the sibling, etc. which would cause us a lot of headache...

We just look into the next hierarchy level and if it has a link to the parent, we display it as another tree. This we can do for each item and we have the freedom to view each item as an independet piece. Of course we start with the items[1] at level 1.

Enjoy!

No comments:

Post a Comment

Please stick to the Netiquette Guidelines and consider asking questions the smart way ..