Building a Treeview component (Part II)

Damjan Namjesnik
Vuetify
Published in
7 min readMay 31, 2018

--

In this article we continue development of a new Vuetify component for treeview. If you haven’t already, you can read Part I here.

Where we are

In the previous part we built a foundation and demonstrated how to build a recursive component. The result of it is a tree of plain looking indented labels.

Current state of treeview development

New feature: selecting items

In Part I we already added a v-model property for holding the list of selected items. We made it optional by setting null value as default. Now, we will bind it to an empty array and render VCheckbox components instead of plain labels to handle it.

We also have to import the VCheckbox component in order for this to work, so we will add it to the top of the component definition along with other imports:

Importing dependencies

Rendering a checkbox

How do we know if the checkboxes need to be used at all? We can check if the value property has been set on the component — i.e. check if it’s not null. Let’s adjust the genRoot function to check for this and render a VCheckbox if needed:

Rendering a checkbox

The else branch contains the previous code which simply renders a label, so we omit it for now.

Like previously, we need to set some options to properly render VCheckbox:

  • label — the component caption will be displayed
  • hideDetails — we hide hint’s and validation errors to make a more compact display
  • inputValue — the v-model bound value
  • value — the value added to bound selection array if checkbox is set. Here we simply add the items reference
  • on: { change … — here we monitor if the checkbox value has changed, and emit an input event to notify bound values of changes to selection

Within our template we define <v-treeview> element like this

v-treeview in the template

… and bind it to the data along with an empty selectedItems array:

Bound data when used in a playground component

The result of these changes is that we can now have a much nicer look for our treeview:

Treeview made of checkboxes

If we start clicking the checkboxes, we will notice that only the “Root” can be checked and unchecked, and that the children and grandchildren are not reacting.

We are emmiting an input event from all of the VTreeview components, but only the top-most root is bound to the selectedItems. The root and child nodes are not listening to the input events coming from below, and are not propagating the changes they get notified of. Let’s fix that by adjusting the genChild function:

Listening to input event and emmiting it upwards

Now we are able to click and check/uncheck all of the items in the tree and have selectedItems contain the corresponding items.

Checkboxes working on all items

Notice how the checkboxes in the picture above are not really behaving as one would expect from such component — if we click on the “Root” and check it, we expect it to put a checkmark on all of the children. Likewise, when only some of the children are selected and some not, we expect that the parent checkbox is in an indeterminate state.

We will comeback to this after we make some more adjustments to the case when the checkboxes are not used.

Rendering plain labels

In case when checkboxes are not used, we still get the plain looking labels as they were created in Part I of the article. We will want the labels to look like the ones found in checkboxes for a consistent look and feel.

By looking at the source code of VCheckbox component, we can find which classes are used for it.

Let’s change the else branch of genRoot function and make the structure like in the VCheckbox component:

Generating plain labels in genRoot function

We will generate a <div> element which has the listed classes, and underneath a <label> element with the caption.

Because we are not using any other input elements, we will add the following styles to the treeview-label class in the src/components/stylus/_treeview.styl file in order to prevent collapsing of all treeview labels:

Additional style for label

We will get that the labels look exactly like those when checkboxes are used:

The rendered treeview with plain labels

Enabling proper checkbox behaviour

To achieve expected behavior of checkboxes, we need to split them into two groups:

  • parents: they can be unchecked, checked and indeterminate depending on if all of the descendants are checked or not. We don’t want to see these elements in the bound value (i.e. selectedItems variable in the template)
  • leafs: they can be either checked or unchecked, and they should behave like they are now

Let’s update the genRoot function in accordance with this:

Changes to genRoot function

What changed is how we handle the following properties:

  • inputValue: if current treeview has children, we will switch to a true/false value based on computed propert; otherwise, same as before
  • value: similar as previous, but the current items reference is given in case of no children
  • indeterminate: new property to determine if checkbox should be in indeterminate state

The hasChildren, allDescendantLeafsSelected and indeterminate are implemented as computed properties to make our code easire to read and avoid repetition. We implemented a number of such properties which return true or false:

hasChildren implementation
allDescendantLeafsSelected implementation

Function allDescendantLeafsSelected will return true only if for all of the leafs under it there exists an item in this.value. Here we use another computed property called allDescendantLeafs:

allDescendantLeafs implementation

The above function will recursively go through all of the children and fill in the leafs array. The searchTree function is responsible for the recursion.

If the given items parameter has children, searchTree will invoke itself for every child. If it doesn’t, this means we encountered a leaf and we need to push it to the resulting array.

This resulting array is then returned after the searchTree is called for the this.items object (i.e. the children).

indeterminate implementation

For indeterminate status, we set it only if the node has some children selected, but not all of them.

Within above mentioned computed properties, we also used some additional computed properties, where the implementation is pretty straightforward:

hasSelection implementation
someDescendantLeafsSelected implementation

Now that the properties are handled, we need to implement changes to the change event handler to support two types of checkboxes we might have:

Checkbox change event handler

With all the helper computed properties in place, it’s easier to read the above function:

  • If item is a parent (i.e. has children):
  • we check if all of it’s descendant leafs have been checked as well
  • if yes: deselect them one by one by removing them from this.value array
  • if not: go through all descendant leafs and push them to the this.value array if they are not already there
  • finally emit that the v-model needs to be updated with our alterations
  • —Else we know item has no children:
  • emit the new selection like before (which holds all of the selected items in an array)

Component in action

With all of the changes in place, we have a functioning treeview component in place. Have a look at the demo:

Treeview in action

Please note, that the selected items will actually be objects (nodes), and not just the names associated with them. In the example above we show the names for the selected objects for illustrative purpose.

What’s next

In the Part III of our treeview building walkthrough, we will continue implementing additional features on top of the current work. We will start by adding the expansion and collapse button, highlighting on hover, and in doing this do some more refactoring for additional features which will be developed afterwards.

With the current approach of keeping the selected items as objects, we will also show how it can lead to some errors, and of course, how to fix it :)

If you have some questions or remarks, do not hesitate do comment below!

--

--