Tree View

TreeView shows a tree you can select from, expand and collapse, for example folders and files. It works with the mouse and with the keyboard. You give it a flat list of TreeNode objects, and it emits select with the key of the row the user picks.

Usage

Selected: nothing yet
<TreeView items="$nodes" selected="$selected" label="Files" (select)="onSelect" />
<div class="text-muted small mt-2">Selected: {$selected === null ? 'nothing yet' : $selected}</div>
<?php

use Viewi\Components\BaseComponent;
use Viewi\UI\Components\Navigation\TreeNode;

class FilesPage extends BaseComponent
{
    public array $nodes = [];
    public $selected = null;

    public function mounted()
    {
        // key, label, depth, parentKey, icon, badge
        $this->nodes = [
            new TreeNode('docs', 'Documents', 1, null, 'bi-folder', '3'),
            new TreeNode('reports', 'Reports', 2, 'docs', 'bi-folder', '2'),
            new TreeNode('q1', 'Q1.pdf', 3, 'reports', 'bi-file-earmark'),
            new TreeNode('q2', 'Q2.pdf', 3, 'reports', 'bi-file-earmark'),
            new TreeNode('notes', 'Notes.txt', 2, 'docs', 'bi-file-earmark'),
            new TreeNode('photos', 'Photos', 1, null, 'bi-folder', '2'),
            new TreeNode('beach', 'Beach.jpg', 2, 'photos', 'bi-file-earmark'),
            new TreeNode('city', 'City.jpg', 2, 'photos', 'bi-file-earmark'),
            new TreeNode('readme', 'Readme.md', 1, null, 'bi-file-earmark'),
        ];
    }

    public function onSelect($key)
    {
        $this->selected = $key;
    }
}

Building the list

items is a flat list in tree order: a node, then all of its children, then the next node. Each node has:

  • depth - its level, 1 for the top level. It sets the indentation.
  • parentKey - the key of its parent, null for the top level.

A node has children when the node right after it in the list has its key as parentKey. Such a node gets a chevron that opens and closes it.

TreeView does not change selected by itself. Handle select and set the value you pass to selected, as in the example above.

Expand and collapse

  • A small tree (collapseOver nodes or fewer) starts fully expanded. A larger tree starts collapsed. Set collapseOver="0" to always start expanded.
  • When selected changes, all parents of the selected node are opened, so the highlighted row is never hidden inside a closed branch.
  • Once the user opens or closes a node, their choice is kept when items changes.
  • Clicking the chevron opens or closes the node without selecting it.

Keyboard

The tree is one Tab stop: the row the user last focused, else the selected row, else the first one.

  • ArrowUp and ArrowDown move between visible rows. Home and End jump to the first and last row.
  • ArrowRight opens a closed node, or moves to its first child when it is open.
  • ArrowLeft closes an open node, or moves to its parent.
  • Enter or Space selects the row.

Row actions and rename

Use the actions slot to put buttons or a Dropdown Menu next to each row. The slot is rendered beside the row button, not inside it. It shows when the user hovers or focuses the row, and it is always visible on the selected row and on touch screens.

Use the edit slot to replace a row with your own content, for example a rename box. It is shown for the row whose key equals editingKey.

Both slots give you the row's TreeNode with data. Hover a row and click the pencil to rename it (press Enter to save, Escape to cancel), or the trash icon to delete it.

<TreeView items="$nodes" selected="$selected" editingKey="$editingKey" label="Files" (select)="onSelect">
    <slotContent name="actions" data="$node">
        <button type="button" class="btn btn-sm btn-link p-1" title="Rename" (click)="startRename($node)"><Icon name="bi-pencil" /></button>
        <button type="button" class="btn btn-sm btn-link p-1 text-danger" title="Delete" (click)="deleteNode($node)"><Icon name="bi-trash" /></button>
    </slotContent>
    <slotContent name="edit" data="$node">
        <input type="text" class="form-control form-control-sm" value="$node->label" aria-label="New name"
            (keydown)="onRenameKey($event)" />
    </slotContent>
</TreeView>
<?php

use Viewi\Components\BaseComponent;
use Viewi\Components\DOM\DomEvent;
use Viewi\UI\Components\Navigation\TreeNode;

class FilesPage extends BaseComponent
{
    public array $nodes = [];
    public $selected = null;
    public $editingKey = null;

    // mounted() and onSelect() as in the first example

    public function startRename(TreeNode $node)
    {
        $this->editingKey = $node->key;
    }

    public function deleteNode(TreeNode $node)
    {
        $this->removeNode($node->key);
    }

    public function onRenameKey(DomEvent $event)
    {
        if ($event->key === 'Enter') {
            $this->renameNode($this->editingKey, $event->target->value);
            $this->editingKey = null;
        } else if ($event->key === 'Escape') {
            $this->editingKey = null;
        }
    }

    public function renameNode($key, string $label)
    {
        $result = [];
        foreach ($this->nodes as $node) {
            if ($node->key === $key && $label !== '') {
                $result[] = new TreeNode($node->key, $label, $node->depth, $node->parentKey, $node->icon, $node->badge);
            } else {
                $result[] = $node;
            }
        }
        $this->nodes = $result;
    }

    // Removes the node and everything under it (the rows that follow it and are deeper).
    public function removeNode($key)
    {
        $result = [];
        $skipBelow = 0;
        foreach ($this->nodes as $node) {
            if ($skipBelow > 0 && $node->depth > $skipBelow) {
                continue;
            }
            $skipBelow = 0;
            if ($node->key === $key) {
                $skipBelow = $node->depth;
                continue;
            }
            $result[] = $node;
        }
        $this->nodes = $result;
        $this->selected = null;
    }
}

Drag and drop

Rows whose key is in draggableKeys can be dragged. The tree emits dragStart with the key when a drag starts and dragEnd when it ends, whether it was dropped or not.

With dropEnabled, every row except those in dropDisabledKeys accepts a drop. The row under the pointer is highlighted, and dropping emits drop with that row's key. A row that does not accept a drop shows the browser's "no drop" cursor and emits nothing.

TreeView does not move anything. Your component remembers what is being dragged (from dragStart, or from anything else on the page you made draggable) and updates items on drop.

In this example files can be dragged, and only folders accept a drop.

Drag a file onto a folder.
<TreeView items="$nodes" selected="$selected" label="Files" draggableKeys="$fileKeys" dropEnabled
    dropDisabledKeys="$fileKeys" (select)="onSelect" (dragStart)="onDragStart" (dragEnd)="onDragEnd"
    (drop)="onDrop" />
<div class="text-muted small mt-2">{$message === '' ? 'Drag a file onto a folder.' : $message}</div>
<?php

use Viewi\Components\BaseComponent;
use Viewi\UI\Components\Navigation\TreeNode;

class FilesPage extends BaseComponent
{
    public array $nodes = [];
    public $selected = null;
    public array $fileKeys = ['q1', 'q2', 'notes', 'beach', 'city', 'readme'];
    public $dragging = null;
    public string $message = '';

    // mounted() and onSelect() as in the first example

    public function onDragStart($key)
    {
        $this->dragging = $key;
    }

    public function onDragEnd()
    {
        $this->dragging = null;
    }

    // A file was dropped on a folder: move it to be the folder's first child.
    public function onDrop($folderKey)
    {
        if ($this->dragging === null) {
            return;
        }
        $moved = null;
        $rest = [];
        foreach ($this->nodes as $node) {
            if ($node->key === $this->dragging) {
                $moved = $node;
            } else {
                $rest[] = $node;
            }
        }
        if ($moved === null) {
            return;
        }
        $result = [];
        foreach ($rest as $node) {
            $result[] = $node;
            if ($node->key === $folderKey) {
                $result[] = new TreeNode($moved->key, $moved->label, $node->depth + 1, $node->key, $moved->icon);
                $this->message = $moved->label . ' moved to ' . $node->label;
            }
        }
        $this->nodes = $result;
        $this->dragging = null;
    }
}

Flat list and disabled rows

Set flat to list every item at one level, without indentation or chevrons. This is useful for search results, where the parents of a match may not be in the list. Tree Picker uses it this way.

Rows whose key is in disabledKeys are shown but can not be selected: clicking them or pressing Enter does not emit select.

<TreeView items="$matches" flat label="Search results" (select)="onSelect" />
<TreeView items="$nodes" disabledKeys="$here" label="Folders" (select)="onSelect" />

Properties

TreeView

items - list of TreeNode objects in tree order. Default: [].

selected - (optional) key of the highlighted row. Default: null.

label - (optional) accessible name (aria-label) of the tree. Default: Tree.

collapseOver - (optional) up to this many nodes the tree starts fully expanded. With more nodes it starts collapsed, except the path to selected. 0 means always start expanded. Default: 20.

indent - (optional) indentation per level, in rem. Default: 0.85.

flat - (optional) shows every item at one level, without indentation or chevrons. Arrow left and right do nothing. Default: false.

disabledKeys - (optional) keys of rows that are shown but can not be selected. Default: [].

editingKey - (optional) key of the row that shows the edit slot instead of its button. Default: null.

draggableKeys - (optional) keys of rows that can be dragged. Default: [].

dropEnabled - (optional) rows accept drops. Default: false.

dropDisabledKeys - (optional) keys of rows that never accept a drop, even with dropEnabled. Default: [].

TreeNode

TreeNode is a data class. Create it with new TreeNode($key, $label, $depth, $parentKey, $icon, $badge, $title).

key - unique key of the node. It is the value of select, drop and dragStart.

label - the text of the row.

depth - (optional) level of the node, 1 for the top level. Default: 1.

parentKey - (optional) key of the parent node, null for the top level. Default: null.

icon - (optional) icon shown before the label. Default: `` (empty), no icon.

badge - (optional) text shown in a badge at the end of the row, for example a count. Default: `` (empty), no badge.

title - (optional) tooltip of the row. Tree Picker also uses it as the full path of the node. Default: `` (empty).

Events

TreeView

(select) - happens when the user clicks a row or presses Enter or Space on it. The event value is the node's key. Disabled rows do not emit it.

(dragStart) - happens when the user starts dragging a row from draggableKeys. The event value is the node's key.

(dragEnd) - happens when a drag of a row ends, dropped or not.

(drop) - happens when something is dropped on a row that accepts drops. The event value is the target node's key.

Slots

TreeView

actions - content shown beside every row, for example buttons or a menu. data is the row's TreeNode.

edit - content shown instead of the row button when the node's key equals editingKey. data is the row's TreeNode.