In the following post I will show how to create a recursive Treeview using Vue.

This is my first time trying Vue, so I figured I'd start learning by building a simple Treeview. To add to the challenge I will also show how to Closure compile the Treeview code using the ADVANCED compiler setting. Using Closure will usually result in great optimizations of the original JavaScript.

You can find a live demo of the Treeview here.

The first step is to include a reference to the Vue library itself by adding a script tag to index.html.

<script src="vue/vue.min.js"></script>

Next I will create a simple Vue component

import { LocationService } from './location.service'; import { Vue } from './vue'; let locationService = new LocationService(); Vue['component']('treeview', { 'props': ['locations'], 'template': ` <ul> <li v-for="location in locations"> <div v-if="location.visible"> <span class="iconButton" v-on:click="location.toggleNode()">{{location.icon}}</span> {{location.name}} <treeview v-bind:locations="location.locations"></treeview> </div> </li> </ul> ` }); var treeview = new Vue({ 'el': '#treeview-demo', 'data': { 'locations': locationService.getLocations() } });

In the component I have defined a recursive template with the necessary bindings to render a treeview with expandable nodes.

I am loading the component in index.html like this:

<treeview v-bind:locations="locations"></treeview>

It may seem like I am integrating with an ES6 build of Vue since I am importing {Vue}. This is just a trick to keep Closure happy. Behind the import I have a simple

export const Vue = window['Vue'];

This is needed because Closure does not recognize the global Vue variable that was introduced when I loaded the Vue script tag.

I am also including a simple location service to return the backing data model for the tree.

import { Location } from './location'; export class LocationService { getLocations() { let usa = new Location('USA', ['New York', 'Texas'], true); let nyc = usa.getLocation('New York').addLocation('New York City'); nyc.addLocation('Brooklyn'); nyc.addLocation('Manhattan'); nyc.addLocation('Queens'); nyc.addLocation('Bronx'); nyc.addLocation('Staten Island'); usa.getLocation('Texas').addLocation('Houston'); usa.getLocation('Texas').addLocation('Austin'); usa.getLocation('Texas').addLocation('Dallas'); let germany = new Location('Germany', ['Berlin'], true); let norway = new Location('Norway', ['Oslo'], true); return [usa, germany, norway]; } }

As you can tell, the treeview will be displaying some simple location based data with parent-child relationships between the locations. LocationService returns an object graph of Location objects.

export class Location { constructor(name, locations, visible) { this['visible'] = visible; this['expanded'] = false; this['name'] = name; this['locations'] = locations.map(l => new Location(l, [], false)); this['icon'] = this.getIcon(); this['toggleNode'] = this.toggle; } toggle() { this['expanded'] = !this['expanded']; if(this['expanded']){ this['icon'] = '-'; } this['icon'] = this.getIcon(); this['locations'].forEach(l => { l['visible'] = this['expanded']; l['icon'] = l.getIcon(); }); } addLocation(name) { let newLocation = new Location(name, [], false); this['locations'].push(newLocation); return newLocation; } getLocation(name) { return this['locations'].find(l => l.name === name); } getIcon() { if(this['locations'].length === 0){ return null; } if(this['expanded']){ return '-'; } return '+'; } }

Location is essentially a view model for each node in the treeview.

You may find it weird that I am using dynamic bracket notation when accessing properties in the Location class.

Bracket notation is one of the tradeoffs of using Closure compiler in combination with template bindings.

Closure will apply aggressive optimizations to my JavaScript code, which includes shortening of property names.

Changing the property names will break my Vue data bindings since the property names must match the bindings in the markup. If the property names are shortened in the JavaScript they will no longer match what's in the view. Luckily we can tell closure to not shorten the names by using bracket notation.

I am using Rollup to invoke the Closure compiler and generate the bundle.

Here is my Rollup configuration:

import rollup from 'rollup' import closure from 'rollup-plugin-closure-compiler-js'; export default { entry: './vue/treeview.js', dest: './vue/treeview.min.js', format: 'umd', globals:['Vue'], plugins: [ closure({ languageIn: 'ECMASCRIPT6', languageOut: 'ECMASCRIPT5', compilationLevel: 'ADVANCED', warningLevel: 'DEFAULT' }) ] }

I am using the JavaScript version of the closure compiler with the Closure Rollup plugin. The closure compiled JavaScript ends up looking like this:

for(var d="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(c.get||c.set)throw new TypeError("ES3 does not support getters and setters.");a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)},e="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this,f=["Array","prototype","find"],g=0;g<f.length-1;g++){var h=f[g];h in e||(e[h]={});e=e[h]} var l=f[f.length-1],m=e[l],n=m?m:function(a,b){a:{var c=this;c instanceof String&&(c=String(c));for(var y=c.length,k=0;k<y;k++){var u=c[k];if(a.call(b,u,k,c)){a=u;break a}}a=void 0}return a};n!=m&&null!=n&&d(e,l,{configurable:!0,writable:!0,value:n});var p=window.Vue;function q(a,b,c){this.visible=c;this.expanded=!1;this.name=a;this.locations=b.map(function(a){return new q(a,[],!1)});this.icon=r(this);this.toggleNode=this.toggle}q.prototype.toggle=function(){var a=this;(this.expanded=!this.expanded)&&(this.icon="-");this.icon=r(this);this.locations.forEach(function(b){b.visible=a.expanded;b.icon=r(b)})};function t(a,b){b=new q(b,[],!1);a.locations.push(b);return b}function v(a){return w.locations.find(function(b){return b.name===a})} function r(a){return 0===a.locations.length?null:a.expanded?"-":"+"};p.component("treeview",{props:["locations"],template:'

\x3cul\x3e

\x3cli v-for\x3d"location in locations"\x3e

\x3cdiv v-if\x3d"location.visible"\x3e

\x3cspan class\x3d"iconButton" v-on:click\x3d"location.toggleNode()"\x3e{{location.icon}}\x3c/span\x3e

{{location.name}}

\x3ctreeview v-bind:locations\x3d"location.locations"\x3e\x3c/treeview\x3e

\x3c/div\x3e

\x3c/li\x3e

\x3c/ul\x3e

'}); var w=new q("USA",["New York","Texas"],!0),x=t(v("New York"),"New York City");t(x,"Brooklyn");t(x,"Manhattan");t(x,"Queens");t(x,"Bronx");t(x,"Staten Island");t(v("Texas"),"Houston");t(v("Texas"),"Austin");t(v("Texas"),"Dallas");var z=new q("Germany",["Berlin"],!0),A=new q("Norway",["Oslo"],!0);new p({el:"#treeview-demo",data:{locations:[w,z,A]}});

My Rollup bundling depends on the following npm dependencies

"google-closure-compiler-js" "rollup" "rollup-plugin-closure-compiler-js"

Run the bundling using the following command:

node_modules/.bin/rollup -c vue/rollup-config.js

Here is a simple demo of the treeview