# Tree

- Entry point: `@koobiq/components/tree`
- NgModule: `KbqTreeModule`
- Page: https://koobiq.io/en/components/tree/overview

## Overview

A hierarchical list (tree) is a tree-structured catalog for working with large amounts of data.

**Example: Tree** (`tree-overview`)

```ts tree-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'components-dev': {
            alert: '',
            badge: ''
        },
        'docs-examples': '',
        'angular-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

@Component({
    selector: 'tree-overview-example',
    imports: [
        KbqTreeModule,
        FormsModule
    ],
    template: `
        <kbq-tree-selection [dataSource]="dataSource" [treeControl]="treeControl" [(ngModel)]="modelValue">
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <kbq-tree-node-toggle [node]="node" />

                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeOverviewExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = '';

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };
}
```

### When to use

When you need to provide the user with convenient access to a large amount of data (hundreds of items). Either the user can organize this data into a tree-structured catalog, or the data already arrives in this form from the system. If these conditions are not met, it is better to use a [regular list](https://koobiq.io/en/components/list.md).

### Component structure

- Name
- Item icons _(optional)_
- Additional action button for the item _(optional)_
- Expand and Collapse buttons
- Checkbox _(optional)_
- Search through items _(optional)_

### How it works

#### List with tree and text search

If a search query is entered, only the following items are shown in the tree:

- items that match the search query;
- items that are parents of items found by the search query.

Text matching the search query in the item name is highlighted in bold.

**Example: Tree filtering** (`tree-filtering`)

```ts tree-filtering-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqHighlightModule } from '@koobiq/components/core';
import { KbqInputModule } from '@koobiq/components/input';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'components-dev': {
            alert: '',
            badge: ''
        },
        'koobiq-examples': '',
        'koobiq-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

@Component({
    selector: 'tree-filtering-example',
    imports: [KbqInputModule, FormsModule, KbqTreeModule, KbqHighlightModule],
    template: `
        <kbq-form-field>
            <input kbqInput type="text" [(ngModel)]="filterValue" (ngModelChange)="onFilterChange($event)" />
        </kbq-form-field>
        <kbq-tree-selection
            class="layout-margin-top-4xl"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <kbq-tree-node-toggle [node]="node" />

                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeFilteringExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = '';
    filterValue: string = '';

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    onFilterChange(value: string): void {
        this.treeControl.filterNodes(value);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };
}
```

#### List with tree and filtering of selected items

**Example: Tree checked filtering** (`tree-checked-filtering`)

```ts tree-checked-filtering-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonToggleChange, KbqButtonToggleModule } from '@koobiq/components/button-toggle';
import { KbqHighlightModule } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import {
    FilterByValues,
    FilterByViewValue,
    FilterParentsForNodes,
    FlatTreeControl,
    KbqTreeFlatDataSource,
    KbqTreeFlattener,
    KbqTreeModule,
    KbqTreeSelection
} from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'mosaic-dev': {
            alert: '',
            badge: ''
        },
        'mosaic-examples': '',
        'mosaic-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

abstract class TreeParams {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;
    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;
    treeData: FileNode[];
    tree: KbqTreeSelection;

    constructor() {
        this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable, this.getValue, this.getViewValue);
        this.treeFlattener = new KbqTreeFlattener<FileNode, FileFlatNode>(
            this.transformer,
            this.getLevel,
            this.isExpandable,
            this.getChildren
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);
        this.dataSource.data = this.treeData = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    getLevel = (node: FileFlatNode) => node.level;

    getValue = (node: FileFlatNode) => node.name;

    getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };

    isExpandable = (node: FileFlatNode) => node.expandable;

    getChildren = (node: FileNode) => node.children;

    transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children?.length;

        return flatNode;
    };
}

@Component({
    selector: 'tree-checked-filtering-example',
    imports: [
        FormsModule,
        KbqButtonToggleModule,
        KbqTreeModule,
        KbqHighlightModule,
        KbqIconModule,
        KbqInputModule
    ],
    template: `
        <kbq-form-field>
            <input kbqInput type="text" [(ngModel)]="filterValue" (ngModelChange)="onFilterChange($event)" />
        </kbq-form-field>
        <div class="layout-margin-top-4xl">
            <kbq-button-toggle-group #group1="kbqButtonToggleGroup" (change)="onToggleClick($event)">
                <kbq-button-toggle [checked]="true" [value]="treeStates.ALL">
                    <i kbq-icon="kbq-bell_16"></i>
                    All
                </kbq-button-toggle>
                <kbq-button-toggle [value]="treeStates.SELECTED">
                    <i kbq-icon="kbq-bell_16"></i>
                    Selected
                </kbq-button-toggle>
                <kbq-button-toggle [value]="treeStates.UNSELECTED">
                    <i kbq-icon="kbq-bell_16"></i>
                    Unselected
                </kbq-button-toggle>
            </kbq-button-toggle-group>
        </div>
        <kbq-tree-selection
            multiple="checkbox"
            [autoSelect]="false"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
            (ngModelChange)="onModelValueChange($event)"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <kbq-tree-node-toggle [node]="node" />

                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeCheckedFilteringExample extends TreeParams {
    treeControl: FlatTreeControl<FileFlatNode>;
    filterByValues: FilterByValues<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = ['docs'];
    filterValue: string = '';

    readonly treeStates = {
        ALL: 0,
        SELECTED: 1,
        UNSELECTED: 2
    };

    constructor() {
        super();
        this.filterByValues = new FilterByValues<FileFlatNode>(this.treeControl);
        this.filterByValues.setValues(this.modelValue);

        this.treeControl.setFilters(
            new FilterByViewValue<FileFlatNode>(this.treeControl),
            this.filterByValues,
            new FilterParentsForNodes<FileFlatNode>(this.treeControl)
        );
    }

    onModelValueChange(values) {
        this.filterByValues.setValues(values);
    }

    onFilterChange(value: string): void {
        this.treeControl.filterNodes(value);
    }

    onToggleClick({ value }: KbqButtonToggleChange) {
        if (value === this.treeStates.ALL) {
            this.filterByValues.setValues([]);
        } else if (value === this.treeStates.SELECTED) {
            this.filterByValues.setValues(this.modelValue);
        } else if (value === this.treeStates.UNSELECTED) {
            const values = this.treeControl.dataNodes
                .filter((node) => !this.modelValue.includes(this.treeControl.getValue(node)))
                .map((node) => this.treeControl.getValue(node));

            this.filterByValues.setValues(values);
        }

        this.treeControl.filterNodes(this.filterValue);
    }
}
```

#### List with tree and multiple item selection

Either single or multiple selection is possible (limited at the tree level).

An item can be selected by clicking on it (in any area except the Expand/Collapse icon and the additional action button). To select multiple items with the mouse, use the Ctrl or Shift keys; to select multiple items with the keyboard, use the Space key.

Non-leaf items can be expanded and collapsed by clicking the arrow icon.

**Example: Tree multiple keyboard** (`tree-multiple-keyboard`)

```ts tree-multiple-keyboard-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'components-dev': {
            alert: '',
            badge: ''
        },
        'koobiq-examples': '',
        'koobiq-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

@Component({
    selector: 'tree-multiple-keyboard-example',
    imports: [
        KbqTreeModule,
        FormsModule
    ],
    template: `
        <kbq-tree-selection
            multiple="keyboard"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <kbq-tree-node-toggle [node]="node" />

                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeMultipleKeyboardExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = [];

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };
}
```

If checkboxes are enabled in the tree list, selecting multiple items is possible by clicking the checkbox.

**Example: Tree multiple checkbox** (`tree-multiple-checkbox`)

```ts tree-multiple-checkbox-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqIconModule } from '@koobiq/components/icon';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'components-dev': {
            alert: '',
            badge: ''
        },
        'koobiq-examples': '',
        'koobiq-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

@Component({
    selector: 'tree-multiple-checkbox-example',
    imports: [
        KbqTreeModule,
        FormsModule,
        KbqIconModule
    ],
    template: `
        <kbq-tree-selection
            multiple="checkbox"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <i kbq-icon="kbq-circle-info_16"></i>

                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <i kbq-icon="kbq-circle-info_16"></i>

                <kbq-tree-node-toggle [node]="node" />

                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeMultipleCheckboxExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = [];

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };
}
```

The mode is set by `multiple`: `checkbox` or `keyboard` for multiple selection. Single selection is the
default, so the way to ask for it is to leave the attribute off entirely. It is a normal input, so `[multiple]` can be bound and changed at any time. Narrowing a
tree that has several nodes selected keeps the first selected one and reports the shortened value; the
reported value is a bare value in single selection and an array in multiple selection, so a change of mode
changes the shape the form control holds. A tree rendered inside a `kbq-tree-select` shares its selection
model with the select, so the select decides how many nodes may be selected — switching the tree between
single and multiple selection there throws, while switching between `checkbox` and `keyboard` is allowed.

Note that this applies to `kbq-tree-selection` and `kbq-list-selection` only. On
[Select](https://koobiq.io/en/components/select.md) and `kbq-tree-select`, `multiple` still cannot be changed after
initialization and throws if you try.

#### Additional action button

Sometimes an additional action button may be present for an item.

**Example: Tree action button** (`tree-action-button`)

```ts tree-action-button-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqBadgeModule } from '@koobiq/components/badge';
import { KbqOptionModule, PopUpPlacements } from '@koobiq/components/core';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqToolTipModule } from '@koobiq/components/tooltip';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'components-dev': {
            alert: '',
            badge: ''
        },
        'koobiq-examples': '',
        'koobiq-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

@Component({
    selector: 'tree-action-button-example',
    imports: [
        KbqTreeModule,
        FormsModule,
        KbqToolTipModule,
        KbqDropdownModule,
        KbqIconModule,
        KbqOptionModule,
        KbqBadgeModule
    ],
    template: `
        <kbq-tree-selection
            style="width: 400px"
            [autoSelect]="false"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
            (selectAllChange)="onSelectAllChange($event)"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <i kbq-icon="kbq-circle-info_16"></i>

                <div class="layout-row layout-align-space-between-center">
                    <span [innerHTML]="treeControl.getViewValue(node)"></span>
                    <kbq-badge badgeColor="theme" [compact]="true">badge</kbq-badge>
                </div>

                <kbq-option-action
                    [kbqDropdownTriggerFor]="dropdown"
                    [kbqPlacement]="popUpPlacements.Right"
                    [kbqTooltip]="'Tooltip text'"
                />
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <i kbq-icon="kbq-circle-info_16"></i>

                <kbq-tree-node-toggle [node]="node" />

                <div class="layout-row layout-align-space-between-center">
                    <span [innerHTML]="treeControl.getViewValue(node)"></span>
                    <kbq-badge badgeColor="theme" [compact]="true">badge</kbq-badge>
                </div>

                <kbq-option-action
                    [kbqDropdownTriggerFor]="dropdown"
                    [kbqPlacement]="popUpPlacements.Right"
                    [kbqTooltip]="'Tooltip text'"
                />
            </kbq-tree-option>
        </kbq-tree-selection>

        <kbq-dropdown #dropdown>
            <button kbq-dropdown-item>action 1</button>
            <button kbq-dropdown-item>action 2</button>
            <button kbq-dropdown-item>action 3</button>
        </kbq-dropdown>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeActionButtonExample {
    popUpPlacements = PopUpPlacements;

    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = '';

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    onSelectAllChange($event) {
        console.log('All items selected', $event);
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };
}
```

#### Caption in options

**Example: tree-select-two-line-option** (`tree-select-two-line-option`)

```ts tree-select-two-line-option-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqIconModule } from '@koobiq/components/icon';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';
import { KbqTreeSelectModule } from '@koobiq/components/tree-select';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'components-dev': {
            alert: '',
            badge: ''
        },
        'koobiq-examples': '',
        'koobiq-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

@Component({
    selector: 'tree-select-two-line-option-example',
    imports: [FormsModule, KbqTreeModule, KbqTreeSelectModule, KbqIconModule],
    template: `
        <kbq-form-field>
            <kbq-tree-select multiple [(ngModel)]="selected">
                <kbq-tree-selection [dataSource]="dataSource" [treeControl]="treeControl">
                    <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding>
                        <div>{{ treeControl.getViewValue(node) }}</div>
                        <div class="kbq-option-caption">caption</div>
                    </kbq-tree-option>

                    <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                        <kbq-tree-node-toggle [node]="node" />
                        <div>{{ treeControl.getViewValue(node) }}</div>
                        <div class="kbq-option-caption">caption</div>
                    </kbq-tree-option>
                </kbq-tree-selection>
            </kbq-tree-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeSelectTwoLineOptionExample {
    selected = ['docs'];

    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        return `${node.name} view`;
    };
}
```

### Multiple selection variants

There are several variants for multiple item selection. [See in examples](https://koobiq.io/en/components/tree.md).

### State Saving

The tree remembers which nodes were expanded and restores the state after a page reload. On by default — use `[useStateSaving]="false"` to turn it off on a specific component.

**Example: Tree state saving** (`tree-state-saving`)

```ts tree-state-saving-example.ts
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import {
    FlatTreeControl,
    KbqTreeFlatDataSource,
    KbqTreeFlattener,
    KbqTreeModule,
    KbqTreeSelection
} from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    Documents: {
        'Delivery terms': 'pdf',
        'Payment terms': 'pdf'
    },
    Pictures: {
        Sun: 'png',
        Woods: 'jpg'
    },
    Downloads: {
        November: 'pdf',
        October: 'pdf'
    }
};

@Component({
    selector: 'tree-state-saving-example',
    imports: [
        KbqTreeModule,
        KbqButtonModule
    ],
    template: `
        <div class="layout-column layout-align-center-center docs-example-vertical-margin">
            <kbq-tree-selection
                stateSavingKey="tree-state-saving-example"
                style="width: 300px"
                [dataSource]="dataSource"
                [treeControl]="treeControl"
            >
                <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding>
                    {{ treeControl.getViewValue(node) }}
                </kbq-tree-option>

                <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                    <kbq-tree-node-toggle [node]="node" />
                    {{ treeControl.getViewValue(node) }}
                </kbq-tree-option>
            </kbq-tree-selection>

            <button kbq-button class="layout-margin-top-l" type="button" (click)="tree().clearSavedState()">
                Reset saved state
            </button>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeStateSavingExample {
    protected readonly tree = viewChild.required(KbqTreeSelection);

    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    // The value expansion is persisted under. It must be stable across reloads, and unique within the
    // tree — a node object is re-created whenever the data is replaced, so it cannot serve as the key.
    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };
}
```

Selection is not persisted: it belongs to the form control the tree is bound to.

Expansion is persisted by the value `getValue` returns for a node, so that value has to be a string, stable across reloads and unique within the tree. A tree on a `NestedTreeControl` has no `getValue` and persists nothing, and neither does one rendered into an overlay — `kbq-tree-select` among them.

Expansion the application performs itself — `treeControl.expandAll()`, or writing to `expansionModel` directly — is not persisted until `saveState()` is called. `clearSavedState()` removes what is stored.

Keys, storage and expiry work the same for every component that persists — see [Saving component state](https://koobiq.io/en/components/core.md#saving-component-state).

### Focus and keyboard navigation

| Key                   | Action                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tab                   | If focus is on the component preceding the tree in tab order → Move to the first tree row or the selected row If focus is on any tree item and it has no additional actions → Move to the next component in tab order after the tree If focus is on any tree item and it has additional actions → Move to the first additional action If focus is on an item's additional action → Move to the next additional action (if one exists), otherwise move to the next component in tab order after the tree |
| Shift + Tab           | If focus is on any tree item → Move to the previous component in tab order before the tree If focus is on the component following the tree in tab order → Move to the first tree row or the selected row                                                                                                                                                                                                                                                                                                |
| →                     | If focus is on a collapsed non-leaf item → Expand the nested items of that item If focus is on an expanded non-leaf item → Move to its first nested item If focus is on a leaf item → Nothing happens                                                                                                                                                                                                                                                                                                   |
| ←                     | If focus is on an expanded non-leaf item → Collapse the nested items of that item If focus is on a collapsed non-leaf item → Move to its parent item If focus is on a leaf item → Move to its parent item If focus is on a top-level item → Nothing happens                                                                                                                                                                                                                                             |
| ↵                     | If focus is on a tree item → Trigger the primary action of that item If focus is on a tree item with a checkbox → Nothing happens If focus is on an additional action → Apply that additional action                                                                                                                                                                                                                                                                                                    |
| Space                 | If focus is on a tree item with a checkbox → Select that tree item (and all nested items, if any) If focus is on a tree item → Nothing happens If focus is on an additional action → Apply that additional action                                                                                                                                                                                                                                                                                       |
| ↑ ↓                   | If focus is on a tree item → Move to the next (if it is the last item — nothing happens) / previous tree item (if it is the first item — nothing happens)                                                                                                                                                                                                                                                                                                                                               |
| PgUp                  | Move focus to the lowest visible item If the current item is the lowest visible → Scroll the tree down If the current item is the last in the tree → Nothing happens                                                                                                                                                                                                                                                                                                                                    |
| PgDn                  | Move focus to the topmost visible item If the current item is the topmost visible → Scroll the tree up If the current item is the topmost in the tree → Nothing happens                                                                                                                                                                                                                                                                                                                                 |
| Home                  | Move focus to the first tree item                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| End                   | Move focus to the last loaded tree item                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Ctrl + click          | Clicking tree items — multiple row selection Clicking a selected item — the item is deselected                                                                                                                                                                                                                                                                                                                                                                                                          |
| Shift + ↓   Shift + ↑ | If focus is on a tree item → Multiple selection of the next / previous tree items (if the row is the first or last — nothing happens)                                                                                                                                                                                                                                                                                                                                                                   |
| Ctrl + C              | If focus is on tree items → Copy the content of the items By default, item names are copied. The separator between items is a line break                                                                                                                                                                                                                                                                                                                                                                |
| Ctrl + A              | If focus is on any tree item → Select all tree items (including unloaded ones)                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Printable characters  | Move focus to the next item whose name starts with the typed characters. Typing several characters in quick succession matches them as one word                                                                                                                                                                                                                                                                                                                                                         |

### Accessibility

The tree exposes the structure it renders to assistive technology, so a screen reader announces an
item's nesting depth and whether it is expanded, selected or checked.

- The tree is a `role="tree"`, each row a `role="treeitem"`.
- The tab stop stays on the tree itself and the rows stay at `tabindex="-1"`, but the row the arrow
  keys land on does take real focus, and is advertised through `aria-activedescendant` on top of that.
- Branch rows carry `aria-expanded` — including while a filter is active, which disables the chevrons
  without turning the branches into leaves — and every row carries `aria-level`.
- Rows with a checkbox report `aria-checked`, including `mixed` for a partially selected branch; rows
  without one report `aria-selected`.
- Expand/collapse chevrons are hidden from assistive technology: their state is already on the row, and
  the ← and → keys drive them.
- The tree has no visible label of its own, so give it an accessible name with `aria-label`, or point
  `aria-labelledby` at the element that labels it. A tree inside a `kbq-tree-select` is labelled by the
  select and needs neither.

### Rendering

Every expanded item is rendered, with no windowing — a collapsed branch costs nothing, an expanded one
costs a DOM row per item. A tree that shows thousands of rows at once should keep its branches
collapsed or narrow the data with the search field.

Nodes are tracked by identity by default, so a data source that rebuilds its node objects on every
emission — a flattener among them — rebuilds every row, and a rebuilt node comes back collapsed.
Give the tree control a `trackBy` that returns a stable key of a node: the node is then expanded by that
key, and the tree re-uses the row of the same key across data updates. Set it before anything is
expanded, as a property or in the options, the last argument of `FlatTreeControl` and
`NestedTreeControl`:

**Example: Tree track by** (`tree-track-by`)

```ts tree-track-by-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

type FileTree = { [name: string]: FileTree | string };

class FileNode {
    name: string;
    type?: string;
    children?: FileNode[];
}

class FileFlatNode {
    id: string;
    name: string;
    type?: string;
    level: number;
    expandable: boolean;
}

function buildFileTree(value: FileTree): FileNode[] {
    return Object.keys(value).map((name) => {
        const node = new FileNode();
        const content = value[name];

        node.name = name;

        if (typeof content === 'object') {
            node.children = buildFileTree(content);
        } else {
            node.type = content;
        }

        return node;
    });
}

const DOCUMENTS: FileTree = {
    'Delivery terms': 'pdf',
    'Payment terms': 'pdf'
};

const DATA_OBJECT: FileTree = {
    Documents: DOCUMENTS,
    Pictures: {
        Sun: 'png',
        Woods: 'jpg'
    },
    Downloads: {
        November: 'pdf',
        October: 'pdf'
    }
};

@Component({
    selector: 'tree-track-by-example',
    imports: [
        KbqTreeModule,
        KbqButtonModule
    ],
    template: `
        <div class="layout-column layout-align-center-center docs-example-vertical-margin">
            <kbq-tree-selection style="width: 300px" [dataSource]="dataSource" [treeControl]="treeControl">
                <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding>
                    {{ treeControl.getViewValue(node) }}
                </kbq-tree-option>

                <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                    <kbq-tree-node-toggle [node]="node" />
                    {{ treeControl.getViewValue(node) }}
                </kbq-tree-option>
            </kbq-tree-selection>

            <button kbq-button class="layout-margin-top-l" type="button" (click)="addDraft()">
                Add a draft to Documents
            </button>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeTrackByExample {
    protected readonly treeControl = new FlatTreeControl<FileFlatNode, string>(
        (node) => node.level,
        (node) => node.expandable,
        (node) => node.id,
        (node) => (node.type ? `${node.name}.${node.type}` : node.name)
    );

    // Every update flattens the data into new node objects: the expanded ones are found by their key.
    protected readonly dataSource = new KbqTreeFlatDataSource(
        this.treeControl,
        new KbqTreeFlattener(
            this.transformer,
            (node) => node.level,
            (node) => node.expandable,
            (node) => node.children
        ),
        buildFileTree(DATA_OBJECT)
    );

    private readonly drafts: FileTree = {};

    constructor() {
        // Before anything is expanded: a key is matched only under the function that stored it.
        this.treeControl.trackBy = (node) => node.id;
    }

    protected hasChild(_: number, node: FileFlatNode): boolean {
        return node.expandable;
    }

    protected addDraft(): void {
        this.drafts[`Draft ${Object.keys(this.drafts).length + 1}`] = 'docx';

        this.dataSource.data = buildFileTree({ ...DATA_OBJECT, Documents: { ...DOCUMENTS, ...this.drafts } });
    }

    private transformer(node: FileNode, level: number, parent: FileFlatNode | null): FileFlatNode {
        const flatNode = new FileFlatNode();

        flatNode.id = parent ? `${parent.id}/${node.name}` : node.name;
        flatNode.name = node.name;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    }
}
```

`expansionModel` then holds the keys rather than the nodes, and the types of the tree, its events and
its data source take the key type as one more parameter: `KbqTreeSelection<FileFlatNode, string>`,
`KbqTreeFlatDataSource<FileNode, FileFlatNode, string>`.

The tree's own `trackBy` input, a `TrackByFunction` of the index and the node, overrides the key the rows
are re-used by; it does not change the expansion. Without a control `trackBy` it is the only way to
re-use the rows.

## API

### Components

#### KbqTree (component)

```ts
@Component({ selector: 'kbq-tree', exportAs: 'kbqTree' })
class KbqTree extends KbqTreeBase<any> {
    treeControl = input<C>();
    readonly trackBy = input<TrackByFunction<T>>();
    dataSource = input<DataSource<T> | Observable<T[]> | T[] | null | undefined>();
    get nodeOutlet(): KbqTreeNodeOutlet;
    readonly nodeDefs: Signal<readonly KbqTreeNodeDef<T>[]>;
    viewChange: BehaviorSubject<{ start: number; end: number }>;
    get hasSavedState(): boolean;
    saveState(): void;
    clearSavedState(): void;
    renderNodeChanges(
        data: T[] | ReadonlyArray<T>,
        dataDiffer?: IterableDiffer<T>,
        viewContainer?: ViewContainerRef,
        parentData?: T
    ): void;
    getNodeDef(data: T, i: number): KbqTreeNodeDef<T>;
    insertNode(
        nodeData: T,
        index: number,
        viewContainer?: ViewContainerRef,
        parentData?: T
    ): void;
}
```

- `[useStateSaving]`: `boolean` — Whether the component remembers its state across reloads and restores it on the next render.
  Defaults to `true`, or to whatever `KBQ_STATE_SAVING_ENABLED` is provided as.
- `[stateSavingKey]`: `string` — The key the state is persisted under. While it is empty the key is derived from where the
  component sits in the document, which moves when the surrounding markup is restructured — an `id`
  on the component or any ancestor pins it just as well as this input does.
- `[treeControl]`: `C` — Controls the expanded state and holds the data nodes of the tree.
- `[trackBy]`: `TrackByFunction<T>` — Tracking function that will be used to check the differences in data changes. Used similarly
  to `ngFor` `trackBy` function. Optimize node operations by identifying a node based on its data
  relative to the function to know if a node should be added/removed/moved.
  Accepts a function that takes two parameters, `index` and `item`.

  Views are reused for nodes the function reports as unchanged, which a data source that rebuilds
  its node objects on every emission — a flattener among them — otherwise cannot get.

  Defaults to the `trackBy` of the tree control, read when the tree initializes and when the data source changes.
- `[dataSource]`: `DataSource<T> | Observable<T[]> | T[] | null | undefined` — Provides a stream containing the latest data array to render. Influenced by the tree's
  stream of view window (what dataNodes are currently on screen).
  Data source can be an observable of data array, or a data array to render.
- `nodeOutlet`: `KbqTreeNodeOutlet` — Outlet within the tree's template where the data nodes are inserted.
- `nodeDefs`: `Signal<readonly KbqTreeNodeDef<T>[]>` — The tree node template for the tree
- `viewChange`: `BehaviorSubject<{ start: number; end: number }>` — Stream containing the latest information on what rows are being displayed on screen.
  Can be used by the data source to as a heuristic of what data should be provided.

  The tree renders every expanded node eagerly, so the window is the whole data set; a data source
  cannot use it to page or virtualize. Windowing would have to emit a real range from a scroll
  listener here first.
- `hasSavedState`: `boolean` — Whether state is currently persisted for this tree — restored on init, or written since.
  Always `false` while `useStateSaving` is unset, and `false` again after `clearSavedState()`.
- `saveState()`: `void` — Persists the values of the currently expanded nodes.

  Called for every expansion and collapse a user performs. Expansion driven by the application —
  `treeControl.expandAll()`, or writing to `expansionModel` — is not persisted; call this to record it.

  A no-op while a filter is active: what is expanded then is the result set, not a chosen state.
- `clearSavedState()`: `void` — Removes the state persisted for this tree.

  Persistence itself stays on — the next expansion is written again.
- `renderNodeChanges()`: `void` — Check for changes made in the data and render each change (node added/removed/moved).
- `getNodeDef()`: `KbqTreeNodeDef<T>` — Finds the matching node definition that should be used for this node data. If there is only
  one node definition, it is returned. Otherwise, find the node definition that has a when
  predicate that returns true with the data. If none return true, return the default node
  definition.
- `insertNode()`: `void` — Create the embedded view for the data node template and place it in the correct index location
  within the data node view container.

#### KbqTreeNodeToggleComponent (component)

```ts
@Component({ selector: 'kbq-tree-node-toggle', exportAs: 'kbqTreeNodeToggle' })
class KbqTreeNodeToggleComponent<T> extends KbqTreeNodeToggleBaseDirective<T> {
    readonly node = input<T>();
    readonly recursive = input<boolean>(false, {
        alias: 'kbqTreeNodeToggleRecursive'
    });
    disabled = input<boolean>(false);
    get iconState(): boolean;
    toggle(event: Event): void;
}
```

- `[kbqTreeNodeToggleRecursive]`: `boolean` — Whether toggling expands or collapses the whole subtree instead of the node alone.

#### KbqTreeOption (component)

```ts
@Component({ selector: 'kbq-tree-option', exportAs: 'kbqTreeOption' })
class KbqTreeOption<T = any> extends KbqTreeNode<T> implements KbqTitleTextRef {
    readonly checkboxThirdState = input<boolean>(false);
    disabled = input<boolean>(false);
    readonly selectable = input<boolean>(true);
    showCheckbox = input<boolean | undefined>();
    readonly selectionChange = output<KbqTreeOptionChange>();
    /** @deprecated */
    readonly onSelectionChange = output<KbqTreeOptionChange>();
    tree: KbqTreeOptionParent<T> & KbqTreeBase<T>;
    readonly onFocus: Observable<KbqTreeOptionEvent>;
    readonly onBlur: Observable<KbqTreeOptionEvent>;
    preventBlur: boolean;
    get parentTextElement(): ElementRef<HTMLElement> | undefined;
    get textElement(): ElementRef<HTMLElement> | undefined;
    readonly toggleElementDirective: Signal<KbqTreeNodeToggleDirective<any> | undefined>;
    readonly toggleElementComponent: Signal<KbqTreeNodeToggleComponent<any> | undefined>;
    readonly pseudoCheckbox: Signal<KbqPseudoCheckbox | undefined>;
    readonly actionButton: Signal<KbqOptionActionComponent | undefined>;
    get tooltipTrigger(): KbqTooltipTrigger | undefined;
    get dropdownTrigger(): KbqDropdownTrigger | undefined;
    get externalPseudoCheckbox(): boolean;
    value: any;
    readonly userInteraction: EventEmitter<void>;
    selected: boolean;
    get id(): string;
    get viewValue(): string;
    hasFocus: boolean;
    get isExpandable(): boolean;
    get level(): number;
    get toggleElement(): KbqTreeNodeToggleBaseDirective<T> | undefined;
    get isToggleInDefaultPlace(): boolean;
    checkboxState: KbqPseudoCheckboxState;
    updateCheckboxState: () => void;
    data: T;
    get isExpanded(): boolean;
    /** @deprecated */
    static mostRecentTreeNode: KbqTreeNode<any> | null;
    descendantsAllSelected(): boolean;
    descendantsPartiallySelected(): boolean;
    updateParentsCheckboxState(node: T): void;
    toggle(): void;
    setSelected(selected: boolean): void;
    focus(focusOrigin?: FocusOrigin): void;
    blur(): void;
    select(setFocus?: boolean): void;
    deselect(): void;
    onKeydown($event: any): void;
    selectViaInteraction($event?: KeyboardEvent | MouseEvent): void;
    emitSelectionChangeEvent(): void;
    getHostElement(): HTMLElement;
    markForCheck(): void;
}
```

- `[disabled]`: `boolean` — Whether the option is disabled: through its own input, the tree, or the tree control's `isDisabled`.
- `[selectable]`: `boolean` — Whether the option can be selected by user interaction (click, keyboard, select all).
  A non-selectable option remains focusable, navigable and expandable.
  Programmatic selection (`setSelected`, `select`, value accessor) is not affected.
  Options that are not rendered (e.g. collapsed) are not checked by "select all".
- `[showCheckbox]`: `boolean | undefined` — Whether the option renders a pseudo-checkbox. Falls back to the tree's own mode until bound.
- `(selectionChange)`: `KbqTreeOptionChange` — Emits whenever the selected state of the option changes.
- `(onSelectionChange)`: `KbqTreeOptionChange` — Deprecated: Use `selectionChange` instead. Will be removed in 22.0.0.
- `onFocus`: `Observable<KbqTreeOptionEvent>` — Emits when the option takes focus.
- `onBlur`: `Observable<KbqTreeOptionEvent>` — Emits when the option loses focus.
- `parentTextElement`: `ElementRef<HTMLElement> | undefined` — Container of the option text, measured by `kbq-title`.
- `tooltipTrigger`: `KbqTooltipTrigger | undefined` — Tooltip trigger projected into the option.
- `dropdownTrigger`: `KbqDropdownTrigger | undefined` — Dropdown trigger projected into the option.
- `level`: `number` — The "select all" row sits above the tree at root depth; `getLevel` has no node to resolve for it.
- `mostRecentTreeNode`: `KbqTreeNode<any> | null` — The most recently created `KbqTreeNode`, across every tree on the page.

  Deprecated: Kept for backwards compatibility only — the tree hands a node its data through
  `KbqTreeBase.registerNode`, which is scoped to the tree that owns the node. Will be removed in 22.0.0.

#### KbqTreeSelection (component)

```ts
@Component({ selector: 'kbq-tree-selection', exportAs: 'kbqTreeSelection' })
class KbqTreeSelection<T = any, K = T> extends KbqTreeBase<T, FlatTreeControl<T, K>> implements ControlValueAccessor {
    readonly ariaLabel = input<string>(undefined, {
        alias: 'aria-label'
    });
    readonly ariaLabelledby = input<string>(undefined, {
        alias: 'aria-labelledby'
    });
    autoSelect = input<boolean | undefined>();
    multiple = input<KbqMultipleInput | undefined>();
    noUnselectLast = input<boolean | undefined>();
    typeAhead = input<boolean>(true);
    readonly selectAllToggle = input<boolean>(false);
    selectAll = model<boolean>(false);
    disabled = input<boolean | undefined>();
    selectAllHandler = input<((event: KeyboardEvent, tree: KbqTreeSelection<T, K>) => void) | undefined>();
    treeControl = input<C>();
    readonly trackBy = input<TrackByFunction<T>>();
    dataSource = input<DataSource<T> | Observable<T[]> | T[] | null | undefined>();
    readonly navigationChange = output<KbqTreeNavigationChange<KbqTreeOption<T>, T, K>>();
    /** @deprecated */
    readonly onSelectAll = output<KbqTreeSelectAllEvent<KbqTreeOption<T>, T, K>>();
    inSelect: boolean;
    renderedOptions: QueryList<KbqTreeOption<T>>;
    keyManager: FocusKeyManager<KbqTreeOption<T>>;
    selectionModel: SelectionModel<T>;
    resetFocusedItemOnBlur: boolean;
    userTabIndex: number | null;
    optionShouldHoldFocusOnBlur: boolean;
    ownsKeyboard: boolean;
    readonly selectAllOption: Signal<KbqTreeOption<T> | undefined>;
    get unorderedOptions(): QueryList<KbqTreeOption<T>>;
    readonly selectionChange: EventEmitter<KbqTreeSelectionChange<KbqTreeOption<T>, T, K>>;
    readonly copyChange: EventEmitter<KbqTreeCopyEvent<KbqTreeOption<T>, T, K>>;
    /** @deprecated */
    readonly onCopy: EventEmitter<KbqTreeCopyEvent<KbqTreeOption<T>, T, K>>;
    get optionFocusChanges(): Observable<KbqTreeOptionEvent>;
    get optionBlurChanges(): Observable<KbqTreeOptionEvent>;
    multipleMode: MultipleMode | null;
    get allOptionsSelected(): boolean;
    get selectAllState(): KbqPseudoCheckboxState;
    get showCheckbox(): boolean;
    get isEmpty(): boolean;
    get nodesCount(): number;
    get nodeOutlet(): KbqTreeNodeOutlet;
    readonly nodeDefs: Signal<readonly KbqTreeNodeDef<T>[]>;
    viewChange: BehaviorSubject<{ start: number; end: number }>;
    get hasSavedState(): boolean;
    initializeForEmbedding(selectionModel: SelectionModel<T>): void;
    focus($event: any): void;
    blur(): void;
    onKeyDown(event: KeyboardEvent): void;
    handleKeydown(event: KeyboardEvent): void;
    selectAllOptions(allowDeselect?: boolean): void;
    copyActiveOption(event: KeyboardEvent): void;
    setStateChildren(option: KbqTreeOption<T>, state: boolean): void;
    saveState(): void;
    clearSavedState(): void;
    getNodeDef(data: T, i: number): KbqTreeNodeDef<T>;
    insertNode(
        nodeData: T,
        index: number,
        viewContainer?: ViewContainerRef,
        parentData?: T
    ): void;
}
```

- `[aria-label]`: `string` — Accessible name of the tree. Set it whenever the tree has no visible label of its own; a tree
  embedded in a `kbq-tree-select` is labelled by the select instead.
- `[aria-labelledby]`: `string` — Id of the element that labels the tree, for when the label is rendered next to it.
- `[autoSelect]`: `boolean | undefined` — Whether clicking a node clears the rest of the selection. Defaults to `true`, and to `false` for
  `multiple="checkbox"`.

  Assigning it — from a template binding or imperatively, as `KbqTreeSelect` does — replaces the
  derived default, so a later `multiple` change leaves the value alone.
- `[multiple]`: `KbqMultipleInput | undefined` — Selection mode of the tree.

  `checkbox` renders a checkbox in every node, `keyboard` selects without one. A bare `multiple` and
  `true` both mean `checkbox`; `single`, `false` and an absent attribute mean single selection. The mode
  can be changed at any time — narrowing it to single selection keeps the first selected node and
  reports the shortened value through the `ControlValueAccessor`.

  A tree rendered inside a `kbq-tree-select` shares its `SelectionModel` with the select, so the
  select decides how many nodes may be selected: switching this input between single and multiple
  selection there throws. Switching between `checkbox` and `keyboard` only changes the presentation
  and is allowed.

  The getter reports whether more than one node can be selected; read `multipleMode` for the mode
  itself.
- `[noUnselectLast]`: `boolean | undefined` — Whether the last selected node can be deselected.
  Derived from the mode until assigned, like `autoSelect`.
- `[typeAhead]`: `boolean` — Whether typing printable characters moves the focus to the next option whose label starts with
  them. On by default.

  Turn it off wherever the tree shares its keystrokes with a text field: an embedder such as
  `KbqTreeSelect` routes everything the user types — the search query included — into this key
  manager, and type-ahead would move the active option out from under the query.
- `[selectAllToggle]`: `boolean` — When `true`, a repeated Ctrl/Cmd+A deselects all options. Off by default (Ctrl+A only selects).
- `[(selectAll)]`: `boolean` — Whether to render the "select all" master checkbox above the nodes. Multiple selection only.

  Enabling it also makes Ctrl/Cmd + A a two-way toggle, so the shortcut and the checkbox never
  disagree (`selectAllToggle` is implied).
- `[disabled]`: `boolean | undefined` — Whether the tree is disabled, bound with `[disabled]` or set by a form control.
- `[selectAllHandler]`: `((event: KeyboardEvent, tree: KbqTreeSelection<T, K>) => void) | undefined` — Function for handling the combination Ctrl + A (select all). By default, the internal handler is used,
  which toggles the selection of all non-disabled, selectable options.
- `[useStateSaving]`: `boolean` — Whether the component remembers its state across reloads and restores it on the next render.
  Defaults to `true`, or to whatever `KBQ_STATE_SAVING_ENABLED` is provided as.
- `[stateSavingKey]`: `string` — The key the state is persisted under. While it is empty the key is derived from where the
  component sits in the document, which moves when the surrounding markup is restructured — an `id`
  on the component or any ancestor pins it just as well as this input does.
- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.
- `[treeControl]`: `C` — Controls the expanded state and holds the data nodes of the tree.
- `[trackBy]`: `TrackByFunction<T>` — Tracking function that will be used to check the differences in data changes. Used similarly
  to `ngFor` `trackBy` function. Optimize node operations by identifying a node based on its data
  relative to the function to know if a node should be added/removed/moved.
  Accepts a function that takes two parameters, `index` and `item`.

  Views are reused for nodes the function reports as unchanged, which a data source that rebuilds
  its node objects on every emission — a flattener among them — otherwise cannot get.

  Defaults to the `trackBy` of the tree control, read when the tree initializes and when the data source changes.
- `[dataSource]`: `DataSource<T> | Observable<T[]> | T[] | null | undefined` — Provides a stream containing the latest data array to render. Influenced by the tree's
  stream of view window (what dataNodes are currently on screen).
  Data source can be an observable of data array, or a data array to render.
- `(onSelectAll)`: `KbqTreeSelectAllEvent<KbqTreeOption<T>, T, K>` — Deprecated: Use `selectAllChange` instead. Will be removed in 22.0.0.
- `inSelect`: `boolean` — Whether this tree-selection is rendered inside a select panel (enables option
  hover-to-focus). Defaults to detecting a wrapping `KbqFormField`, and is forced to `true`
  by the host `KbqTreeSelect` so hover works even when the tree-select has no form-field
  wrapper (e.g. the filter-bar pipes render the tree-select bare).
- `selectionModel`: `SelectionModel<T>` — Model backing the selection.

  The instance is replaced whenever `multiple` changes the multiplicity, because CDK freezes that
  flag at construction. Subscribe to `selectionChange` rather than to `selectionModel.changed`:
  a subscription taken on the model directly is left behind on the discarded instance.

  `T` is the type of the data nodes, inferred from `[treeControl]` in a template. A query names it:
  `viewChild.required<KbqTreeSelection<Node>>(KbqTreeSelection)`, and a tree whose control has a `trackBy`
  names the key type too: `KbqTreeSelection<Node, string>`.
- `ownsKeyboard`: `boolean` — Whether the tree acts on keyboard input itself. Turned off by an embedding host that owns the
  keyboard, such as `KbqTreeSelect`.
- `selectAllOption`: `Signal<KbqTreeOption<T> | undefined>` — Reference to the built-in "select all" row, rendered only while `selectAll` is on.
- `selectionChange`: `EventEmitter<KbqTreeSelectionChange<KbqTreeOption<T>, T, K>>` — Emits when the selection changes. A stream rather than an `output()`: `KbqTreeSelect` pipes it with rxjs.
- `copyChange`: `EventEmitter<KbqTreeCopyEvent<KbqTreeOption<T>, T, K>>` — Emits when the active option is copied with Ctrl/Cmd + C. Subscribing replaces the built-in
  clipboard handler, so the event has to stay an `EventEmitter`: the decision is made by reading
  `observed`, which `OutputEmitterRef` does not have. A template listener subscribes to it through
  the output below.
- `onCopy`: `EventEmitter<KbqTreeCopyEvent<KbqTreeOption<T>, T, K>>` — Deprecated: Use `copyChange` instead. Will be removed in 22.0.0.
- `optionFocusChanges`: `Observable<KbqTreeOptionEvent>` — Emits when an option of the tree takes focus.
- `optionBlurChanges`: `Observable<KbqTreeOptionEvent>` — Emits when an option of the tree loses focus.
- `multipleMode`: `MultipleMode | null` — Resolved selection mode, or `null` when only one node can be selected.
- `allOptionsSelected`: `boolean` — Whether every node "select all" can act on is selected.
- `selectAllState`: `KbqPseudoCheckboxState` — State of the "select all" master checkbox.
- `nodesCount`: `number` — Number of real, data-backed nodes currently rendered — unlike `renderedOptions.length`, never
  includes the "select all" row, which is appended to `renderedOptions` on its own, later-processed
  reactive pass and would otherwise make a consumer's own node count race against that pass.
- `nodeOutlet`: `KbqTreeNodeOutlet` — Outlet within the tree's template where the data nodes are inserted.
- `nodeDefs`: `Signal<readonly KbqTreeNodeDef<T>[]>` — The tree node template for the tree
- `viewChange`: `BehaviorSubject<{ start: number; end: number }>` — Stream containing the latest information on what rows are being displayed on screen.
  Can be used by the data source to as a heuristic of what data should be provided.

  The tree renders every expanded node eagerly, so the window is the whole data set; a data source
  cannot use it to page or virtualize. Windowing would have to emit a real range from a scroll
  listener here first.
- `hasSavedState`: `boolean` — Whether state is currently persisted for this tree — restored on init, or written since.
  Always `false` while `useStateSaving` is unset, and `false` again after `clearSavedState()`.
- `initializeForEmbedding()`: `void` — Rebuilds the tree around a selection model owned by an embedding host, such as `KbqTreeSelect`.

  The host has to take the model over before the tree starts reacting to it, and re-running
  `ngAfterContentInit()` is not a way to do that: neither `unorderedOptions` nor `renderedOptions` is
  ever re-created, so a second run would only leave a second set of subscribers on both — every options
  change handled twice, and the whole option sweep run twice per emission. Only the subscription that
  depends on the model has to move, which is exactly what `bindSelectionModel` does.
- `onKeyDown()`: `void` — Host listener for keyboard input.

  Does nothing while an embedding host owns the keyboard — `KbqTreeSelect` routes every key through
  its own panel handler and calls back into `handleKeydown` for the parts it delegates, so without
  this guard the same event would be acted on twice.
- `handleKeydown()`: `void` — Runs the tree's own keyboard handling, whether or not it owns the keyboard.
- `saveState()`: `void` — Persists the values of the currently expanded nodes.

  Called for every expansion and collapse a user performs. Expansion driven by the application —
  `treeControl.expandAll()`, or writing to `expansionModel` — is not persisted; call this to record it.

  A no-op while a filter is active: what is expanded then is the result set, not a chosen state.
- `clearSavedState()`: `void` — Removes the state persisted for this tree.

  Persistence itself stays on — the next expansion is written again.
- `getNodeDef()`: `KbqTreeNodeDef<T>` — Finds the matching node definition that should be used for this node data. If there is only
  one node definition, it is returned. Otherwise, find the node definition that has a when
  predicate that returns true with the data. If none return true, return the default node
  definition.
- `insertNode()`: `void` — Create the embedded view for the data node template and place it in the correct index location
  within the data node view container.

### Directives

#### KbqTreeBase (directive)

Rendering core of the tree.

Originally forked from `@angular/cdk/tree` and maintained independently since: the node/outlet
contract, the option-based selection model and the filtering pipeline have all diverged, so
`@angular/cdk/tree` is deliberately not a dependency of this package and upstream fixes have to be
ported by hand.

`C` narrows the tree control a concrete tree accepts, and so the type its `treeControl` binding is checked against.
The bound leaves out `expansionModel` and `trackBy`, the members typed by the control's key, so that it admits
a control of any key type: the model makes a control invariant in that type.

```ts
@Directive()
class KbqTreeBase<T, C extends Omit<TreeControl<T>, 'expansionModel' | 'trackBy'> = TreeControl<T>> implements CollectionViewer {
    treeControl = input<C>();
    readonly trackBy = input<TrackByFunction<T>>();
    dataSource = input<DataSource<T> | Observable<T[]> | T[] | null | undefined>();
    get nodeOutlet(): KbqTreeNodeOutlet;
    readonly nodeDefs: Signal<readonly KbqTreeNodeDef<T>[]>;
    viewChange: BehaviorSubject<{ start: number; end: number }>;
    get hasSavedState(): boolean;
    saveState(): void;
    clearSavedState(): void;
    renderNodeChanges(
        data: T[] | ReadonlyArray<T>,
        dataDiffer?: IterableDiffer<T>,
        viewContainer?: ViewContainerRef,
        parentData?: T
    ): void;
    getNodeDef(data: T, i: number): KbqTreeNodeDef<T>;
    insertNode(
        nodeData: T,
        index: number,
        viewContainer?: ViewContainerRef,
        parentData?: T
    ): void;
}
```

- `[treeControl]`: `C` — Controls the expanded state and holds the data nodes of the tree.
- `[trackBy]`: `TrackByFunction<T>` — Tracking function that will be used to check the differences in data changes. Used similarly
  to `ngFor` `trackBy` function. Optimize node operations by identifying a node based on its data
  relative to the function to know if a node should be added/removed/moved.
  Accepts a function that takes two parameters, `index` and `item`.

  Views are reused for nodes the function reports as unchanged, which a data source that rebuilds
  its node objects on every emission — a flattener among them — otherwise cannot get.

  Defaults to the `trackBy` of the tree control, read when the tree initializes and when the data source changes.
- `[dataSource]`: `DataSource<T> | Observable<T[]> | T[] | null | undefined` — Provides a stream containing the latest data array to render. Influenced by the tree's
  stream of view window (what dataNodes are currently on screen).
  Data source can be an observable of data array, or a data array to render.
- `nodeOutlet`: `KbqTreeNodeOutlet` — Outlet within the tree's template where the data nodes are inserted.
- `nodeDefs`: `Signal<readonly KbqTreeNodeDef<T>[]>` — The tree node template for the tree
- `viewChange`: `BehaviorSubject<{ start: number; end: number }>` — Stream containing the latest information on what rows are being displayed on screen.
  Can be used by the data source to as a heuristic of what data should be provided.

  The tree renders every expanded node eagerly, so the window is the whole data set; a data source
  cannot use it to page or virtualize. Windowing would have to emit a real range from a scroll
  listener here first.
- `hasSavedState`: `boolean` — Whether state is currently persisted for this tree — restored on init, or written since.
  Always `false` while `useStateSaving` is unset, and `false` again after `clearSavedState()`.
- `saveState()`: `void` — Persists the values of the currently expanded nodes.

  Called for every expansion and collapse a user performs. Expansion driven by the application —
  `treeControl.expandAll()`, or writing to `expansionModel` — is not persisted; call this to record it.

  A no-op while a filter is active: what is expanded then is the result set, not a chosen state.
- `clearSavedState()`: `void` — Removes the state persisted for this tree.

  Persistence itself stays on — the next expansion is written again.
- `renderNodeChanges()`: `void` — Check for changes made in the data and render each change (node added/removed/moved).
- `getNodeDef()`: `KbqTreeNodeDef<T>` — Finds the matching node definition that should be used for this node data. If there is only
  one node definition, it is returned. Otherwise, find the node definition that has a when
  predicate that returns true with the data. If none return true, return the default node
  definition.
- `insertNode()`: `void` — Create the embedded view for the data node template and place it in the correct index location
  within the data node view container.

#### KbqTreeNode (directive)

```ts
@Directive({ selector: 'kbq-tree-node', exportAs: 'kbqTreeNode' })
class KbqTreeNode<T> implements IFocusableOption {
    tree: KbqTreeBase<T, TreeControl<T, T>>;
    data: T;
    get isExpanded(): boolean;
    get level(): number;
    /** @deprecated */
    static mostRecentTreeNode: KbqTreeNode<any> | null;
    focus(): void;
}
```

- `mostRecentTreeNode`: `KbqTreeNode<any> | null` — The most recently created `KbqTreeNode`, across every tree on the page.

  Deprecated: Kept for backwards compatibility only — the tree hands a node its data through
  `KbqTreeBase.registerNode`, which is scoped to the tree that owns the node. Will be removed in 22.0.0.

#### KbqTreeNodeDef (directive)

Data node definition for the KbqTree.
Captures the node's template and a when predicate that describes when this node should be used.

```ts
@Directive({ selector: '[kbqTreeNodeDef]' })
class KbqTreeNodeDef<T> {
    readonly data = input<T>(undefined, { alias: 'kbqTreeNode' });
    @Input('kbqTreeNodeDefWhen') when: (index: number, nodeData: T) => boolean;
    template: TemplateRef<any>;
}
```

- `[kbqTreeNodeDefWhen]`: `(index: number, nodeData: T) => boolean` — Function that should return true if this node template should be used for the provided node
  data and index. If left undefined, this node will be considered the default node template to
  use when no other when functions return true for the data.
  For every node, there must be at least one when function that passes or an undefined to
  default.

#### KbqTreeNodeOutlet (directive)

```ts
@Directive({ selector: '[kbqTreeNodeOutlet]' })
class KbqTreeNodeOutlet {
    viewContainer: ViewContainerRef;
}
```

#### KbqTreeNodePadding (directive)

```ts
@Directive({ selector: '[kbqTreeNodePadding]', exportAs: 'kbqTreeNodePadding' })
class KbqTreeNodePadding<T> {
    readonly indent = input<number | string>(12, {
        alias: 'kbqTreeNodePaddingIndent'
    });
    level: number;
    get leftPadding(): number;
    get leftPaddingForFirstLevel(): number;
    get indentUnits(): string;
    withIcon: boolean;
    iconWidth: number;
    paddingIndent(): string | null;
}
```

- `[kbqTreeNodePaddingIndent]`: `number | string` — Indentation added per nesting level. Accepts a bare number (px) or a value with CSS units.
- `indentUnits`: `string` — CSS units used for the indentation value.
- `iconWidth`: `number` — Horizontal room a toggle takes up in a row: its own 16px box plus the option's 8px `gap`. A node
  without a toggle reserves the same amount so its content lines up with a sibling that has one —
  which is why widening the toggle itself pushes every branch row out of line.

#### KbqTreeNodeToggleDirective (directive)

```ts
@Directive({
    selector: '[kbq-tree-node-toggle], [kbqTreeNodeToggle]',
    exportAs: 'kbqTreeNodeToggle'
})
class KbqTreeNodeToggleDirective<T> extends KbqTreeNodeToggleBaseDirective<T> {
    readonly node = input<T>();
    readonly recursive = input<boolean>(false, {
        alias: 'kbqTreeNodeToggleRecursive'
    });
    disabled = input<boolean>(false);
    get iconState(): boolean;
    toggle(event: Event): void;
}
```

- `[kbqTreeNodeToggleRecursive]`: `boolean` — Whether toggling expands or collapses the whole subtree instead of the node alone.

### Classes

#### BaseTreeControl (abstract class)

Base tree control. It has basic toggle/expand/collapse operations on a single data node.

```ts
abstract class BaseTreeControl<T, K = T> implements TreeControl<T, K> {
    dataNodes: T[];
    expansionModel: SelectionModel<K>;
    trackBy?: (dataNode: T) => K;
    filterModel: SelectionModel<T>;
    filterValue: BehaviorSubject<T[] | string | null>;
    getLevel: (dataNode: T) => number;
    isExpandable: (dataNode: T) => boolean;
    getChildren: (dataNode: T) => Observable<T[]>;
    abstract getDescendants(dataNode: T): T[];
    abstract expandAll(): void;
    toggle(dataNode: T): void;
    expand(dataNode: T): void;
    collapse(dataNode: T): void;
    isExpanded(dataNode: T): boolean;
    toggleDescendants(dataNode: T): void;
    collapseAll(): void;
    expandDescendants(dataNode: T): void;
    collapseDescendants(dataNode: T): void;
    protected expansionKey(dataNode: T): K;
}
```

- `expansionModel`: `SelectionModel<K>` — A selection model with multi-selection to track expansion status: it holds the key of every expanded node.
- `trackBy?`: `(dataNode: T) => K` — Gives the key a data node is expanded by. Without it the node object is its own key, so a node
  rebuilt as a new object — as a flattener does whenever its data changes — comes back collapsed.

  Set it before anything is expanded: keys stored under another function no longer match.
- `getLevel`: `(dataNode: T) => number` — Get depth of a given data node, return the level number. This is for flat tree node.
- `isExpandable`: `(dataNode: T) => boolean` — Whether the data node is expandable. Returns true if expandable.
  This is for flat tree node.
- `getChildren`: `(dataNode: T) => Observable<T[]>` — Gets a stream that emits whenever the given data node's children change.
- `getDescendants()`: `T[]` — Gets a list of descendent data nodes of a subtree rooted at given data node recursively.
- `expandAll()`: `void` — Expands all data nodes in the tree.
- `toggle()`: `void` — Toggles one single data node's expanded/collapsed state.
- `expand()`: `void` — Expands one single data node.
- `collapse()`: `void` — Collapses one single data node.
- `isExpanded()`: `boolean` — Whether a given data node is expanded or not. Returns true if the data node is expanded.
- `toggleDescendants()`: `void` — Toggles a subtree rooted at `node` recursively.
- `collapseAll()`: `void` — Collapse all dataNodes in the tree.
- `expandDescendants()`: `void` — Expands a subtree rooted at given data node recursively.
- `collapseDescendants()`: `void` — Collapses a subtree rooted at given data node recursively.
- `expansionKey()`: `K` — The key `expansionModel` holds for a data node.

#### FilterByValues (class)

```ts
class FilterByValues<T, K = T> implements FlatTreeControlFilter<T> {
    result: T[];
    setValues: (values: string[]) => void;
    constructor(control: FlatTreeControl<T, K>);
    handle(_: any, prevFilter?: FlatTreeControlFilter<T>): T[];
    getValues(): string[];
}
```

#### FlatTreeControl (class)

Flat tree control. Able to expand/collapse a subtree recursively for flattened tree.

```ts
class FlatTreeControl<T, K = T> extends BaseTreeControl<T, K> {
    expandedItemsBeforeFiltration: K[];
    getLevel: (dataNode: T) => number;
    isExpandable: (dataNode: T) => boolean;
    getValue: (dataNode: T) => any;
    getViewValue: (dataNode: T) => string;
    compareValues: (firstValue: any, secondValue: any) => boolean;
    compareViewValues: (firstViewValue: any, secondViewValue: any) => boolean;
    isDisabled: (dataNode: T) => boolean;
    dataNodes: T[];
    expansionModel: SelectionModel<K>;
    trackBy?: (dataNode: T) => K;
    filterModel: SelectionModel<T>;
    filterValue: BehaviorSubject<T[] | string | null>;
    getChildren: (dataNode: T) => Observable<T[]>;
    constructor(
        getLevel: (dataNode: T) => number,
        isExpandable: (dataNode: T) => boolean,
        getValue: (dataNode: T) => any,
        getViewValue: (dataNode: T) => string,
        compareValues?: (firstValue: any, secondValue: any) => boolean,
        compareViewValues?: (firstViewValue: any, secondViewValue: any) => boolean,
        isDisabled?: (dataNode: T) => boolean,
        options?: KbqTreeControlOptions<T, K>
    );
    getFilters(): FlatTreeControlFilter<T>[];
    setFilters(...filters: FlatTreeControlFilter<T>[]): void;
    getDescendants(dataNode: T): T[];
    expandAll(): void;
    getParents(node: T, result?: T[]): T[];
    hasValue(value: string): T | undefined;
    filterNodes(value?: string | null): void;
    toggle(dataNode: T): void;
    expand(dataNode: T): void;
    collapse(dataNode: T): void;
    isExpanded(dataNode: T): boolean;
    toggleDescendants(dataNode: T): void;
    collapseAll(): void;
    expandDescendants(dataNode: T): void;
    collapseDescendants(dataNode: T): void;
    protected expansionKey(dataNode: T): K;
}
```

- `getValue`: `(dataNode: T) => any` — getValue will be used to determine if the tree contains value or not. Used in method hasValue
- `getViewValue`: `(dataNode: T) => string` — getViewValue will be used for filter nodes. Returned value will be first argument in filterNodesFunction
- `compareValues`: `(firstValue: any, secondValue: any) => boolean` — compareValues will be used to comparing values.
- `compareViewValues`: `(firstViewValue: any, secondViewValue: any) => boolean` — compareValues will be used to comparing values.
- `isDisabled`: `(dataNode: T) => boolean` — isDisabled will be used to determine if the node is disabled.
- `expansionModel`: `SelectionModel<K>` — A selection model with multi-selection to track expansion status: it holds the key of every expanded node.
- `trackBy?`: `(dataNode: T) => K` — Gives the key a data node is expanded by. Without it the node object is its own key, so a node
  rebuilt as a new object — as a flattener does whenever its data changes — comes back collapsed.

  Set it before anything is expanded: keys stored under another function no longer match.
- `getChildren`: `(dataNode: T) => Observable<T[]>` — Gets a stream that emits whenever the given data node's children change.
- `constructor()` — Construct with flat tree data node functions getLevel, isExpandable, getValue and getViewValue.
  - `getValue`: `(dataNode: T) => any` — getValue will be used to determine if the tree contains value or not. Used in method hasValue
  - `getViewValue`: `(dataNode: T) => string` — getViewValue will be used for filter nodes. Returned value will be first argument in filterNodesFunction
  - `compareValues`: `(firstValue: any, secondValue: any) => boolean` — compareValues will be used to comparing values.
  - `compareViewValues`: `(firstViewValue: any, secondViewValue: any) => boolean` — compareValues will be used to comparing values.
  - `isDisabled`: `(dataNode: T) => boolean` — isDisabled will be used to determine if the node is disabled.
  - `options`: `KbqTreeControlOptions<T, K> | undefined` — Further options: the `trackBy` the nodes are expanded by.
- `getDescendants()`: `T[]` — Gets a list of the data node's subtree of descendent data nodes.

  To make this working, the `dataNodes` of the TreeControl must be flattened tree nodes
  with correct levels.
- `expandAll()`: `void` — Expands all data nodes in the tree.

  To make this working, the `dataNodes` variable of the TreeControl must be set to all flattened
  data nodes of the tree.
- `getParents()`: `T[]` — Ancestors of a data node, outermost first.

  Derived from `dataNodes` rather than from a `parent` back-reference: the flattened nodes are in
  depth-first order, so scanning backwards from the node, the first node of a smaller level is its
  parent, and repeating with that level yields the whole chain. A data node whose shape carries no
  `parent` therefore resolves just as well as one that does.
  - `result`: `T[]` — Collector the ancestors are prepended to. Provided so callers can accumulate into
    an existing array; a fresh one is used when omitted.
- `toggle()`: `void` — Toggles one single data node's expanded/collapsed state.
- `expand()`: `void` — Expands one single data node.
- `collapse()`: `void` — Collapses one single data node.
- `isExpanded()`: `boolean` — Whether a given data node is expanded or not. Returns true if the data node is expanded.
- `toggleDescendants()`: `void` — Toggles a subtree rooted at `node` recursively.
- `collapseAll()`: `void` — Collapse all dataNodes in the tree.
- `expandDescendants()`: `void` — Expands a subtree rooted at given data node recursively.
- `collapseDescendants()`: `void` — Collapses a subtree rooted at given data node recursively.
- `expansionKey()`: `K` — The key `expansionModel` holds for a data node.

#### KbqTreeCopyEvent (class)

Event class that occurs when copying an item from the KbqTreeSelection.
Used to pass data about the copied item and copy context.

```ts
class KbqTreeCopyEvent<T, N = any, K = N> {
    source: KbqTreeSelection<N, K>;
    option: T;
    event?: KeyboardEvent;
    constructor(
        source: KbqTreeSelection<N, K>,
        option: T,
        event?: KeyboardEvent
    );
}
```

#### KbqTreeFlatDataSource (class)

Data source for flat tree.
The data source need to handle expansion/collapsion of the tree node and change the data feed
to `KbqTree`.
The nested tree nodes of type `T` are flattened through `KbqTreeFlattener`, and converted
to type `F` for `KbqTree` to consume.

```ts
class KbqTreeFlatDataSource<T, F, K = F> extends DataSource<F> {
    flattenedData: BehaviorSubject<F[]>;
    expandedData: BehaviorSubject<F[]>;
    filteredData: BehaviorSubject<F[]>;
    data: T[];
    constructor(
        treeControl: FlatTreeControl<F, K>,
        treeFlattener: KbqTreeFlattener<T, F>,
        initialData?: T[]
    );
    connect(collectionViewer: CollectionViewer): Observable<F[]>;
    filterHandler(): F[];
    expansionHandler(_change: SelectionChange<K>): F[];
    disconnect(): void;
}
```

#### KbqTreeFlattener (class)

Tree flattener to convert a normal type of node to node with children & level information.
Transform nested nodes of type `T` to flattened nodes of type `F`.

For example, the input data of type `T` is nested, and contains its children data:

```ts
SomeNode: {
    key: 'Fruits',
    children: [
        NodeOne: {
            key: 'Apple'
        },
        NodeTwo: {
            key: 'Pear'
        }
    ]
}
```

After flattener flatten the tree, the structure will become

```ts
SomeNode: {
    key: 'Fruits',
    expandable: true,
    level: 1
},
NodeOne: {
    key: 'Apple',
    expandable: false,
    level: 2
},
NodeTwo: {
    key: 'Pear',
    expandable: false,
    level: 2
}
```

and the output flattened type is `F` with additional information.

```ts
class KbqTreeFlattener<T, F> {
    transformFunction: (node: T, level: number, parent: F | null) => F;
    getLevel: (node: F) => number;
    isExpandable: (node: F) => boolean;
    getChildren: (node: T) => T[] | Observable<T[]> | null | undefined;
    constructor(
        transformFunction: (node: T, level: number, parent: F | null) => F,
        getLevel: (node: F) => number,
        isExpandable: (node: F) => boolean,
        getChildren: (node: T) => T[] | Observable<T[]> | null | undefined
    );
    flattenNodes(structuredData: T[]): F[];
    expandFlattenedNodes(
        nodes: F[],
        treeControl: Pick<TreeControl<F>, 'isExpanded'>
    ): F[];
}
```

- `flattenNodes()`: `F[]` — Flatten a list of node type T to flattened version of node F.
  Please note that type T may be nested, and the length of `structuredData` may be different
  from that of returned list `F[]`.
- `expandFlattenedNodes()`: `F[]` — Expand flattened node with current expansion status.
  The returned list may have different length.

  Reads only `isExpanded` of the tree control, so it takes a control of any key type.

#### KbqTreeNavigationChange (class)

```ts
class KbqTreeNavigationChange<T, N = any, K = N> {
    source: KbqTreeSelection<N, K>;
    option: T;
    constructor(source: KbqTreeSelection<N, K>, option: T);
}
```

#### KbqTreeNestedDataSource (class)

Data source for nested tree.

The data source for nested tree doesn't have to consider node flattener, or the way to expand
or collapse. The expansion/collapsion will be handled by TreeControl and each non-leaf node.

```ts
class KbqTreeNestedDataSource<T> extends DataSource<T> {
    data: T[];
    connect(collectionViewer: CollectionViewer): Observable<T[]>;
    disconnect(): void;
}
```

#### KbqTreeNodeOutletContext (class)

Context provided to the tree node component.

```ts
class KbqTreeNodeOutletContext<T> {
    $implicit: T;
    level: number;
    index?: number;
    count?: number;
    constructor(data: T);
}
```

- `$implicit`: `T` — Data for the node.
- `level`: `number` — Depth of the node.
- `index?`: `number` — Index location of the node.
- `count?`: `number` — Length of the number of total dataNodes.

#### KbqTreeOptionChange (class)

Represents a change event for a tree option.

```ts
class KbqTreeOptionChange {
    source: KbqTreeOption<any>;
    constructor(source: KbqTreeOption<any>);
}
```

- `source`: `KbqTreeOption<any>` — The tree option that has been modified or selected.
- `constructor()`
  - `source`: `KbqTreeOption<any>` — The tree option that has been modified or selected.

#### KbqTreeSelectAllEvent (class)

```ts
class KbqTreeSelectAllEvent<T, N = any, K = N> {
    source: KbqTreeSelection<N, K>;
    options: T[];
    constructor(source: KbqTreeSelection<N, K>, options: T[]);
}
```

#### KbqTreeSelectionChange (class)

```ts
class KbqTreeSelectionChange<T, N = any, K = N> {
    source: KbqTreeSelection<N, K>;
    option: T;
    options?: T[];
    constructor(
        source: KbqTreeSelection<N, K>,
        option: T,
        options?: T[]
    );
}
```

#### NestedTreeControl (class)

Nested tree control. Able to expand/collapse a subtree recursively for NestedNode type.

```ts
class NestedTreeControl<T, K = T> extends BaseTreeControl<T, K> {
    getChildren: (dataNode: T) => Observable<T[]>;
    dataNodes: T[];
    expansionModel: SelectionModel<K>;
    trackBy?: (dataNode: T) => K;
    filterModel: SelectionModel<T>;
    filterValue: BehaviorSubject<T[] | string | null>;
    getLevel: (dataNode: T) => number;
    isExpandable: (dataNode: T) => boolean;
    constructor(
        getChildren: (dataNode: T) => Observable<T[]>,
        options?: KbqTreeControlOptions<T, K>
    );
    expandAll(): void;
    getDescendants(dataNode: T): T[];
    toggle(dataNode: T): void;
    expand(dataNode: T): void;
    collapse(dataNode: T): void;
    isExpanded(dataNode: T): boolean;
    toggleDescendants(dataNode: T): void;
    collapseAll(): void;
    expandDescendants(dataNode: T): void;
    collapseDescendants(dataNode: T): void;
    protected expansionKey(dataNode: T): K;
}
```

- `getChildren`: `(dataNode: T) => Observable<T[]>` — Must emit **synchronously** (e.g. `of(children)` or a `BehaviorSubject`).
  `getDescendants` walks the tree with `take(1)` and returns as soon as the recursion unwinds, so
  an asynchronous source yields an incomplete — usually empty — descendant list.
- `expansionModel`: `SelectionModel<K>` — A selection model with multi-selection to track expansion status: it holds the key of every expanded node.
- `trackBy?`: `(dataNode: T) => K` — Gives the key a data node is expanded by. Without it the node object is its own key, so a node
  rebuilt as a new object — as a flattener does whenever its data changes — comes back collapsed.

  Set it before anything is expanded: keys stored under another function no longer match.
- `getLevel`: `(dataNode: T) => number` — Get depth of a given data node, return the level number. This is for flat tree node.
- `isExpandable`: `(dataNode: T) => boolean` — Whether the data node is expandable. Returns true if expandable.
  This is for flat tree node.
- `constructor()` — Construct with nested tree function getChildren.
  - `getChildren`: `(dataNode: T) => Observable<T[]>` — Must emit **synchronously** (e.g. `of(children)` or a `BehaviorSubject`).
    `getDescendants` walks the tree with `take(1)` and returns as soon as the recursion unwinds, so
    an asynchronous source yields an incomplete — usually empty — descendant list.
- `expandAll()`: `void` — Expands all dataNodes in the tree.

  To make this working, the `dataNodes` variable of the TreeControl must be set to all root level
  data nodes of the tree.
- `getDescendants()`: `T[]` — Gets a list of descendant dataNodes of a subtree rooted at given data node recursively.
- `toggle()`: `void` — Toggles one single data node's expanded/collapsed state.
- `expand()`: `void` — Expands one single data node.
- `collapse()`: `void` — Collapses one single data node.
- `isExpanded()`: `boolean` — Whether a given data node is expanded or not. Returns true if the data node is expanded.
- `toggleDescendants()`: `void` — Toggles a subtree rooted at `node` recursively.
- `collapseAll()`: `void` — Collapse all dataNodes in the tree.
- `expandDescendants()`: `void` — Expands a subtree rooted at given data node recursively.
- `collapseDescendants()`: `void` — Collapses a subtree rooted at given data node recursively.
- `expansionKey()`: `K` — The key `expansionModel` holds for a data node.

### Functions

#### defaultCompareValues (function)

```ts
function defaultCompareValues(
    firstValue: string,
    secondValue: string
): boolean;
```

#### defaultCompareViewValues (function)

```ts
function defaultCompareViewValues(
    firstViewValue: string,
    secondViewValue: string
): boolean;
```

### Types

#### FlatTreeControlFilter (interface)

```ts
interface FlatTreeControlFilter<T> {
    result: T[];
    handle(
        value: string | null,
        prevFilter?: FlatTreeControlFilter<T> | null
    ): T[];
}
```

#### KbqTreeControlOptions (interface)

Options of `FlatTreeControl` and `NestedTreeControl`.

```ts
interface KbqTreeControlOptions<T, K> {
    trackBy?: (dataNode: T) => K;
}
```

- `trackBy?`: `(dataNode: T) => K` — Initial value of the control's `trackBy`.

#### KbqTreeOptionEvent (interface)

```ts
interface KbqTreeOptionEvent {
    option: KbqTreeOption;
}
```

#### KbqTreeOptionParent (interface)

The part of the tree an option talks to.

Declared as an interface rather than as `KbqTreeSelection` so the token stays free of a circular
import, and so anything embedding options only has to provide these members.

```ts
interface KbqTreeOptionParent<T = any> {
    disabled: boolean;
    multiple: boolean;
    showCheckbox: boolean;
    inSelect: boolean;
    optionShouldHoldFocusOnBlur: boolean;
    selectAllState: KbqPseudoCheckboxState;
    treeControl: FlatTreeControl<T>;
    selectionModel: SelectionModel<T>;
    unorderedOptions: QueryList<KbqTreeOption<T>>;
    setSelectedOptionsByClick(
        option: KbqTreeOption<T>,
        shiftKey: boolean,
        ctrlKey: boolean
    ): void;
    reportOptionFocus?(event: KbqTreeOptionEvent): void;
    reportOptionBlur?(event: KbqTreeOptionEvent): void;
}
```

- `disabled`: `boolean` — Whether the whole tree is disabled.
- `multiple`: `boolean` — Whether more than one option can be selected at a time.
- `showCheckbox`: `boolean` — Whether the options render a checkbox.
- `inSelect`: `boolean` — Whether the tree is rendered inside a select panel, which makes hovering an option focus it.
- `optionShouldHoldFocusOnBlur`: `boolean` — Whether an option keeps focus while the tree loses it, as a select with a search field needs.
- `selectAllState`: `KbqPseudoCheckboxState` — State of the tree-level "select all" checkbox.
- `reportOptionFocus?()`: `void` — Called when an option takes focus.
- `reportOptionBlur?()`: `void` — Called when an option loses focus.

#### KbqTreeState (type)

The persisted state of a tree — the values of the nodes that were expanded.

```ts
type KbqTreeState = string[];
```

#### TreeControl (interface)

Tree control interface. User can implement TreeControl to expand/collapse dataNodes in the tree.
The Tree will use this TreeControl to expand/collapse a node.
User can also use it outside the `<kbq-tree>` to control the expansion status of the tree.

```ts
interface TreeControl<T, K = T> {
    dataNodes: T[];
    expansionModel: SelectionModel<K>;
    filterModel: SelectionModel<T>;
    trackBy?: (dataNode: T) => K;
    filterValue: BehaviorSubject<T[] | string | null>;
    getLevel(dataNode: T): number;
    isExpandable(dataNode: T): boolean;
    getChildren(dataNode: T): Observable<T[]>;
    isExpanded(dataNode: T): boolean;
    getDescendants(dataNode: T): any[];
    toggle(dataNode: T): void;
    expand(dataNode: T): void;
    collapse(dataNode: T): void;
    expandAll(): void;
    collapseAll(): void;
    toggleDescendants(dataNode: T): void;
    expandDescendants(dataNode: T): void;
    collapseDescendants(dataNode: T): void;
}
```

- `dataNodes`: `T[]` — The saved tree nodes data for `expandAll` action.
- `expansionModel`: `SelectionModel<K>` — The expansion model: the keys of the expanded data nodes, which are the nodes themselves by default.
- `trackBy?`: `(dataNode: T) => K` — Gives the key a data node is expanded by. Without it the node object is its own key.
- `getLevel()`: `number` — Get depth of a given data node, return the level number. This is for flat tree node.
- `isExpandable()`: `boolean` — Whether the data node is expandable. Returns true if expandable.
  This is for flat tree node.
- `getChildren()`: `Observable<T[]>` — Gets a stream that emits whenever the given data node's children change.
- `isExpanded()`: `boolean` — Whether the data node is expanded or collapsed. Return true if it's expanded.
- `getDescendants()`: `any[]` — Get all descendants of a data node
- `toggle()`: `void` — Expand or collapse data node
- `expand()`: `void` — Expand one data node
- `collapse()`: `void` — Collapse one data node
- `expandAll()`: `void` — Expand all the dataNodes in the tree
- `collapseAll()`: `void` — Collapse all the dataNodes in the tree
- `toggleDescendants()`: `void` — Toggle a data node by expand/collapse it and all its descendants
- `expandDescendants()`: `void` — Expand a data node and all its descendants
- `collapseDescendants()`: `void` — Collapse a data node and all its descendants

### Constants

#### KBQ_SELECTION_TREE_VALUE_ACCESSOR (const)

```ts
const KBQ_SELECTION_TREE_VALUE_ACCESSOR: Provider;
```

#### KBQ_TREE_OPTION_PARENT_COMPONENT (const)

Injection token used to provide the parent component to options.

```ts
const KBQ_TREE_OPTION_PARENT_COMPONENT: InjectionToken<KbqTreeOptionParent>;
```

#### kbqTreeSelectAllValue (const)

```ts
const kbqTreeSelectAllValue: string;
```

## Examples

### Search

The search results include matches and all of their parents:

- the tree collapses down to the matches and the path to them: the found items and the chain of their parents are visible
- the path to a match is expanded, while the items nested inside it are hidden;
- matched branches can be expanded and collapsed to view their contents;
- the matched fragment is highlighted.

**Example: Tree custom filtering** (`tree-custom-filtering`)

```ts tree-custom-filtering-example.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqHighlightModule } from '@koobiq/components/core';
import { KbqInputModule } from '@koobiq/components/input';
import {
    FlatTreeControl,
    FlatTreeControlFilter,
    KbqTreeFlatDataSource,
    KbqTreeFlattener,
    KbqTreeModule
} from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'mosaic-dev': {
            alert: '',
            badge: ''
        },
        'mosaic-examples': '',
        'mosaic-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

export class CustomTreeControlFilter<T> implements FlatTreeControlFilter<T> {
    result: T[];

    constructor(private control: FlatTreeControl<T>) {}

    handle(value: string | null): T[] {
        if (!value) {
            this.result = [];

            return this.result;
        }

        const result: Set<T> = new Set();

        const foundedNodes = this.control.dataNodes.filter((node: any) => {
            return this.control.compareViewValues(this.control.getViewValue(node), value);
        });

        foundedNodes.forEach((filteredNode) => {
            this.control.getParents(filteredNode, []).forEach((node) => result.add(node));

            result.add(filteredNode);
        });

        this.result = Array.from(result);

        return this.result;
    }
}

@Component({
    selector: 'tree-custom-filtering-example',
    imports: [KbqInputModule, FormsModule, KbqTreeModule, KbqHighlightModule],
    template: `
        <kbq-form-field>
            <input
                kbqInput
                type="text"
                placeholder="Search"
                [(ngModel)]="filterValue"
                (ngModelChange)="onFilterChange($event)"
            />
        </kbq-form-field>
        <kbq-tree-selection
            class="layout-margin-top-m"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <kbq-tree-node-toggle [node]="node" (click)="onChevronClick(node, $event)" />

                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeCustomFilteringExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = '';
    filterValue: string = '';

    /** Folders (matches) the user manually expanded while a filter is active. */
    readonly expandedMatches = signal(new Set<FileFlatNode>());

    /** Nodes whose own view value matches the current query (excludes ancestors). Only these can be expanded by the user. */
    private baseMatches = new Set<FileFlatNode>();

    private readonly customFilter: CustomTreeControlFilter<FileFlatNode>;

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );

        this.customFilter = new CustomTreeControlFilter<FileFlatNode>(this.treeControl);
        this.treeControl.setFilters(this.customFilter);

        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);

        // Reset expanded + match state whenever the filter goes idle.
        this.treeControl.filterValue.subscribe((value) => {
            if (!this.isFilterActive(value)) {
                this.expandedMatches.set(new Set());
                this.baseMatches = new Set();
            }
        });
    }

    onFilterChange(value: string): void {
        // Each new query starts from a clean expanded state.
        this.expandedMatches.set(new Set());
        this.baseMatches = this.computeBaseMatches(value);
        this.treeControl.filterNodes(value);

        if (this.baseMatches.size > 0) {
            // FlatTreeControl.filterNodes auto-selects every expandable node in the filter result
            // (matches + ancestors). For not-yet-expanded MATCH folders the chevron should read "collapsed"
            // because no children are visible yet — deselect them. Ancestors keep their selection so
            // the chevron correctly shows "expanded" (the path to the match IS visible).
            //
            // The deselect is deferred to a microtask so it runs AFTER `updateFilterValue`'s
            // Promise.resolve callback has set `filterValue.value`. Otherwise `expansionModel.changed`
            // here would route through the data source's `expansionHandler` and our work would be
            // overwritten when filterValue is later set.
            Promise.resolve().then(() => {
                let mutated = false;

                this.baseMatches.forEach((match) => {
                    if (this.treeControl.isExpandable(match) && this.treeControl.expansionModel.isSelected(match)) {
                        this.treeControl.expansionModel.deselect(match);
                        mutated = true;
                    }
                });

                if (mutated) {
                    // Refresh the filter rendering after expansionModel mutation.
                    this.treeControl.filterValue.next(this.treeControl.filterValue.value);
                }
            });
        }
    }

    /**
     * Fires on every click on `<kbq-tree-node-toggle>`. When no filter is active the toggle's own
     * host handler runs and the early `return` here makes us a no-op. When a filter is active the
     * toggle disables itself, so we take over and update the expanded set — but only for nodes that
     * actually match the search query (ancestors and revealed siblings cannot be expanded).
     */
    onChevronClick(node: FileFlatNode, event: Event): void {
        if (!this.isFilterActive(this.treeControl.filterValue.value)) return;

        // Only nodes whose own view value matches the query can be expanded.
        if (!this.baseMatches.has(node)) {
            event.stopPropagation();

            return;
        }

        event.stopPropagation();

        const next = new Set(this.expandedMatches());

        if (next.has(node)) {
            next.delete(node);
            // Mirror to expansionModel so the chevron rotates back to "collapsed".
            this.treeControl.expansionModel.deselect(node);
            // Collapsing a parent must also drop any nested expanded descendants.
            Array.from(next).forEach((expanded) => {
                if (this.isDescendantOf(expanded, node)) {
                    next.delete(expanded);
                    this.treeControl.expansionModel.deselect(expanded);
                }
            });
        } else {
            next.add(node);
            // Mirror to expansionModel so the chevron rotates to "expanded".
            this.treeControl.expansionModel.select(node);
        }

        this.expandedMatches.set(next);
        this.rebuildFilterVisible();
    }

    private computeBaseMatches(value: string | null): Set<FileFlatNode> {
        if (!value) return new Set();

        return new Set(
            this.treeControl.dataNodes.filter((node) =>
                this.treeControl.compareViewValues(this.treeControl.getViewValue(node), value)
            )
        );
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private isFilterActive(value: string | FileFlatNode[] | null): boolean {
        return Array.isArray(value) ? value.length > 0 : !!value;
    }

    /**
     * Recomputes `filterModel` as `customFilter.handle(query) ∪ direct children of each expanded folder`,
     * preserves hierarchical order, and re-emits `filterValue` so the data source rebuilds via
     * `filterHandler()`.
     */
    private rebuildFilterVisible(): void {
        const query = this.filterValue;
        const base = this.customFilter.handle(query);
        const combined = new Set<FileFlatNode>(base);

        this.expandedMatches().forEach((expanded) => {
            const childLevel = this.treeControl.getLevel(expanded) + 1;

            this.treeControl.getDescendants(expanded).forEach((descendant) => {
                if (this.treeControl.getLevel(descendant) === childLevel) {
                    combined.add(descendant);
                }
            });
        });

        // Preserve hierarchical (dataNodes) order so expanded children render next to their parent.
        const ordered = this.treeControl.dataNodes.filter((n) => combined.has(n));

        this.treeControl.filterModel.clear();
        this.treeControl.filterModel.select(...ordered);

        this.treeControl.filterValue.next(this.treeControl.filterValue.value);
    }

    /** Walks `node.parent` upward; returns true if `ancestor` is reached. */
    private isDescendantOf(node: FileFlatNode, ancestor: FileFlatNode): boolean {
        let parent = node.parent;

        while (parent) {
            if (parent === ancestor) return true;

            parent = parent.parent;
        }

        return false;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };
}
```

### Filtering with search + selected

**Example: Tree checked filtering** (`tree-checked-filtering`)

```ts tree-checked-filtering-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonToggleChange, KbqButtonToggleModule } from '@koobiq/components/button-toggle';
import { KbqHighlightModule } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import {
    FilterByValues,
    FilterByViewValue,
    FilterParentsForNodes,
    FlatTreeControl,
    KbqTreeFlatDataSource,
    KbqTreeFlattener,
    KbqTreeModule,
    KbqTreeSelection
} from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'mosaic-dev': {
            alert: '',
            badge: ''
        },
        'mosaic-examples': '',
        'mosaic-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

abstract class TreeParams {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;
    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;
    treeData: FileNode[];
    tree: KbqTreeSelection;

    constructor() {
        this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable, this.getValue, this.getViewValue);
        this.treeFlattener = new KbqTreeFlattener<FileNode, FileFlatNode>(
            this.transformer,
            this.getLevel,
            this.isExpandable,
            this.getChildren
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);
        this.dataSource.data = this.treeData = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    getLevel = (node: FileFlatNode) => node.level;

    getValue = (node: FileFlatNode) => node.name;

    getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };

    isExpandable = (node: FileFlatNode) => node.expandable;

    getChildren = (node: FileNode) => node.children;

    transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children?.length;

        return flatNode;
    };
}

@Component({
    selector: 'tree-checked-filtering-example',
    imports: [
        FormsModule,
        KbqButtonToggleModule,
        KbqTreeModule,
        KbqHighlightModule,
        KbqIconModule,
        KbqInputModule
    ],
    template: `
        <kbq-form-field>
            <input kbqInput type="text" [(ngModel)]="filterValue" (ngModelChange)="onFilterChange($event)" />
        </kbq-form-field>
        <div class="layout-margin-top-4xl">
            <kbq-button-toggle-group #group1="kbqButtonToggleGroup" (change)="onToggleClick($event)">
                <kbq-button-toggle [checked]="true" [value]="treeStates.ALL">
                    <i kbq-icon="kbq-bell_16"></i>
                    All
                </kbq-button-toggle>
                <kbq-button-toggle [value]="treeStates.SELECTED">
                    <i kbq-icon="kbq-bell_16"></i>
                    Selected
                </kbq-button-toggle>
                <kbq-button-toggle [value]="treeStates.UNSELECTED">
                    <i kbq-icon="kbq-bell_16"></i>
                    Unselected
                </kbq-button-toggle>
            </kbq-button-toggle-group>
        </div>
        <kbq-tree-selection
            multiple="checkbox"
            [autoSelect]="false"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
            (ngModelChange)="onModelValueChange($event)"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <kbq-tree-node-toggle [node]="node" />

                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeCheckedFilteringExample extends TreeParams {
    treeControl: FlatTreeControl<FileFlatNode>;
    filterByValues: FilterByValues<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = ['docs'];
    filterValue: string = '';

    readonly treeStates = {
        ALL: 0,
        SELECTED: 1,
        UNSELECTED: 2
    };

    constructor() {
        super();
        this.filterByValues = new FilterByValues<FileFlatNode>(this.treeControl);
        this.filterByValues.setValues(this.modelValue);

        this.treeControl.setFilters(
            new FilterByViewValue<FileFlatNode>(this.treeControl),
            this.filterByValues,
            new FilterParentsForNodes<FileFlatNode>(this.treeControl)
        );
    }

    onModelValueChange(values) {
        this.filterByValues.setValues(values);
    }

    onFilterChange(value: string): void {
        this.treeControl.filterNodes(value);
    }

    onToggleClick({ value }: KbqButtonToggleChange) {
        if (value === this.treeStates.ALL) {
            this.filterByValues.setValues([]);
        } else if (value === this.treeStates.SELECTED) {
            this.filterByValues.setValues(this.modelValue);
        } else if (value === this.treeStates.UNSELECTED) {
            const values = this.treeControl.dataNodes
                .filter((node) => !this.modelValue.includes(this.treeControl.getValue(node)))
                .map((node) => this.treeControl.getValue(node));

            this.filterByValues.setValues(values);
        }

        this.treeControl.filterNodes(this.filterValue);
    }
}
```

### Multiple selection variants

These behaviors are also available for [tree select](https://koobiq.io/en/components/tree-select.md).

#### Child subcategories

**When to use**

Used when nested items are a subset of their parents.

For example, in a hierarchical product category filter. When we search in coffee makers and toasters, we are actually searching within small kitchen appliances, which is itself a subcategory of home appliances in general.

**How it works**

The algorithm for this tree-select is the familiar 3-state checkbox behavior:

- If all N-level children are selected, their parent (N-1) becomes selected.
- If no N-level items are selected, their parent (N-1) becomes deselected.
- If some N-level items are selected and some are not, their parent (N-1) becomes selected.

**How to save to the model and select matcher**

Ideal: Display in the select matcher and save to the model the minimum set of items — N-level items if not all are selected. And one N-1 level item if all N-level items are selected.

Acceptable: When selecting an N-1 level item, save all N-level items (all children) to the model. If N+1 level items exist, save those too.

An extension of this case is the folder/group access rights scenario (variant 3).

**Example: Tree descendants subcategories** (`tree-descendants-subcategories`)

```ts tree-descendants-subcategories-example.ts
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqHighlightModule } from '@koobiq/components/core';
import {
    FlatTreeControl,
    KbqTreeFlatDataSource,
    KbqTreeFlattener,
    KbqTreeModule,
    KbqTreeOption,
    KbqTreeSelection,
    KbqTreeSelectionChange
} from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        koobiq: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'koobiq-dev': {
            alert: '',
            badge: ''
        },
        'koobiq-examples': '',
        'koobiq-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

abstract class TreeParams {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;
    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;
    treeData: FileNode[];

    constructor() {
        this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable, this.getValue, this.getViewValue);
        this.treeFlattener = new KbqTreeFlattener<FileNode, FileFlatNode>(
            this.transformer,
            this.getLevel,
            this.isExpandable,
            this.getChildren
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);
        this.dataSource.data = this.treeData = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    getLevel = (node: FileFlatNode) => node.level;

    getValue = (node: FileFlatNode) => node.name;

    getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };

    isExpandable = (node: FileFlatNode) => node.expandable;

    getChildren = (node: FileNode) => node.children;

    transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children?.length;

        return flatNode;
    };
}

@Component({
    selector: 'tree-descendants-subcategories-example',
    imports: [
        KbqTreeModule,
        FormsModule,
        KbqHighlightModule
    ],
    template: `
        <kbq-tree-selection
            multiple="checkbox"
            [autoSelect]="false"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
            (selectionChange)="onSelectionChange($event)"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [checkboxThirdState]="true">
                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding [checkboxThirdState]="true">
                <kbq-tree-node-toggle [node]="node" />

                <span
                    [innerHTML]="treeControl.getViewValue(node) | kbqHighlightBackground: treeControl.filterValue.value"
                ></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeDescendantsSubcategoriesExample extends TreeParams {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = ['docs'];

    readonly tree = viewChild.required(KbqTreeSelection);

    constructor() {
        super();
    }

    onSelectionChange($event: KbqTreeSelectionChange<KbqTreeOption>) {
        const option: KbqTreeOption = $event.option;

        if (option.isExpandable) {
            this.tree().setStateChildren(option, !option.selected);
        }

        this.toggleParents(($event.option.data as any)?.parent);
    }

    private toggleParents(parent) {
        if (!parent) {
            return;
        }

        const descendants = this.treeControl.getDescendants(parent);
        const isParentSelected = this.tree().selectionModel.selected.includes(parent);

        if (!isParentSelected && descendants.every((d: any) => this.tree().selectionModel.selected.includes(d))) {
            this.tree().selectionModel.select(parent);
            this.toggleParents(parent.parent);
        } else if (isParentSelected) {
            this.tree().selectionModel.deselect(parent);
            this.toggleParents(parent.parent);
        }
    }
}
```

#### Independent items

The second tree behavior algorithm — levels that are independent of each other.

**When to use**

All items are independent of each other, simply visually grouped in a tree.

For example, in a system where users can assign tags, and these tags can be organized into folders and subfolders. Assigning a child tag to an object does not mean the folder object should also be assigned. And conversely, assigning a folder tag has no effect on assigning a child tag.

**How it works**

No checkboxes affect each other.

**How to save to the model and select matcher**

Checked checkboxes are saved.

**Example: Tree multiple checkbox** (`tree-multiple-checkbox`)

```ts tree-multiple-checkbox-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqIconModule } from '@koobiq/components/icon';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        mosaic: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'components-dev': {
            alert: '',
            badge: ''
        },
        'koobiq-examples': '',
        'koobiq-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

@Component({
    selector: 'tree-multiple-checkbox-example',
    imports: [
        KbqTreeModule,
        FormsModule,
        KbqIconModule
    ],
    template: `
        <kbq-tree-selection
            multiple="checkbox"
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [(ngModel)]="modelValue"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'tests'">
                <i kbq-icon="kbq-circle-info_16"></i>

                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <i kbq-icon="kbq-circle-info_16"></i>

                <kbq-tree-node-toggle [node]="node" />

                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeMultipleCheckboxExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = [];

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): string => {
        return node.name;
    };

    private getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };
}
```

#### Access rights

The third tree behavior variant is an extension of the "Child subcategories" variant.

**When to use**

When you need to assign access rights to a tree of objects. Both rules apply:

1. Granting access rights to a parent object also grants rights to child items.
2. It is important to preserve the explicit selection of child items, even when parents are selected. For example, rights to `group 1.1` were granted first, then rights to group 1 were granted. If the system requires that the rights to 1.1 be preserved, this algorithm should be applied.

**How it works**

Similar to the "Child subcategories" variant, but with one restriction.
When a parent is selected, child items are not just checked — they are also disabled.
Only what is explicitly checked is saved.

In addition, if an item was selected and then its parent was selected, the siblings of that item will be selected and disabled, while the explicitly selected item remains enabled. This indicates that a specific rule is set on it.
If the checkbox on such an item is unchecked, it becomes disabled. To restore its explicit assignment, the parent's checkbox must be unchecked.

- Single non-expandable node
  - On selection:
    - Highlighted in blue; the node is not blocked.
  - On deselection:
    - If no parent is selected, simply deselected.
    - If any parent node is selected, selection and disabling occurs (becomes gray).
- Expandable node
  - On selection:
    - All child nodes that are not selected become selected and disabled (become gray).
    - If a child is already selected, nothing happens.
  - On deselection:
    - If any parent node is selected, selection and disabling occurs (becomes gray).
    - If a child node was explicitly selected and highlighted in blue, it remains untouched, while other child nodes are deselected (note that some disabled nodes may be inside nodes selected via singleSelection).
    - If N+1 level child nodes are disabled, deselection and node activation occur.
    - If an N+1 level child node is expandable, it is recursively checked.
- **Difference from "Child subcategories":** If all child nodes are selected, this does not mean the parent node is automatically selected.

**How to save to the model and select matcher**

Only explicitly checked items are saved. Disabled items are not saved.

**Example: Tree access rights** (`tree-access-rights`)

```ts tree-access-rights-example.ts
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
    FlatTreeControl,
    KbqTreeFlatDataSource,
    KbqTreeFlattener,
    KbqTreeModule,
    KbqTreeSelection
} from '@koobiq/components/tree';
import { Subscription } from 'rxjs';

export class FileNode {
    children: FileNode[];
    name: string;
    type: any;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    type: any;
    level: number;
    expandable: boolean;
    parent: any;
}

const allDescendantsSelected = <T>(descendants: T[], selectionList: any) => {
    return descendants.every((descendant) => selectionList.includes(descendant));
};

const recursiveDeselect = (node: any, control: FlatTreeControl<any>) => {
    const descendants: any[] = control.getDescendants(node);
    const descendantsForDeselection: any[] = [];

    for (const descendant of descendants) {
        if (descendant.disabled && descendant.level === node.level + 1) {
            descendantsForDeselection.push(descendant);

            if (descendant.expandable) {
                descendantsForDeselection.push(...recursiveDeselect(descendant, control));
            }

            descendantsForDeselection.push(descendant);
        }
    }

    return descendantsForDeselection;
};

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(value: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const k of Object.keys(value)) {
        const v = value[k];
        const node = new FileNode();

        node.name = `${k}`;

        if (v === null || v === undefined) {
            // no action
        } else if (typeof v === 'object') {
            node.children = buildFileTree(v, level + 1);
        } else {
            node.type = v;
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = {
    docs: 'app',
    src: {
        cdk: {
            a11ly: {
                'aria describer': {
                    'aria-describer': 'ts',
                    'aria-describer.spec': 'ts',
                    'aria-reference': 'ts',
                    'aria-reference.spec': 'ts'
                },
                'focus monitor': {
                    'focus-monitor': 'ts',
                    'focus-monitor.spec': 'ts'
                }
            }
        },
        documentation: {
            source: '',
            tools: ''
        },
        koobiq: {
            autocomplete: '',
            button: '',
            'button-toggle': '',
            index: 'ts',
            package: 'json',
            version: 'ts'
        },
        'koobiq-dev': {
            alert: '',
            badge: ''
        },
        'koobiq-examples': '',
        'koobiq-moment-adapter': '',
        README: 'md',
        'tsconfig.build': 'json'
    },
    scripts: {
        deploy: {
            'cleanup-preview': 'ts',
            'publish-artifacts': 'sh',
            'publish-docs': 'sh',
            'publish-docs-preview': 'ts'
        },
        'tsconfig.deploy': 'json'
    },
    tests: ''
};

abstract class TreeParams {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;
    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;
    treeData: FileNode[];

    constructor() {
        this.treeControl = new FlatTreeControl(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue,
            undefined,
            undefined,
            this.isDisabled
        );
        this.treeFlattener = new KbqTreeFlattener<FileNode, FileFlatNode>(
            this.transformer,
            this.getLevel,
            this.isExpandable,
            this.getChildren
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);
        this.dataSource.data = this.treeData = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    getLevel = (node: FileFlatNode) => node.level;

    getValue = (node: FileFlatNode) => node.name;

    getViewValue = (node: FileFlatNode): string => {
        const nodeType = node.type ? `.${node.type}` : '';

        return `${node.name}${nodeType}`;
    };

    isDisabled = (node: any): boolean => {
        return node.disabled;
    };

    isExpandable = (node: FileFlatNode) => node.expandable;

    getChildren = (node: FileNode) => node.children;

    transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.type = node.type;
        flatNode.level = level;
        flatNode.expandable = !!node.children?.length;

        return flatNode;
    };
}

@Component({
    selector: 'tree-access-rights-example',
    imports: [
        KbqTreeModule,
        FormsModule
    ],
    template: `
        <kbq-tree-selection
            multiple="checkbox"
            [autoSelect]="false"
            [dataSource]="dataSource"
            [ngModel]="modelValue"
            [treeControl]="treeControl"
            (ngModelChange)="onModelChange($event)"
        >
            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding>
                <span>{{ treeControl.getViewValue(node) }}</span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node; when: hasChild" kbqTreeNodePadding>
                <kbq-tree-node-toggle [node]="node" />

                <span>{{ treeControl.getViewValue(node) }}</span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeAccessRightsExample extends TreeParams implements AfterViewInit, OnDestroy {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = [];
    filteredModel: any = [];

    selectionChangeSubscription: Subscription = Subscription.EMPTY;

    readonly tree = viewChild.required(KbqTreeSelection);

    constructor() {
        super();
    }

    ngAfterViewInit(): void {
        const handleSingleOption = (items, cb) => {
            // Process only when a single item is selected
            if (items.length === 1) {
                const singleOrParent: any = items[0];

                cb(singleOrParent);
            }
        };

        this.selectionChangeSubscription = this.tree().selectionModel.changed.subscribe(({ added, removed }) => {
            handleSingleOption(added, (singleItemOrParent: any) => {
                if (singleItemOrParent.expandable) {
                    const descendants = this.treeControl.getDescendants(singleItemOrParent);

                    if (!allDescendantsSelected(descendants, this.tree().selectionModel.selected)) {
                        descendants.forEach((descendant) => {
                            const previouslySelected = (descendant as any)?.singleSelected;

                            if (!previouslySelected) {
                                (descendant as any).disabled = true;
                            }
                        });
                        setTimeout(() => this.tree().selectionModel.select(...(descendants as any[])));
                    }
                }

                // check after descendants selected with parent selection
                setTimeout(() => {
                    if (!singleItemOrParent.disabled) {
                        singleItemOrParent.singleSelected = true;
                    }
                });
            });

            handleSingleOption(removed, (singleItemOrParent: any) => {
                const parents = this.treeControl.getParents(singleItemOrParent, []);

                const descendantsForDeselection: any[] = recursiveDeselect(singleItemOrParent, this.treeControl);

                if (descendantsForDeselection.length) {
                    this.tree().selectionModel.deselect(...descendantsForDeselection);
                    descendantsForDeselection.forEach((descendant) => (descendant.disabled = undefined));
                }

                // set disabled if any parent selected
                if (
                    singleItemOrParent.singleSelected &&
                    parents.some((parent: any) => this.tree().selectionModel.selected.includes(parent))
                ) {
                    singleItemOrParent.disabled = true;
                    // Delay selection for the next loop after parent deselection
                    setTimeout(() => {
                        this.tree().selectionModel.select(singleItemOrParent);
                    }, 0);
                }

                // unselect in any case
                setTimeout(() => {
                    singleItemOrParent.singleSelected = undefined;
                }, 0);
            });
        });
    }

    ngOnDestroy(): void {
        this.selectionChangeSubscription.unsubscribe();
    }

    onModelChange($event) {
        this.modelValue = $event;
        this.filteredModel = this.tree()
            .selectionModel.selected.filter((option: any) => !option.disabled)
            .map((node: any) => this.treeControl.getValue(node));
    }
}
```

### Selection and marking

Clicking an item selects it and updates the information in the right panel. Clicking the checkbox marks the item, but the information in the right panel does not change.

The list can also be navigated with the keyboard. Enter selects an item and updates the text in the right panel, while Space marks it. The selected item changes along with navigation.

**Example: tree-select-and-mark** (`tree-select-and-mark`)

```ts tree-select-and-mark-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqPseudoCheckboxModule } from '@koobiq/components/core';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    description: string;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    description: string;
    level: number;
    expandable: boolean;
    parent: any;
    checked: boolean;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(countries: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const country of countries) {
        const node = new FileNode();

        node.name = country.name;
        node.description = country.description;

        if (country.clubs && country.clubs.length) {
            node.children = buildFileTree(country.clubs, level + 1);
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = [
    {
        name: 'England',
        description:
            "England is considered the birthplace of modern football, with the Football Association (FA) founded in 1863. The English Premier League, established in 1992, is one of the most watched sports leagues globally. England won its first and only World Cup in 1966. The country has a rich football culture with intense local rivalries and historic stadiums. English football is known for its physicality, pace, and passionate fan support. The FA Cup, first held in 1871, is the world's oldest national football competition.'",
        clubs: [
            {
                name: 'Arsenal',
                description:
                    "Founded in 1886 as Dial Square, Arsenal is one of England's most successful clubs with 13 league titles. Based in North London, they're known as 'The Gunners'. Arsène Wenger's 'Invincibles' of 2003-04 went an entire Premier League season unbeaten. The club has a reputation for attractive, attacking football and has developed many world-class players. Their home, the Emirates Stadium, is one of England's finest football arenas. Arsenal's rivalry with Tottenham Hotspur is among English football's most intense derbies."
            },
            {
                name: 'Chelsea',
                description:
                    "Founded in 1905, Chelsea rose to global prominence after Roman Abramovich's 2003 takeover. Based in West London, they've won multiple Premier League titles and two Champions League trophies (2012, 2021). Known as 'The Blues', their Stamford Bridge stadium has been home since formation. Chelsea became known for defensive solidity under José Mourinho and later for developing young talent. The club holds the record for most consecutive home games unbeaten in English football (86 from 2004-2008). Their rivalry with nearby clubs like Fulham and QPR is fierce"
            },
            {
                name: 'Liverpool',
                description:
                    "Liverpool FC, founded in 1892, is England's most successful club in European competitions with 6 Champions League titles. Based at Anfield, their anthem 'You'll Never Walk Alone' is world-famous. The club dominated English football in the 1970s-80s under Bill Shankly and Bob Paisley. The Hillsborough disaster (1989) profoundly impacted the club and football safety standards. Jürgen Klopp's modern team restored Liverpool to European elite status. Their rivalry with Everton (Merseyside derby) and Manchester United are among football's most passionate"
            }
        ]
    },
    {
        name: 'Germany',
        description:
            "German football is renowned for its efficiency, strong youth development, and fan-friendly stadium culture. The Bundesliga, founded in 1963, boasts the highest average attendance globally. Germany has won 4 World Cups (1954, 1974, 1990, 2014) and 3 European Championships. The country revolutionized football tactics with innovations like 'gegenpressing'. German clubs are majority fan-owned under the 50+1 rule. The DFB-Pokal is Germany's premier cup competition. German football recovered strongly after post-war division, with East Germany having its own football structure until reunification.'",
        clubs: [
            {
                name: 'Bayern Munich',
                description:
                    "Germany's most successful club, Bayern Munich was founded in 1900 and dominates the Bundesliga with over 30 titles. Based at the Allianz Arena, they've won 6 Champions League trophies. Known for developing world-class players like Beckenbauer, Müller, and Lahm. Their rivalry with Borussia Dortmund defines modern German football. Under Uli Hoeneß, Bayern became a global brand while maintaining local traditions. The club's 1970s team revolutionized European football. Bayern's women's team is also among Germany's best. Their motto 'Mia san mia' (We are who we are) reflects Bavarian identity"
            },
            {
                name: 'Wolfsburg',
                description:
                    "VfL Wolfsburg, founded in 1945, is unique as being originally a works team for Volkswagen. Based in Lower Saxony, they won their only Bundesliga title in 2008-09 under Felix Magath. The club is known for developing young talents and has a strong women's team that dominates German women's football. Their home stadium is the Volkswagen Arena. Wolfsburg's rise reflects Germany's industrial football culture. The club has participated in European competitions multiple times. Their green-and-white colors match Volkswagen's corporate identity, maintaining close ties with the automaker."
            }
        ]
    },
    {
        name: 'Spain',
        description:
            "Spanish football is characterized by technical skill, tactical innovation, and fierce regional rivalries. La Liga, founded in 1929, features some of the world's best clubs. Spain's national team dominated world football from 2008-2012, winning two European Championships and a World Cup with their 'tiki-taka' style. The Copa del Rey is Spain's premier cup competition. Spanish clubs have been exceptionally successful in European competitions. The country's football culture reflects its diverse regional identities, with historic clubs representing different linguistic and cultural communities within Spain.",
        clubs: [
            {
                name: 'Atletico Madrid',
                description:
                    "Founded in 1903, Atlético Madrid is known as 'Los Colchoneros' (The Mattress Makers) due to their striped jerseys. Based at the Wanda Metropolitano, they've won 11 La Liga titles, often challenging Barcelona and Real Madrid. Under Diego Simeone, they became known for defensive solidity and intense work ethic. The club has won 3 Europa Leagues and reached 3 Champions League finals. Their identity is built on fighting spirit and overcoming financial limitations. Atlético's rivalry with Real Madrid is among Europe's most intense city derbies. The club developed world-class strikers like Fernando Torres and Antoine Griezmann."
            },
            {
                name: 'Barcelona',
                description:
                    "FC Barcelona, founded in 1899, is more than a club ('Més que un club') representing Catalan identity. Based at Camp Nou, they've won 5 Champions League titles and 26 La Liga titles. Their La Masia academy produced legends like Messi, Xavi, and Iniesta. Johan Cruyff's 'Dream Team' in the 1990s established Barcelona's commitment to attacking football. The club pioneered the tiki-taka style under Pep Guardiola. Barcelona's rivalry with Real Madrid (El Clásico) is football's biggest club match. The club operates as a membership society, owned by its fans. Their 2009 sextuple remains unmatched in football history."
            },
            {
                name: 'Real Madrid',
                description:
                    "Real Madrid, founded in 1902, is the most successful club in Champions League history with 14 titles. Based at the Santiago Bernabéu, they've won 35 La Liga titles. The club was instrumental in creating the European Cup in the 1950s. Their all-white kit and Galácticos transfer policy are legendary. Alfredo Di Stéfano, Cristiano Ronaldo, and Zinedine Zidane are among their icons. Under Florentino Pérez, Madrid became a global sporting brand. Their rivalry with Barcelona mixes football with Spain's political tensions. The club was named 'Real' (Royal) by King Alfonso XIII in 1920"
            }
        ]
    }
];

@Component({
    selector: 'tree-select-and-mark-example',
    imports: [
        KbqTreeModule,
        FormsModule,
        KbqPseudoCheckboxModule
    ],
    template: `
        <div class="layout-row">
            <div class="flex-30">
                <kbq-tree-selection [dataSource]="dataSource" [treeControl]="treeControl" [(ngModel)]="modelValue">
                    <kbq-tree-option
                        *kbqTreeNodeDef="let node"
                        kbqTreeNodePadding
                        [class.kbq-checked]="node.checked"
                        (keydown.space)="toggleCheckboxState($event, node)"
                    >
                        <kbq-pseudo-checkbox [state]="node.checked" (click)="toggleCheckboxState($event, node)" />
                        <span [innerHTML]="treeControl.getViewValue(node)"></span>
                    </kbq-tree-option>

                    <kbq-tree-option
                        *kbqTreeNodeDef="let node; when: hasChild"
                        kbqTreeNodePadding
                        [class.kbq-checked]="node.checked"
                        (keydown.space)="toggleCheckboxState($event, node)"
                    >
                        <kbq-tree-node-toggle [node]="node" />

                        <kbq-pseudo-checkbox [state]="node.checked" (click)="toggleCheckboxState($event, node)" />
                        <span [innerHTML]="treeControl.getViewValue(node)"></span>
                    </kbq-tree-option>
                </kbq-tree-selection>
            </div>

            <div class="layout-padding-left-l layout-padding-right-l flex-70">
                <div class="kbq-subheading layout-padding-bottom-s">{{ modelValue.name }}</div>
                <div class="kbq-text-normal">{{ modelValue.description }}</div>
            </div>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeSelectAndMarkExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = '';

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.description = node.description;
        flatNode.parent = parent;
        flatNode.level = level;
        flatNode.expandable = !!node.children;
        flatNode.checked = false;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): FileFlatNode => {
        return node;
    };

    private getViewValue = (node: FileFlatNode): string => {
        return `${node.name}`;
    };

    toggleCheckboxState(event: Event, node: FileFlatNode) {
        event.preventDefault();
        event.stopPropagation();

        node.checked = !node.checked;
    }
}
```

### Selection separate from focus

When navigating the list, the selected item does not change along with the focus position. An item can be selected by clicking or pressing Enter.

**Example: tree-selection-separate-from-focus** (`tree-selection-separate-from-focus`)

```ts tree-selection-separate-from-focus-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqPseudoCheckboxModule } from '@koobiq/components/core';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
    description: string;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    description: string;
    level: number;
    expandable: boolean;
    parent: any;
    checked: boolean;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(countries: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const country of countries) {
        const node = new FileNode();

        node.name = country.name;
        node.description = country.description;

        if (country.clubs && country.clubs.length) {
            node.children = buildFileTree(country.clubs, level + 1);
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = [
    {
        name: 'England',
        description:
            "England is considered the birthplace of modern football, with the Football Association (FA) founded in 1863. The English Premier League, established in 1992, is one of the most watched sports leagues globally. England won its first and only World Cup in 1966. The country has a rich football culture with intense local rivalries and historic stadiums. English football is known for its physicality, pace, and passionate fan support. The FA Cup, first held in 1871, is the world's oldest national football competition.'",
        clubs: [
            {
                name: 'Arsenal',
                description:
                    "Founded in 1886 as Dial Square, Arsenal is one of England's most successful clubs with 13 league titles. Based in North London, they're known as 'The Gunners'. Arsène Wenger's 'Invincibles' of 2003-04 went an entire Premier League season unbeaten. The club has a reputation for attractive, attacking football and has developed many world-class players. Their home, the Emirates Stadium, is one of England's finest football arenas. Arsenal's rivalry with Tottenham Hotspur is among English football's most intense derbies."
            },
            {
                name: 'Chelsea',
                description:
                    "Founded in 1905, Chelsea rose to global prominence after Roman Abramovich's 2003 takeover. Based in West London, they've won multiple Premier League titles and two Champions League trophies (2012, 2021). Known as 'The Blues', their Stamford Bridge stadium has been home since formation. Chelsea became known for defensive solidity under José Mourinho and later for developing young talent. The club holds the record for most consecutive home games unbeaten in English football (86 from 2004-2008). Their rivalry with nearby clubs like Fulham and QPR is fierce"
            },
            {
                name: 'Liverpool',
                description:
                    "Liverpool FC, founded in 1892, is England's most successful club in European competitions with 6 Champions League titles. Based at Anfield, their anthem 'You'll Never Walk Alone' is world-famous. The club dominated English football in the 1970s-80s under Bill Shankly and Bob Paisley. The Hillsborough disaster (1989) profoundly impacted the club and football safety standards. Jürgen Klopp's modern team restored Liverpool to European elite status. Their rivalry with Everton (Merseyside derby) and Manchester United are among football's most passionate"
            }
        ]
    },
    {
        name: 'Germany',
        description:
            "German football is renowned for its efficiency, strong youth development, and fan-friendly stadium culture. The Bundesliga, founded in 1963, boasts the highest average attendance globally. Germany has won 4 World Cups (1954, 1974, 1990, 2014) and 3 European Championships. The country revolutionized football tactics with innovations like 'gegenpressing'. German clubs are majority fan-owned under the 50+1 rule. The DFB-Pokal is Germany's premier cup competition. German football recovered strongly after post-war division, with East Germany having its own football structure until reunification.'",
        clubs: [
            {
                name: 'Bayern Munich',
                description:
                    "Germany's most successful club, Bayern Munich was founded in 1900 and dominates the Bundesliga with over 30 titles. Based at the Allianz Arena, they've won 6 Champions League trophies. Known for developing world-class players like Beckenbauer, Müller, and Lahm. Their rivalry with Borussia Dortmund defines modern German football. Under Uli Hoeneß, Bayern became a global brand while maintaining local traditions. The club's 1970s team revolutionized European football. Bayern's women's team is also among Germany's best. Their motto 'Mia san mia' (We are who we are) reflects Bavarian identity"
            },
            {
                name: 'Wolfsburg',
                description:
                    "VfL Wolfsburg, founded in 1945, is unique as being originally a works team for Volkswagen. Based in Lower Saxony, they won their only Bundesliga title in 2008-09 under Felix Magath. The club is known for developing young talents and has a strong women's team that dominates German women's football. Their home stadium is the Volkswagen Arena. Wolfsburg's rise reflects Germany's industrial football culture. The club has participated in European competitions multiple times. Their green-and-white colors match Volkswagen's corporate identity, maintaining close ties with the automaker."
            }
        ]
    },
    {
        name: 'Spain',
        description:
            "Spanish football is characterized by technical skill, tactical innovation, and fierce regional rivalries. La Liga, founded in 1929, features some of the world's best clubs. Spain's national team dominated world football from 2008-2012, winning two European Championships and a World Cup with their 'tiki-taka' style. The Copa del Rey is Spain's premier cup competition. Spanish clubs have been exceptionally successful in European competitions. The country's football culture reflects its diverse regional identities, with historic clubs representing different linguistic and cultural communities within Spain.",
        clubs: [
            {
                name: 'Atletico Madrid',
                description:
                    "Founded in 1903, Atlético Madrid is known as 'Los Colchoneros' (The Mattress Makers) due to their striped jerseys. Based at the Wanda Metropolitano, they've won 11 La Liga titles, often challenging Barcelona and Real Madrid. Under Diego Simeone, they became known for defensive solidity and intense work ethic. The club has won 3 Europa Leagues and reached 3 Champions League finals. Their identity is built on fighting spirit and overcoming financial limitations. Atlético's rivalry with Real Madrid is among Europe's most intense city derbies. The club developed world-class strikers like Fernando Torres and Antoine Griezmann."
            },
            {
                name: 'Barcelona',
                description:
                    "FC Barcelona, founded in 1899, is more than a club ('Més que un club') representing Catalan identity. Based at Camp Nou, they've won 5 Champions League titles and 26 La Liga titles. Their La Masia academy produced legends like Messi, Xavi, and Iniesta. Johan Cruyff's 'Dream Team' in the 1990s established Barcelona's commitment to attacking football. The club pioneered the tiki-taka style under Pep Guardiola. Barcelona's rivalry with Real Madrid (El Clásico) is football's biggest club match. The club operates as a membership society, owned by its fans. Their 2009 sextuple remains unmatched in football history."
            },
            {
                name: 'Real Madrid',
                description:
                    "Real Madrid, founded in 1902, is the most successful club in Champions League history with 14 titles. Based at the Santiago Bernabéu, they've won 35 La Liga titles. The club was instrumental in creating the European Cup in the 1950s. Their all-white kit and Galácticos transfer policy are legendary. Alfredo Di Stéfano, Cristiano Ronaldo, and Zinedine Zidane are among their icons. Under Florentino Pérez, Madrid became a global sporting brand. Their rivalry with Barcelona mixes football with Spain's political tensions. The club was named 'Real' (Royal) by King Alfonso XIII in 1920"
            }
        ]
    }
];

@Component({
    selector: 'tree-selection-separate-from-focus-example',
    imports: [
        KbqTreeModule,
        FormsModule,
        KbqPseudoCheckboxModule
    ],
    template: `
        <div class="layout-row">
            <div class="flex-30">
                <kbq-tree-selection
                    [autoSelect]="false"
                    [dataSource]="dataSource"
                    [treeControl]="treeControl"
                    [(ngModel)]="modelValue"
                >
                    <kbq-tree-option
                        *kbqTreeNodeDef="let node"
                        kbqTreeNodePadding
                        [class.kbq-checked]="node.checked"
                        (keydown.space)="toggleCheckboxState($event, node)"
                    >
                        <kbq-pseudo-checkbox [state]="node.checked" (click)="toggleCheckboxState($event, node)" />
                        <span [innerHTML]="treeControl.getViewValue(node)"></span>
                    </kbq-tree-option>

                    <kbq-tree-option
                        *kbqTreeNodeDef="let node; when: hasChild"
                        kbqTreeNodePadding
                        [class.kbq-checked]="node.checked"
                        (keydown.space)="toggleCheckboxState($event, node)"
                    >
                        <kbq-tree-node-toggle [node]="node" />

                        <kbq-pseudo-checkbox [state]="node.checked" (click)="toggleCheckboxState($event, node)" />
                        <span [innerHTML]="treeControl.getViewValue(node)"></span>
                    </kbq-tree-option>
                </kbq-tree-selection>
            </div>

            <div class="layout-padding-left-l layout-padding-right-l flex-70">
                <div class="kbq-subheading layout-padding-bottom-s">{{ modelValue.name }}</div>
                <div class="kbq-text-normal">{{ modelValue.description }}</div>
            </div>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeSelectionSeparateFromFocusExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: FileFlatNode;

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
        this.modelValue = this.treeControl.dataNodes[0];
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.description = node.description;
        flatNode.parent = parent;
        flatNode.level = level;
        flatNode.expandable = !!node.children;
        flatNode.checked = false;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): FileFlatNode => {
        return node;
    };

    private getViewValue = (node: FileFlatNode): string => {
        return `${node.name}`;
    };

    toggleCheckboxState(event: Event, node: FileFlatNode) {
        event.preventDefault();
        event.stopPropagation();

        node.checked = !node.checked;
    }
}
```

### Expanding a branch on click

Clicking an element expands the first level of child nodes, only leaf elements can be selected. Selection is disabled with the `selectable` attribute of `kbq-tree-option`: with `[selectable]="false"` the option remains focusable, navigable and expandable, but cannot be selected.

**Example: tree-select-and-mark** (`tree-toggle-on-click`)

```ts tree-toggle-on-click-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqPseudoCheckboxModule } from '@koobiq/components/core';
import { FlatTreeControl, KbqTreeFlatDataSource, KbqTreeFlattener, KbqTreeModule } from '@koobiq/components/tree';

export class FileNode {
    children: FileNode[];
    name: string;
}

/** Flat node with expandable and level information */
export class FileFlatNode {
    name: string;
    description: string;
    level: number;
    expandable: boolean;
    parent: any;
}

/**
 * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
 * The return value is the list of `FileNode`.
 */
export function buildFileTree(countries: any, level: number): FileNode[] {
    const data: any[] = [];

    for (const country of countries) {
        const node = new FileNode();

        node.name = country.name;

        if (country.children && country.children.length) {
            node.children = buildFileTree(country.children, level + 1);
        }

        data.push(node);
    }

    return data;
}

export const DATA_OBJECT = [
    { name: 'Lonely leaf' },
    {
        name: 'Branch 1',
        children: [
            {
                name: 'Arsenal'
            },
            {
                name: 'Chelsea'
            },
            {
                name: 'Liverpool'
            },
            {
                name: 'Manchester City'
            },
            {
                name: 'Tottenham Hotspur'
            }
        ]
    },
    {
        name: 'Branch 2',
        children: [
            {
                name: 'Atlético de Madrid'
            },
            {
                name: 'FC Barcelona'
            },
            {
                name: 'Real Betis'
            },
            {
                name: 'Real Madrid'
            },
            {
                name: 'Sevilla FC'
            },
            {
                name: 'Valencia CF'
            }
        ]
    },
    {
        name: 'Disabled Node'
    }
];

@Component({
    selector: 'tree-toggle-on-click-example',
    imports: [
        KbqTreeModule,
        FormsModule,
        KbqPseudoCheckboxModule
    ],
    template: `
        <kbq-tree-selection
            [dataSource]="dataSource"
            [treeControl]="treeControl"
            [autoSelect]="false"
            [(ngModel)]="modelValue"
        >
            <kbq-tree-option
                *kbqTreeNodeDef="let node; when: hasChild"
                kbqTreeNodeToggle
                kbqTreeNodePadding
                [selectable]="false"
            >
                <kbq-tree-node-toggle [node]="node" />

                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>

            <kbq-tree-option *kbqTreeNodeDef="let node" kbqTreeNodePadding [disabled]="node.name === 'Disabled Node'">
                <span [innerHTML]="treeControl.getViewValue(node)"></span>
            </kbq-tree-option>
        </kbq-tree-selection>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeToggleOnClickExample {
    treeControl: FlatTreeControl<FileFlatNode>;
    treeFlattener: KbqTreeFlattener<FileNode, FileFlatNode>;

    dataSource: KbqTreeFlatDataSource<FileNode, FileFlatNode>;

    modelValue: any = '';

    constructor() {
        this.treeFlattener = new KbqTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);

        this.treeControl = new FlatTreeControl<FileFlatNode>(
            this.getLevel,
            this.isExpandable,
            this.getValue,
            this.getViewValue
        );
        this.dataSource = new KbqTreeFlatDataSource(this.treeControl, this.treeFlattener);

        this.dataSource.data = buildFileTree(DATA_OBJECT, 0);
    }

    hasChild(_: number, nodeData: FileFlatNode) {
        return nodeData.expandable;
    }

    private transformer = (node: FileNode, level: number, parent: any) => {
        const flatNode = new FileFlatNode();

        flatNode.name = node.name;
        flatNode.parent = parent;
        flatNode.level = level;
        flatNode.expandable = !!node.children;

        return flatNode;
    };

    private getLevel = (node: FileFlatNode) => {
        return node.level;
    };

    private isExpandable = (node: FileFlatNode) => {
        return node.expandable;
    };

    private getChildren = (node: FileNode): FileNode[] => {
        return node.children;
    };

    private getValue = (node: FileFlatNode): FileFlatNode => {
        return node;
    };

    private getViewValue = (node: FileFlatNode): string => {
        return `${node.name}`;
    };
}
```
