Skip to content

Amphiluke/cbx-tree

Repository files navigation

<cbx-tree>: The Checkbox Tree element

The <cbx-tree> element is a web component for building tree-like hierarchic lists with checkable items. Tree items in the <cbx-tree> element are collapsible if they have nested subtrees. Every item is equipped with a checkbox which can be in one of the following states:

  • checked: the item and all its children are checked,
  • unchecked: the item and all its children are unchecked,
  • indeterminate: the item is technically unchecked but some of its children are checked.

Demonstration of design and functionality of the <cbx-tree> element

Live demo on CodePen

Installation and import

If you use a bundler in your project, install cbx-tree as a dependency:

npm install cbx-tree

Now you may import it wherever it’s needed:

import 'cbx-tree';

If you don’t use bundlers, just import the component as a module in your HTML files:

<script type="module" src="https://unpkg.com/cbx-tree"></script>

or in ES modules:

import 'https://unpkg.com/cbx-tree';

Usage notes

There are two ways to feed initial tree data to the <cbx-tree> component.

The first way is to provide tree data directly in HTML by adding JSON content as follows:

<cbx-tree name="reading-list[]">
  <script type="application/json">
    [
      {
        "title": "Epic poetry",
        "value": "category-123",
        "icon": "/icons/epic-icon.svg",
        "children": [
          {
            "title": "Ancient Greek poems",
            "value": "category-179",
            "icon": "/icons/greek-icon.svg",
            "children": [
              {
                "title": "Iliad",
                "value": "book-10",
                "icon": "/icons/manuscript-icon.svg",
                "checked": true
              },
              {
                "title": "Odyssey",
                "value": "book-11",
                "icon": "/icons/manuscript-icon.svg",
                "checked": true
              }
            ]
          },
          {
            "title": "Ancient Mesopotamian poems",
            "value": "category-151",
            "icon": "/icons/mesopotamian-icon.svg",
            "collapsed": true,
            "children": [
              {
                "title": "Epic of Gilgamesh",
                "value": "book-8",
                "icon": "/icons/clay-tablet-icon.svg"
              }
            ]
          }
        ]
      }
    ]
  </script>
</cbx-tree>

Note

Similarly to the <textarea> content, the data you provide in HTML is only used as a default value. In other words, dynamic updates of the HTML content don’t affect the current tree. To update the tree dynamically, one should use the JavaScript API provided by the component.

The second option is to fill the initial tree programmatically using the setData() method.

HTML:

<cbx-tree name="reading-list[]"></cbx-tree>

JavaScript:

customElements.whenDefined('cbx-tree').then(() => {
  const readingList = document.querySelector('[name="reading-list[]"]');
  readingList.setData([
    {
      title: 'Epic poetry',
      value: 'category-123',
      icon: '/icons/epic-icon.svg',
      children: [
        {
          title: 'Ancient Greek poems',
          value: 'category-179',
          icon: '/icons/greek-icon.svg',
          children: [
            {
              title: 'Iliad',
              value: 'book-10',
              icon: '/icons/manuscript-icon.svg',
              checked: true,
            },
            {
              title: 'Odyssey',
              value: 'book-11',
              icon: '/icons/manuscript-icon.svg',
              checked: true,
            },
          ],
        },
        {
          title: 'Ancient Mesopotamian poems',
          value: 'category-151',
          icon: '/icons/mesopotamian-icon.svg',
          collapsed: true,
          children: [
            {
              title: 'Epic of Gilgamesh',
              value: 'book-8',
              icon: '/icons/clay-tablet-icon.svg',
            },
          ],
        },
      ],
    },
  ]);
});

Note

JavaScript API of the <cbx-tree> component becomes fully functional as soon as the element is registered and defined. To stay on the safe side, it’s worth using the whenDefined() guard as shown in the example above.

Tree data structure

As shown in the examples above, the tree is initialised with an array of objects representing the tree’s root items. Each item can have children forming a nested subtree. The table below provides information about the properties that can be specified for tree items at any nesting level.

Property Type Required Description
title string yes Text label of the tree item
value string¹ yes Internal value identifying the checked item in the submitted data
icon string no Item icons’s URL or SVG icon code
checked boolean no Initial state of the item selection
collapsed boolean no Whether a nested subtree is collapsed initially
children array or null² no Nested subtree items

¹ Must be unique within the entire tree.
² The value null of the children property is used for on-demand loading of the subtree.

Attributes

This element includes the global attributes.

disabled

Applying this Boolean attribute turns all interactive controls within the tree into the disabled state. Items in the disabled tree cannot be collapsed or expanded by the user, and states of the checkboxes cannot be changed via the GUI.

name

A mandatory attribute name is used by the <cbx-tree> component to construct data to be submitted with the form. Since the widget contains multiple checkable items, it may be a good idea to use a name with square brackets appended. This notation allows some server-side frameworks treat submitted data as an array.

<cbx-tree name="reading-list[]"></cbx-tree>

nohover

By default, items in the <cbx-tree> component grab focus and get highlighted when pointer hovers over them, similarly to options in the <select> element’s dropdown. A Boolean attribute nohover makes the <cbx-tree> component deactivate this behaviour, so that items only become selected when clicked or focused by keyboard navigation (similarly to options in a <select> with the multiple attribute specified).

<cbx-tree name="reading-list[]" nohover></cbx-tree>

Instance properties

The CbxTree interface also inherits properties from its parent, HTMLElement.

Validation-related properties validity, validationMessage, and willValidate are transparently exposed from the underlying ElementInternals object which allows the <cbx-tree> element participate in form validation.

CbxTree.disabled

Reflects the value of the element’s disabled attribute.

CbxTree.form

The read-only property that references the HTMLFormElement associated with this element.

CbxTree.formData

A FormData object which contains key-value pairs of the currently checked items in the tree. Note that the element’s name attribute is used for all keys, so the FormData object represents an array of checked values. The property is read-only.

const readingList = document.querySelector('[name="reading-list[]"]');
console.log('Checked values:', readingList.formData.getAll('reading-list[]'));

CbxTree.name

Reflects the value of the element’s name attribute.

CbxTree.noHover

Reflects the value of the element’s nohover attribute.

CbxTree.subtreeProvider

The subtreeProvider property is used in cases where on-demand subtree loading is required. If your initial tree doesn’t contain data for some nested subtrees, you may define your custom function for subtree generation/fetching which will be called when the user expands the target item for the first time.

Important

The items that allow on-demand loading, should have their children property set to null initially.

The custom subtree provider is a function that accepts the value of the target item as its argument and returns a promise that resolves with an array representing a subtree data for this specific item.

customElements.whenDefined('cbx-tree').then(() => {
  const readingList = document.querySelector('[name="reading-list[]"]');
  readingList.subtreeProvider = async (itemValue) => {
    const response = await fetch(`/reading-list/items/${itemValue}`);
    return (await response.json()).children;
  };
});

CbxTree.type

A read-only property provided for consistency with browser-provided form controls. The same as Element.localName.

Instance methods

The CbxTree interface also inherits methods from its parent, HTMLElement.

Validation-related methods checkValidity(), reportValidity(), and setValidity() are transparently exposed from the underlying ElementInternals object which allows the <cbx-tree> element participate in form validation.

CbxTree.filter()

This method can be used to “filter” the tree by hiding those items that don’t meet custom criteria. The method accepts a single argument, a predicate function. The predicate is passed an object argument with item’s title and value as properties, and the return value must be true if the item passes the filter and false otherwise. It should be noted that if an item passes the filter, its descendants remain visible even if they themselves don’t satisfy the filtering condition.

const readingList = document.querySelector('[name="reading-list[]"]');
const filterInput = document.getElementById('filter');
filterInput.addEventListener('input', () => {
  const query = filterInput.value.trim().toLocaleLowerCase();
  const predicate = query.length ? ({title}) => title.toLocaleLowerCase().includes(query) : () => true;
  readingList.filter(predicate);
});

CbxTree.setData()

The setData() method is used for complete overwriting and rerendering the entire tree. It accepts a single argument, a new tree data. All existing changes will be lost and replaced by the newly provided data after calling this method. See an example in the Usage notes section.

CbxTree.toggle()

Use this method for dynamic expansion or collapsing of all items in the tree. The method accepts an optional boolean argument, isExpanding, which controls whether items should be expanded (true) or collapsed (false).

const readingList = document.querySelector('[name="reading-list[]"]');
readingList.toggle(false); // collapse all

Note that this method doesn’t expand items that have on-demand loading behavior. Also, programmatic toggling doesn’t trigger the cbxtreetoggle event.

CbxTree.toggleChecked()

This method can be used to check or uncheck all the items in the tree. The method accepts an optional boolean argument, checked, which controls whether items should be checked (true) or unchecked (false).

const readingList = document.querySelector('[name="reading-list[]"]');
readingList.toggleChecked(true); // check all

Note that programmatic checking of the items doesn’t trigger the cbxtreechange event.

CbxTree.toJSON()

Returns the current state of the tree in the same format as the array used for tree initialisation. This method allows for JSON serialisation of the control state.

const readingList = document.querySelector('[name="reading-list[]"]');
console.log('Tree data:', JSON.stringify(readingList, null, 2));

Events

cbxtreechange

The cbxtreechange custom event is fired when the user changes the state of a checkbox in the tree. A complete information on the tree selection state is available as a FormData object through the detail property of the event instance.

const readingList = document.querySelector('[name="reading-list[]"]');
readingList.addEventListener('cbxtreechange', (e) => {
  const selectionData = e.detail; // FormData instance
  console.log('Selected books & categories:', ...selectionData.values());
});

cbxtreetoggle

The cbxtreetoggle custom event is fired when the user clicks a toggle button to expand or collapse a subtree under one of the tree items. The detail property of the event instance provides additional information about the target item:

Property Description
title Title of the target item
value Value of the target item
newState New state of the target item (either expanded or collapsed)
const readingList = document.querySelector('[name="reading-list[]"]');
readingList.addEventListener('cbxtreetoggle', (e) => {
  const {title, value, newState} = e.detail;
  console.log(`Item “${title}” (${value}) is now ${newState}`);
});

Styling with CSS

The <cbx-tree> element provides a few CSS custom properties (variables) that you can override for your needs.

Variable Data type Description
--cbx-tree-toggle-closed-mask <url>¹ Mask image for the toggle button in the collapsed state
--cbx-tree-toggle-open-mask <url> Mask image for the toggle button in the expanded state
--cbx-tree-toggle-pending-mask <url> Mask image for the toggle button in the pending state
--cbx-tree-label-focus-bg <color>² Background color for the highlighted item’s label
--cbx-tree-label-focus-fg <color> Text color for the highlighted item’s label
--cbx-tree-nesting-indent <length>³ Indentation size for nested subtrees

¹ https://developer.mozilla.org/en-US/docs/Web/CSS/url_value
² https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
³ https://developer.mozilla.org/en-US/docs/Web/CSS/length

In the following example, item toggle button’s mask is changed from the default arrow to “+/−” icons:

cbx-tree {
  --cbx-tree-toggle-closed-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="14" height="14"><path d="M3 7L11 7M7 3L7 11" stroke="black"/></svg>');
  --cbx-tree-toggle-open-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="14" height="14"><path d="M3 7L11 7" stroke="black"/></svg>');
}

Additionally, parts of the <cbx-tree> element can be directly styled through the ::part() pseudo-element.

Caution

Directly styling the inner parts of the tree is (to some extent) an advanced technique that comes with the risk of breaking the tree’s UI. Use it as a last resort if the desired result cannot be achieved with regular CSS inheritance.

The available ::part() pseudo-elements are listed in the following table and are shown in the picture below.

Pseudo-element Matched parts
::part(tree) The root tree and any nested subtree
::part(item) Any individual item of a tree/subtree
::part(toggle) Item toggle buttons
::part(label) Wrappers around any item’s checkbox, icon, and title
::part(checkbox) Any item’s checkbox
::part(icon) Any item’s icon
::part(title) Any item’s title

Tree parts that can be styled using the ::part() selector

cbx-tree::part(title) {
  transition: scale 0.2s ease-in-out;
  transform-origin: 0 50%;
}
cbx-tree::part(title):hover {
  scale: 1.1;
}

About

Web Component for building tree-like UI with checkable items

Topics

Resources

License

Stars

Watchers

Forks