Tree
Tree components display hierarchical data in a collapsible structure, allowing users to explore nested relationships while keeping the interface organized and navigable.
import {TreeModule} from "@qualcomm-ui/angular/tree"Overview
- The tree relies on the
TreeCollectionclass to manage its items. Refer to the API below for details. - Trees are composed of nodes, which are objects that describe the tree data. There are two types of nodes:
- A
branchnode is a node that has children. - A
leafnode is a node that does not have children.
- A
- Each node has a
value(unique identifier used for selection/expansion) and atext(display text).
Default object shape:
value(required): unique identifier for expansion/selectiontext(required): display textnodes(optional): child nodes for branchesdisabled(optional): prevents interaction whentrue
These defaults can be overridden in the TreeCollection constructor.
Examples
Node Shorthand
We expose the q-tree-nodes component for rendering Branch and Leaf nodes. Use the <ng-template q-tree-branch-template> and <ng-template q-tree-leaf-template> directives to customize the content of each tree item.
Note that q-tree-nodes automatically renders child nodes for branches, so you only have to customize the content of the node itself.
<q-tree-nodes [indexPath]="[i]" [node]="node">
<ng-template
let-branch
q-tree-branch-template
[rootNode]="collection.rootNode"
>
<div q-tree-branch-node>
<div q-tree-branch-trigger></div>
<svg q-tree-node-icon qIcon="FolderIcon"></svg>
<span q-tree-node-text>{{ branch.node.name }}</span>
</div>
</ng-template>
<ng-template
let-leaf
q-tree-leaf-template
[rootNode]="collection.rootNode"
>
<div q-tree-leaf-node>
<div q-tree-node-indicator></div>
<svg q-tree-node-icon qIcon="FileText"></svg>
<span q-tree-node-text>{{ leaf.node.name }}</span>
</div>
</ng-template>
</q-tree-nodes>
Node Types
Pass the rootNode input to the template directives to enable TypeScript type inference. This allows branch.node and leaf.node to have your custom node type instead of the generic TreeNode type.
Nodes
You can bring your own recursive component to create your own abstraction for the tree.
NOTE
This approach is recommended only for advanced use cases. Most users should use the shorthand q-tree-nodes instead.
import {Component, computed, inject, input, type OnInit} from "@angular/core"
import {FileText, FolderIcon} from "lucide-angular"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {
provideTreeNodePropsContext,
provideTreeNodeStateContext,
TreeNodePropsContextService,
TreeNodeStateContextService,
useTreeContext,
} from "@qualcomm-ui/angular-core/tree"
import {IconDirective} from "@qualcomm-ui/angular/icon"
import {TreeModule} from "@qualcomm-ui/angular/tree"
import {createTreeCollection, type NodeProps} from "@qualcomm-ui/core/tree"
interface FileNode {
id: string
name: string
nodes?: FileNode[]
}
const collection = createTreeCollection<FileNode>({
nodeChildren: "nodes",
nodeText: (node) => node.name,
nodeValue: (node) => node.id,
rootNode: {
id: "ROOT",
name: "",
nodes: [
{
id: "node_modules",
name: "node_modules",
nodes: [
{
id: "@qui",
name: "@qui",
nodes: [
{
id: "node_modules/@qualcomm-ui/core",
name: "@qualcomm-ui/core",
},
{
id: "node_modules/@qualcomm-ui/react",
name: "@qualcomm-ui/react",
},
{
id: "node_modules/@qualcomm-ui/react-core",
name: "@qualcomm-ui/react-core",
},
],
},
{
id: "node_modules/@types",
name: "@types",
nodes: [
{id: "node_modules/@types/react", name: "react"},
{id: "node_modules/@types/react-dom", name: "react-dom"},
],
},
],
},
{
id: "src",
name: "src",
nodes: [
{id: "src/app.tsx", name: "app.tsx"},
{id: "src/index.ts", name: "index.ts"},
],
},
{id: "prettier.config.js", name: "prettier.config.js"},
{id: "package.json", name: "package.json"},
{id: "tsconfig.json", name: "tsconfig.json"},
],
},
})
@Component({
imports: [TreeModule, IconDirective],
providers: [
provideIcons({FileText, FolderIcon}),
provideTreeNodePropsContext(),
provideTreeNodeStateContext(),
],
selector: "tree-nodes-recursive",
template: `
@let childNodes = treeContext().collection.getNodeChildren(node());
@if (childNodes.length) {
<div q-tree-branch>
<div q-tree-branch-node>
<div q-tree-node-indicator></div>
<div q-tree-branch-trigger></div>
<svg q-tree-node-icon qIcon="FolderIcon"></svg>
<span q-tree-node-text>
{{ treeContext().collection.stringifyNode(node()) }}
</span>
</div>
<div q-tree-branch-content>
<div q-tree-branch-indent-guide></div>
@for (
childNode of childNodes;
let j = $index;
track treeContext().collection.getNodeValue(childNode)
) {
<tree-nodes-recursive
[indexPath]="indexPath().concat(j)"
[node]="childNode"
/>
}
</div>
</div>
} @else {
<div q-tree-leaf-node>
<div q-tree-node-indicator></div>
<svg q-tree-node-icon qIcon="FileText"></svg>
<span q-tree-node-text>
{{ treeContext().collection.stringifyNode(node()) }}
</span>
</div>
}
`,
})
export class TreeNodesRecursive implements OnInit {
readonly indexPath = input.required<number[]>()
readonly node = input.required<FileNode>()
protected readonly treeContext = useTreeContext()
private readonly nodePropsService = inject(TreeNodePropsContextService)
private readonly nodeStateService = inject(TreeNodeStateContextService)
ngOnInit() {
const nodeProps = computed<NodeProps<FileNode>>(() => ({
indexPath: this.indexPath(),
node: this.node(),
}))
this.nodePropsService.init(nodeProps)
this.nodeStateService.init(
computed(() => this.treeContext().getNodeState(nodeProps())),
)
}
}
@Component({
imports: [TreeModule, TreeNodesRecursive],
providers: [provideIcons({FileText, FolderIcon})],
selector: "tree-nodes-demo",
template: `
<div class="w-full max-w-sm" q-tree-root [collection]="collection">
@for (
node of collection.rootNode.nodes;
let i = $index;
track collection.getNodeValue(node)
) {
<tree-nodes-recursive [indexPath]="[i]" [node]="node" />
}
</div>
`,
})
export class TreeNodesDemo {
collection = collection
}Default Expanded
Expand nodes by default using the defaultExpandedValue input. Or use expandedValue and expandedValueChanged to control the expansion manually. These inputs follow our controlled state pattern.
<div
class="w-full max-w-sm"
q-tree-root
[collection]="collection"
[defaultExpandedValue]="['src']"
>
Checkbox Trees
Use the q-tree-node-checkbox directive within each node to create a checkbox tree. The checked state of the tree can be controlled using the checkedValue, checkedValueChanged, and defaultCheckedValue inputs, which follow our controlled state pattern.
import {Component} from "@angular/core"
import {TreeModule} from "@qualcomm-ui/angular/tree"
import {createTreeCollection} from "@qualcomm-ui/core/tree"
interface Node {
id: string
nodes?: Node[]
text: string
}
@Component({
imports: [TreeModule],
selector: "tree-checkbox-demo",
template: `
<div
class="w-full max-w-sm"
q-tree-root
[collection]="collection"
[defaultExpandedValue]="['qualcomm', 'amd', 'intel']"
>
@for (
node of collection.rootNode.nodes;
let i = $index;
track collection.getNodeValue(node)
) {
<q-tree-nodes [indexPath]="[i]" [node]="node">
<ng-template
let-branch
q-tree-branch-template
[rootNode]="collection.rootNode"
>
<div q-tree-branch-node>
<div q-tree-branch-trigger></div>
<span q-tree-node-checkbox></span>
<span q-tree-node-text>{{ branch.node.text }}</span>
</div>
</ng-template>
<ng-template
let-leaf
q-tree-leaf-template
[rootNode]="collection.rootNode"
>
<div q-tree-leaf-node>
<div q-tree-node-indicator></div>
<span q-tree-node-checkbox></span>
<span q-tree-node-text>{{ leaf.node.text }}</span>
</div>
</ng-template>
</q-tree-nodes>
}
</div>
`,
})
export class TreeCheckboxDemo {
collection = createTreeCollection<Node>({
nodeText: "text",
nodeValue: "id",
rootNode: {
id: "ROOT",
nodes: [
{
id: "qualcomm",
nodes: [
{
id: "snapdragon_x_elite",
nodes: [
{id: "X1E-00-1DE", text: "12-core X1E-00-1DE"},
{id: "X1E-84-100", text: "12-core X1E-84-100"},
{id: "X1E-80-100", text: "12-core X1E-80-100"},
{id: "X1E-78-100", text: "12-core X1E-78-100"},
],
text: "Snapdragon X Elite",
},
{
id: "snapdragon_x_plus",
nodes: [
{id: "X1P-66-100", text: "10-core X1P-66-100"},
{id: "X1P-64-100", text: "10-core X1P-64-100"},
{id: "X1P-46-100", text: "8-core Plus X1P-46-100"},
{id: "X1P-42-100", text: "8-core Plus X1P-42-100"},
],
text: "Snapdragon X Plus",
},
],
text: "Qualcomm",
},
{
id: "intel",
nodes: [
{
id: "intel_core_ultra",
nodes: [
{id: "ultra9_s2", text: "Core Ultra 9 (Series 2)"},
{id: "ultra7_s2", text: "Core Ultra 7 (Series 2)"},
{id: "ultra5_s2", text: "Core Ultra 5 (Series 2)"},
],
text: "Intel Core Ultra",
},
{
id: "intel_core_i9",
nodes: [
{id: "i9_14th", text: "Core i9 14th Gen"},
{id: "i9_13th", text: "Core i9 13th Gen"},
],
text: "Intel Core i9",
},
{
id: "intel_core_i7",
nodes: [
{id: "i7_14th", text: "Core i7 14th Gen"},
{id: "i7_13th", text: "Core i7 13th Gen"},
],
text: "Intel Core i7",
},
{
id: "intel_core_i5",
nodes: [
{id: "i5_14th", text: "Core i5 14th Gen"},
{id: "i5_13th", text: "Core i5 13th Gen"},
],
text: "Intel Core i5",
},
{
id: "intel_core_i3",
nodes: [
{id: "i3_14th", text: "Core i3 14th Gen"},
{id: "i3_13th", text: "Core i3 13th Gen"},
],
text: "Intel Core i3",
},
],
text: "Intel",
},
{
id: "amd",
nodes: [
{
id: "amd_threadripper",
nodes: [
{
id: "threadripper_9000",
text: "Ryzen Threadripper 9000 Series",
},
{
id: "threadripper_7000",
text: "Ryzen Threadripper 7000 Series",
},
],
text: "AMD Threadripper",
},
{
id: "amd_ryzen_9",
nodes: [
{id: "ryzen9_9000", text: "Ryzen 9 9000 Series"},
{id: "ryzen9_7000", text: "Ryzen 9 7000 Series"},
],
text: "AMD Ryzen 9",
},
{
id: "amd_ryzen_7_5",
nodes: [
{id: "ryzen7_9000", text: "Ryzen 7 9000 Series"},
{id: "ryzen7_8000g", text: "Ryzen 7 8000-G Series"},
{id: "ryzen7_8000", text: "Ryzen 7 8000 Series"},
{id: "ryzen7_7000", text: "Ryzen 7 7000 Series"},
{id: "ryzen5_9000", text: "Ryzen 5 9000 Series"},
{id: "ryzen5_8000g", text: "Ryzen 5 8000-G Series"},
{id: "ryzen5_8000", text: "Ryzen 5 8000 Series"},
{id: "ryzen5_7000", text: "Ryzen 5 7000 Series"},
],
text: "AMD Ryzen 7 / 5",
},
],
text: "AMD",
},
],
text: "",
},
})
}Checkbox selection state
The Tree handles nested checkbox selection automatically:
- If all of a node's children are checked, the node will also be checked.
- If only some of a node's children are checked, the node will appear indeterminate to indicate partial selection.
When you supply the checkedValue or defaultCheckedValue inputs, you must account for the above logic.
Consider the following tree:
const collection = createTreeCollection<Node>({
rootNode: {
id: "ROOT",
name: "",
nodes: [
{
id: "qualcomm",
name: "Qualcomm",
nodes: [
{
id: "sdx",
name: "Snapdragon X",
nodes: [
{id: "elite", name: "Snapdragon X Elite"},
{id: "plus", name: "Snapdragon X Plus"},
],
},
],
},
],
},
})Let's say that we want to select the sdx node and its children by default.
@Component({
template: `
<!-- won't work, we need to supply the child nodes instead -->
<div q-tree-root [defaultCheckedValue]="['sdx']" [collection]="collection">
...
</div>
`,
})
export class MyComponent {
// ...
}Instead, supply all the child nodes to indicate that the parent is selected:
<div
class="w-full max-w-sm"
q-tree-root
[collection]="collection"
[defaultCheckedValue]="['elite', 'plus']"
[defaultExpandedValue]="['qualcomm', 'sdx']"
>
The following demo shows checked state below the tree. Interact with it to see state updates:
[ "X1E-00-1DE", "X1E-84-100" ]
<q-tree-nodes [indexPath]="[i]" [node]="node">
<ng-template
let-branch
q-tree-branch-template
[rootNode]="collection.rootNode"
>
<div q-tree-branch-node>
<div q-tree-branch-trigger></div>
<span q-tree-node-checkbox></span>
<span q-tree-node-text>{{ branch.node.id }}</span>
</div>
</ng-template>
<ng-template
let-leaf
q-tree-leaf-template
[rootNode]="collection.rootNode"
>
<div q-tree-leaf-node>
<span q-tree-node-checkbox></span>
<span q-tree-node-text>{{ leaf.node.id }}</span>
</div>
</ng-template>
</q-tree-nodes>
Disabled Nodes
You can disable nodes by setting the disabled property on the node object.
import {Component} from "@angular/core"
import {FileText, FolderIcon} from "lucide-angular"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {IconDirective} from "@qualcomm-ui/angular/icon"
import {TreeModule} from "@qualcomm-ui/angular/tree"
import {createTreeCollection} from "@qualcomm-ui/core/tree"
interface FileNode {
disabled?: boolean
id: string
name: string
nodes?: FileNode[]
}
@Component({
imports: [TreeModule, IconDirective],
providers: [provideIcons({FileText, FolderIcon})],
selector: "tree-disabled-node-demo",
template: `
<div class="w-full max-w-sm" q-tree-root [collection]="collection">
@for (
node of collection.rootNode.nodes;
let i = $index;
track collection.getNodeValue(node)
) {
<q-tree-nodes [indexPath]="[i]" [node]="node">
<ng-template
let-branch
q-tree-branch-template
[rootNode]="collection.rootNode"
>
<div q-tree-branch-node>
<div q-tree-node-indicator></div>
<div q-tree-branch-trigger></div>
<svg q-tree-node-icon qIcon="FolderIcon"></svg>
<span q-tree-node-text>{{ branch.node.name }}</span>
</div>
</ng-template>
<ng-template
let-leaf
q-tree-leaf-template
[rootNode]="collection.rootNode"
>
<div q-tree-leaf-node>
<div q-tree-node-indicator></div>
<svg q-tree-node-icon qIcon="FileText"></svg>
<span q-tree-node-text>{{ leaf.node.name }}</span>
</div>
</ng-template>
</q-tree-nodes>
}
</div>
`,
})
export class TreeDisabledNodeDemo {
collection = createTreeCollection<FileNode>({
nodeChildren: "nodes",
nodeText: (node) => node.name,
nodeValue: (node) => node.id,
rootNode: {
id: "ROOT",
name: "",
nodes: [
{
id: "node_modules",
name: "node_modules",
nodes: [
{
id: "@qui",
name: "@qui",
nodes: [
{
id: "node_modules/@qualcomm-ui/core",
name: "@qualcomm-ui/core",
},
{
id: "node_modules/@qualcomm-ui/react",
name: "@qualcomm-ui/react",
},
{
id: "node_modules/@qualcomm-ui/react-core",
name: "@qualcomm-ui/react-core",
},
],
},
{
id: "node_modules/@types",
name: "@types",
nodes: [
{id: "node_modules/@types/react", name: "react"},
{id: "node_modules/@types/react-dom", name: "react-dom"},
],
},
],
},
{
id: "src",
name: "src",
nodes: [
{id: "src/app.tsx", name: "app.tsx"},
{id: "src/index.ts", name: "index.ts"},
],
},
{id: "prettier.config.js", name: "prettier.config.js"},
{id: "package.json", name: "package.json"},
{disabled: true, id: "renovate.json", name: "renovate.json"},
{id: "tsconfig.json", name: "tsconfig.json"},
],
},
})
}Filtering
Here's an example that filters the nodes using matchSorter.
import {Component, signal} from "@angular/core"
import {FormsModule} from "@angular/forms"
import {FileText, FolderIcon, Search} from "lucide-angular"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {IconDirective} from "@qualcomm-ui/angular/icon"
import {TextInputModule} from "@qualcomm-ui/angular/text-input"
import {TreeModule} from "@qualcomm-ui/angular/tree"
import {createTreeCollection} from "@qualcomm-ui/core/tree"
import type {TreeCollection} from "@qualcomm-ui/utils/collection"
import {matchSorter} from "@qualcomm-ui/utils/match-sorter"
interface FileNode {
id: string
name: string
nodes?: FileNode[]
}
const initialCollection = createTreeCollection<FileNode>({
nodeChildren: "nodes",
nodeText: (node) => node.name,
nodeValue: (node) => node.id,
rootNode: {
id: "ROOT",
name: "",
nodes: [
{
id: "node_modules",
name: "node_modules",
nodes: [
{
id: "@qui",
name: "@qui",
nodes: [
{
id: "node_modules/@qualcomm-ui/core",
name: "@qualcomm-ui/core",
},
{
id: "node_modules/@qualcomm-ui/react",
name: "@qualcomm-ui/react",
},
{
id: "node_modules/@qualcomm-ui/react-core",
name: "@qualcomm-ui/react-core",
},
],
},
{
id: "node_modules/@types",
name: "@types",
nodes: [
{id: "node_modules/@types/react", name: "react"},
{id: "node_modules/@types/react-dom", name: "react-dom"},
],
},
],
},
{
id: "src",
name: "src",
nodes: [
{id: "src/app.tsx", name: "app.tsx"},
{id: "src/index.ts", name: "index.ts"},
],
},
{id: "prettier.config.js", name: "prettier.config.js"},
{id: "package.json", name: "package.json"},
{id: "tsconfig.json", name: "tsconfig.json"},
],
},
})
@Component({
imports: [TreeModule, IconDirective, TextInputModule, FormsModule],
providers: [provideIcons({FileText, FolderIcon, Search})],
selector: "tree-filtering-demo",
template: `
<div
class="w-full max-w-sm"
q-tree-root
[collection]="initialCollection"
[expandedValue]="expanded()"
(expandedValueChanged)="expanded.set($event.expandedValue)"
>
<q-text-input
aria-label="Search for files"
class="mb-1"
placeholder="Search for files: 'react'"
size="sm"
startIcon="Search"
[ngModel]="query()"
(ngModelChange)="search($event)"
/>
@for (
node of collection().rootNode.nodes;
let i = $index;
track collection().getNodeValue(node)
) {
<q-tree-nodes [indexPath]="[i]" [node]="node">
<ng-template
let-branch
q-tree-branch-template
[rootNode]="collection().rootNode"
>
<div q-tree-branch-node>
<div q-tree-node-indicator></div>
<div q-tree-branch-trigger></div>
<svg q-tree-node-icon qIcon="FolderIcon"></svg>
<span q-tree-node-text>{{ branch.node.name }}</span>
</div>
</ng-template>
<ng-template
let-leaf
q-tree-leaf-template
[rootNode]="collection().rootNode"
>
<div q-tree-leaf-node>
<div q-tree-node-indicator></div>
<svg q-tree-node-icon qIcon="FileText"></svg>
<span q-tree-node-text>{{ leaf.node.name }}</span>
</div>
</ng-template>
</q-tree-nodes>
}
</div>
`,
})
export class TreeFilteringDemo {
readonly initialCollection = initialCollection
readonly collection = signal<TreeCollection<FileNode>>(initialCollection)
readonly expanded = signal<string[]>([])
readonly query = signal("")
search(value: string) {
this.query.set(value)
if (!value) {
this.collection.set(initialCollection)
return
}
const nodes = matchSorter(initialCollection.getDescendantNodes(), value, {
keys: ["name"],
})
const nextCollection = initialCollection.filter((node) =>
nodes.some((n) => n.id === node.id),
)
this.collection.set(nextCollection)
this.expanded.set(nextCollection.getBranchValues())
}
}Links
Tree nodes can be links using Angular's RouterLink directive. Apply the q-tree-branch-node or q-tree-leaf-node directive to an anchor element with [routerLink].
@if (leaf.node.pathname) {
<a q-tree-leaf-node [routerLink]="leaf.node.pathname">
<div q-tree-node-indicator></div>
<span q-tree-node-text>{{ leaf.node.name }}</span>
</a>
} @else {
<div q-tree-leaf-node>
<div q-tree-node-indicator></div>
<span q-tree-node-text>{{ leaf.node.name }}</span>
</div>
}
Sizes
Tree item sizes are controlled using the size input on the root of the tree.
import {NgTemplateOutlet} from "@angular/common"
import {Component} from "@angular/core"
import {FileText, FolderIcon} from "lucide-angular"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {IconDirective} from "@qualcomm-ui/angular/icon"
import {TreeModule} from "@qualcomm-ui/angular/tree"
import {createTreeCollection} from "@qualcomm-ui/core/tree"
interface FileNode {
id: string
name: string
nodes?: FileNode[]
}
const collection = createTreeCollection<FileNode>({
nodeChildren: "nodes",
nodeText: (node) => node.name,
nodeValue: (node) => node.id,
rootNode: {
id: "ROOT",
name: "",
nodes: [
{
id: "node_modules",
name: "node_modules",
nodes: [
{
id: "@qui",
name: "@qui",
nodes: [
{
id: "node_modules/@qualcomm-ui/core",
name: "@qualcomm-ui/core",
},
{
id: "node_modules/@qualcomm-ui/react",
name: "@qualcomm-ui/react",
},
{
id: "node_modules/@qualcomm-ui/react-core",
name: "@qualcomm-ui/react-core",
},
],
},
{
id: "node_modules/@types",
name: "@types",
nodes: [
{id: "node_modules/@types/react", name: "react"},
{id: "node_modules/@types/react-dom", name: "react-dom"},
],
},
],
},
{
id: "src",
name: "src",
nodes: [
{id: "src/app.tsx", name: "app.tsx"},
{id: "src/index.ts", name: "index.ts"},
],
},
{id: "prettier.config.js", name: "prettier.config.js"},
{id: "package.json", name: "package.json"},
{id: "tsconfig.json", name: "tsconfig.json"},
],
},
})
@Component({
imports: [TreeModule, IconDirective, NgTemplateOutlet],
providers: [provideIcons({FileText, FolderIcon})],
selector: "tree-size-demo",
template: `
<div class="flex w-full flex-col gap-4">
<div
#root1="treeRoot"
class="w-full max-w-sm"
q-tree-root
size="sm"
[collection]="collection"
>
<span q-tree-label>Small (sm)</span>
<ng-container
[ngTemplateOutlet]="treeContent"
[ngTemplateOutletInjector]="root1.injector"
/>
</div>
<div
#root2="treeRoot"
class="w-full max-w-sm"
q-tree-root
size="md"
[collection]="collection"
>
<span q-tree-label>Medium (md)</span>
<ng-container
[ngTemplateOutlet]="treeContent"
[ngTemplateOutletInjector]="root2.injector"
/>
</div>
</div>
<ng-template #treeContent>
@for (
node of collection.rootNode.nodes;
let i = $index;
track collection.getNodeValue(node)
) {
<q-tree-nodes [indexPath]="[i]" [node]="node">
<ng-template
let-branch
q-tree-branch-template
[rootNode]="collection.rootNode"
>
<div q-tree-branch-node>
<div q-tree-node-indicator></div>
<div q-tree-branch-trigger></div>
<svg q-tree-node-icon qIcon="FolderIcon"></svg>
<span q-tree-node-text>{{ branch.node.name }}</span>
</div>
</ng-template>
<ng-template
let-leaf
q-tree-leaf-template
[rootNode]="collection.rootNode"
>
<div q-tree-leaf-node>
<div q-tree-node-indicator></div>
<svg q-tree-node-icon qIcon="FileText"></svg>
<span q-tree-node-text>{{ leaf.node.name }}</span>
</div>
</ng-template>
</q-tree-nodes>
}
</ng-template>
`,
})
export class TreeSizeDemo {
collection = collection
}Add / Remove nodes
The TreeCollection class exposes methods to handle the addition and removal of nodes. Here's an example of how to use them.
import {Component, input, output, signal} from "@angular/core"
import {FileText, FolderIcon, Plus, Trash} from "lucide-angular"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {useTreeContext} from "@qualcomm-ui/angular-core/tree"
import {IconDirective} from "@qualcomm-ui/angular/icon"
import {TreeModule} from "@qualcomm-ui/angular/tree"
import {createTreeCollection} from "@qualcomm-ui/core/tree"
import type {TreeCollection} from "@qualcomm-ui/utils/collection"
interface FileNode {
id: string
name: string
nodes?: FileNode[]
}
@Component({
imports: [TreeModule],
providers: [provideIcons({Plus, Trash})],
selector: "tree-node-actions",
template: `
<button
aria-label="Remove node"
icon="Trash"
q-tree-node-action
size="sm"
(click)="onRemove()"
></button>
@if (isBranch()) {
<button
aria-label="Add node"
icon="Plus"
q-tree-node-action
size="sm"
(click)="onAdd()"
></button>
}
`,
})
export class TreeNodeActions {
readonly node = input.required<FileNode>()
readonly indexPath = input.required<number[]>()
readonly isBranch = input(false)
readonly add = output<{indexPath: number[]; node: FileNode}>()
readonly remove = output<{indexPath: number[]; node: FileNode}>()
protected readonly treeContext = useTreeContext()
onRemove() {
this.remove.emit({indexPath: this.indexPath(), node: this.node()})
}
onAdd() {
this.treeContext().expand([this.node().id])
this.add.emit({indexPath: this.indexPath(), node: this.node()})
}
}
const initialCollection = createTreeCollection<FileNode>({
nodeChildren: "nodes",
nodeText: "name",
nodeValue: "id",
rootNode: {
id: "ROOT",
name: "",
nodes: [
{
id: "node_modules",
name: "node_modules",
nodes: [
{
id: "@qui",
name: "@qui",
nodes: [
{
id: "node_modules/@qualcomm-ui/core",
name: "@qualcomm-ui/core",
},
{
id: "node_modules/@qualcomm-ui/react",
name: "@qualcomm-ui/react",
},
{
id: "node_modules/@qualcomm-ui/react-core",
name: "@qualcomm-ui/react-core",
},
],
},
{
id: "node_modules/@types",
name: "@types",
nodes: [
{id: "node_modules/@types/react", name: "react"},
{id: "node_modules/@types/react-dom", name: "react-dom"},
],
},
],
},
{
id: "src",
name: "src",
nodes: [
{id: "src/app.tsx", name: "app.tsx"},
{id: "src/index.ts", name: "index.ts"},
],
},
{id: "prettier.config.js", name: "prettier.config.js"},
{id: "package.json", name: "package.json"},
{id: "tsconfig.json", name: "tsconfig.json"},
],
},
})
@Component({
imports: [TreeModule, IconDirective, TreeNodeActions],
providers: [provideIcons({FileText, FolderIcon, Plus, Trash})],
selector: "tree-add-remove-demo",
template: `
<div class="w-full max-w-sm" q-tree-root [collection]="collection()">
@for (
node of collection().rootNode.nodes;
let i = $index;
track collection().getNodeValue(node)
) {
<q-tree-nodes [indexPath]="[i]" [node]="node">
<ng-template
let-branch
q-tree-branch-template
[rootNode]="collection().rootNode"
>
<div q-tree-branch-node role="treeitem">
<div q-tree-node-indicator></div>
<div q-tree-branch-trigger></div>
<svg q-tree-node-icon qIcon="FolderIcon"></svg>
<span q-tree-node-text>
{{ collection().stringifyNode(branch.node) }}
</span>
<tree-node-actions
[indexPath]="branch.indexPath"
[isBranch]="true"
[node]="branch.node"
(add)="addNode($event)"
(remove)="removeNode($event)"
/>
</div>
</ng-template>
<ng-template
let-leaf
q-tree-leaf-template
[rootNode]="collection().rootNode"
>
<div q-tree-leaf-node>
<div q-tree-node-indicator></div>
<svg q-tree-node-icon qIcon="FileText"></svg>
<span q-tree-node-text>
{{ collection().stringifyNode(leaf.node) }}
</span>
<tree-node-actions
[indexPath]="leaf.indexPath"
[isBranch]="false"
[node]="leaf.node"
(remove)="removeNode($event)"
/>
</div>
</ng-template>
</q-tree-nodes>
}
</div>
`,
})
export class TreeAddRemoveDemo {
readonly collection = signal<TreeCollection<FileNode>>(initialCollection)
removeNode(event: {indexPath: number[]; node: FileNode}) {
this.collection.update((c) => c.remove([event.indexPath]))
}
addNode(event: {indexPath: number[]; node: FileNode}) {
const {indexPath, node} = event
if (!this.collection().isBranchNode(node)) {
return
}
const nodes = [
{
id: `untitled-${Date.now()}`,
name: `untitled-${node.nodes?.length || 0}.tsx`,
},
...(node.nodes || []),
]
this.collection.update((c) => c.replace(indexPath, {...node, nodes}))
}
}Explorer
Component Anatomy
Hover to highlight, click to view API
API
q-tree-root
string[]
string[]
string[]
stringstring[]
'ltr' | 'rtl'
string[]
booleanstring() =>
| Node
| ShadowRoot
| Document
stringboolean(details: {
indexPath: number[]
node: T
signal: AbortSignal
valuePath: string[]
}) => Promise<any[]>
string[]
| 'multiple'
| 'single'
InputSigna'sm' | 'md'
booleanboolean{
checkedNodes: Array<T>
checkedValue: string[]
}
{
expandedNodes: Array<T>
expandedValue: string[]
focusedValue: string
}
{
focusedNode: T
focusedValue: string
}
{
collection: TreeCollection<T>
}
{
nodes: NodeWithError[]
}
{
focusedValue: string
selectedNodes: Array<T>
selectedValue: string[]
}
class'qui-tree__root'data-size'sm' | 'md'
data-tree-part'root'tabIndex-1
q-tree-nodes
number[]
TTemplateRef<any>
TemplateRef<any>
booleanTreeCollection
Note that the TreeCollection accepts a single generic type parameter, T, which is the object type of the node used in the collection.
Constructor
The constructor of the TreeCollection class accepts the following options:
keyof T
(
node: T,
) => number
| keyof T
| ((node: T) => boolean)
| keyof T
| ((node: T) => string)
| keyof T
| ((node: T) => string)
T(
indexPath: number[],
) => T
- indexPath:Array of indices representing the path to the node
(
parentIndexPath: number[],
valueIndexPath: number[],
) => boolean
- parentIndexPath:The parent path
- valueIndexPath:The child path to check
(
rootNode: T,
) => any
- rootNode:The new root node for the copied collection
(
predicate: (
node: T,
indexPath: number[],
) => boolean,
) => any
- predicate:Function to test each node
(
value: string,
rootNode?: T,
) => T
- value:The value to search for
- rootNode:The root node to start searching from
(
predicate: (
node: T,
indexPath: number[],
) => boolean,
rootNode?: T,
) => T
(
values: string[],
rootNode?: T,
) => Array<T>
- values:Array of values to search for
- rootNode:The root node to start searching from
(
rootNode?: T,
) => Array<
T & {
_children: number[]
_index: number
_parent: number
}
>
- rootNode:The root node to start flattening from
(
rootNode?: T,
opts?: {
skip?: (args: {
indexPath: number[]
node: T
value: string
}) => boolean | void
} & {
depth?:
| number
| ((
nodeDepth: number,
) => boolean)
},
) => string[]
- rootNode:The root node to start from
- opts:Options for skipping nodes and filtering by depth
(
value: string,
) => number
- value:The value to find the depth for
(
valueOrIndexPath?:
| string
| number[],
options?: T & {
disabled?: boolean
id?: string
onUnregister?: (
index: number,
) => void
requireContext?: boolean
},
) => Array<T>
- valueOrIndexPath:Either a node value or index path
- options:Options for controlling which descendants to include
(
valueOrIndexPath:
| string
| number[],
options?: T & {
disabled?: boolean
id?: string
onUnregister?: (
index: number,
) => void
requireContext?: boolean
},
) => string[]
- valueOrIndexPath:Either a node value or index path
- options:Options for controlling which descendants to include
(
rootNode?: T,
) => T
- rootNode:The root node to start searching from
(
value: string,
) => number[]
- value:The value to find the index path for
(
rootNode?: T,
opts?: {
skip?: (args: {
indexPath: number[]
node: T
value: string
}) => boolean | void
},
) => T
- rootNode:The root node to start searching from
- opts:Options for skipping nodes during traversal
(
value: string,
opts?: {
skip?: (args: {
indexPath: number[]
node: T
value: string
}) => boolean | void
},
) => T
- value:The value to find the next node from
- opts:Options for skipping nodes during traversal
(
indexPath: number[],
) => T
- indexPath:Array of indices representing the path to the node
(
node: T,
) => Array<T>
(
node: T,
) => number
- node:The node to get children count for
(
node: T,
) => boolean
- node:The node to check
(
node: T,
) => string
- node:The node to get the value from
(
valueOrIndexPath:
| string
| number[],
) => T
- valueOrIndexPath:Either a node value or index path
(
valueOrIndexPath:
| string
| number[],
) => Array<T>
- valueOrIndexPath:Either a node value or index path
(
value: string,
opts?: {
skip?: (args: {
indexPath: number[]
node: T
value: string
}) => boolean | void
},
) => T
- value:The value to find the previous node from
- opts:Options for skipping nodes during traversal
(
indexPath: number[],
) => T
- indexPath:Array of indices representing the path to the node
(
indexPath: number[],
) => Array<T>
- indexPath:Array of indices representing the path to the node
(
indexPath: number[],
) => string
- indexPath:Array of indices representing the path to the node
(
indexPath: number[],
) => string[]
- indexPath:Array of indices representing the path to the node
(
rootNode?: T,
) => string[]
- rootNode:The root node to start from
(
parentIndexPath: IndexPath,
groupBy: (
node: T,
index: number,
) => string,
sortGroups?:
| string[]
| ((
a: {
items: Array<{
indexPath: IndexPath
node: T
}>
key: string
},
b: {
items: Array<{
indexPath: IndexPath
node: T
}>
key: string
},
) => number),
) => GroupedTreeNode<T>[]
- parentIndexPath:Index path of the parent node whose children to group. Pass
[]for root-level children. - groupBy:Function that determines the group key for each child node
- sortGroups:Optional array of group keys defining order, or comparator function to sort the groups. By default, groups are sorted by first occurrence in the tree (insertion order)
// Group root-level children
const groups = collection.groupChildren([], (node) => node.group ?? 'default')
// Group with explicit order
const groups = collection.groupChildren(
[],
(node) => node.group,
['primary', 'secondary', 'tertiary']
)
// Group with custom sorter
const groups = collection.groupChildren(
[],
(node) => node.group,
(a, b) => String(a.key).localeCompare(String(b.key))
)
(
indexPath: number[],
nodes: Array<T>,
) => any
- indexPath:Array of indices representing the insertion point
- nodes:Array of nodes to insert
(
indexPath: number[],
nodes: Array<T>,
) => any
- indexPath:Array of indices representing the insertion point
- nodes:Array of nodes to insert
(
node: T,
) => boolean
- node:The node to check
(
other: TreeCollection<T>,
) => boolean
- other:The other tree collection to compare with
(
node: T,
) => boolean
- node:The node to check
(
node: T,
other: T,
) => boolean
- node:First node to compare
- other:Second node to compare
(
fromIndexPaths: Array<
number[]
>,
toIndexPath: number[],
) => any
- fromIndexPaths:Array of index paths to move from
- toIndexPath:Index path to move to
(
indexPaths: Array<number[]>,
) => any
- indexPaths:Array of index paths to remove
(
indexPath: number[],
node: T,
) => any
- indexPath:Array of indices representing the path to the node
- node:The new node to replace with
(
indexPath: number[],
children: Array<T>,
) => any
- indexPath:Array of indices representing the path to the node
- children:Array of child nodes to set on the target node
T(
values: string[],
) => string[]
- values:Array of values to sort
(
value: string,
) => string
(
T,
) => string
node.text, or node.value if node.text is not available.() => string[]
(opts: {
onEnter?: (
node: T,
indexPath: number[],
) => void | 'skip' | 'stop'
onLeave?: (
node: T,
indexPath: number[],
) => void | 'stop'
reuseIndexPath?: boolean
skip?: (args: {
indexPath: number[]
node: T
value: string
}) => boolean | void
}) => void
- opts:Options for visiting nodes, including skip predicate
Element API
q-tree-label
stringclass'qui-tree__label'data-size'sm' | 'md'
data-tree-part'label'q-tree-branch
class'qui-tree__branch-root'data-branchstringdata-depthnumberdata-disableddata-loadingdata-ownedbystringdata-pathstringdata-selecteddata-state| 'open'
| 'closed'
data-tree-part'branch'data-valuestringhiddenbooleanstyleq-tree-branch-node
class'qui-tree__node-root'data-depthnumberdata-disableddata-focusdata-loadingdata-pathstringdata-selecteddata-size'sm' | 'md'
data-state| 'open'
| 'closed'
data-tree-part'branch-node'data-valuestringtabIndex-1 | 0
q-tree-branch-trigger
| LucideIconData
| string
class'qui-tree__branch-trigger'data-disableddata-loadingdata-state| 'open'
| 'closed'
data-tree-part'branch-trigger'data-valuestringq-tree-branch-content
stringclass'qui-tree__branch-content'data-depthnumberdata-pathstringdata-tree-part'branch-content'data-valuestringq-tree-branch-indent-guide
q-tree-leaf-node
class'qui-tree__node-root'data-depthnumberdata-disableddata-focusdata-ownedbystringdata-pathstringdata-selecteddata-size'sm' | 'md'
data-tree-part'leaf-node'data-valuestringhiddenbooleanstyletabIndex-1 | 0
q-tree-node-checkbox
class'qui-checkbox__control qui-checkmark__root'data-checkmark-part'root'data-disableddata-state| 'checked'
| 'indeterminate'
| 'unchecked'
data-tree-part'node-checkbox'tabIndex-1
q-tree-node-action
class'qui-tree__node-action'data-disableddata-focusdata-selecteddata-tree-part'node-action'q-tree-node-indicator
class'qui-tree__node-indicator'data-disableddata-focusdata-selecteddata-tree-part'node-indicator'hiddenbooleanq-tree-node-icon
| LucideIconData
| string
class'qui-tree__node-icon'data-disableddata-focusdata-selecteddata-size'sm' | 'md'
data-tree-part'node-icon'q-tree-node-text
QdsTreeNodeTextBindingsTemplate Directives
q-tree-branch-template
Tq-tree-leaf-template
ng-template to customize how leaf nodes (nodes without
children) are displayed.<ng-template q-tree-leaf-template let-node>
<div q-tree-leaf-node>
<span q-tree-node-text>{{ node.item.label }}</span>
</div>
</ng-template>
TUse this directive on an ng-template to customize the rendering of leaf nodes within q-tree-nodes.
ng-templateto customize how branch nodes (nodes with children) are displayed. Note that this template will only customize the content of the node. The parent<q-tree-nodes>component renders the branch children internally.