Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree #tree [dataSource]="dataSource" [childrenAccessor]="childrenAccessor">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<button matIconButton disabled></button>
{{node.item}}
{{node.name}}
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding matTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.item">
<button matIconButton
[attr.aria-label]="'Toggle ' + node.item" matTreeNodeToggle>
[cdkTreeNodeTypeaheadLabel]="node.name" (expandedChange)="onNodeExpanded(node, $event)">
<button matIconButton [attr.aria-label]="'Toggle ' + node.name" matTreeNodeToggle>
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
{{tree.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.item}}
{{node.name}}
@if (node.isLoading()) {
<mat-progress-bar
mode="indeterminate"
class="example-tree-progress-bar"></mat-progress-bar>
<mat-progress-bar mode="indeterminate" class="example-tree-progress-bar"></mat-progress-bar>
}
</mat-tree-node>
</mat-tree>
</mat-tree>
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import {CollectionViewer, SelectionChange, DataSource} from '@angular/cdk/collections';
import {FlatTreeControl} from '@angular/cdk/tree';
import {ChangeDetectionStrategy, Component, Injectable, inject, signal} from '@angular/core';
import {BehaviorSubject, merge, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {MatTreeModule} from '@angular/material/tree';

/** Flat node with expandable and level information */
class DynamicFlatNode {
constructor(
public item: string,
public level = 1,
public expandable = false,
public isLoading = signal(false),
) {}
/** Node with expandable and level information */
interface DynamicNode {
name: string;
level: number;
expandable: boolean;
isLoading: ReturnType<typeof signal<boolean>>;
children?: DynamicNode[];
}

/**
Expand All @@ -34,102 +29,26 @@ export class DynamicDatabase {
rootLevelNodes: string[] = ['Fruits', 'Vegetables'];

/** Initial data from database */
initialData(): DynamicFlatNode[] {
return this.rootLevelNodes.map(name => new DynamicFlatNode(name, 0, true));
initialData(): DynamicNode[] {
return this.rootLevelNodes.map(name => this.createNode(name, 0, true));
}

getChildren(node: string): string[] | undefined {
return this.dataMap.get(node);
createNode(name: string, level: number, expandable: boolean): DynamicNode {
return {
name,
level,
expandable,
isLoading: signal(false),
children: undefined,
};
}

isExpandable(node: string): boolean {
return this.dataMap.has(node);
getChildren(name: string): string[] | undefined {
return this.dataMap.get(name);
}
}
/**
* File database, it can build a tree structured Json object from string.
* Each node in Json object represents a file or a directory. For a file, it has filename and type.
* For a directory, it has filename and children (a list of files or directories).
* The input will be a json object string, and the output is a list of `FileNode` with nested
* structure.
*/
export class DynamicDataSource implements DataSource<DynamicFlatNode> {
dataChange = new BehaviorSubject<DynamicFlatNode[]>([]);

get data(): DynamicFlatNode[] {
return this.dataChange.value;
}
set data(value: DynamicFlatNode[]) {
this._treeControl.dataNodes = value;
this.dataChange.next(value);
}

constructor(
private _treeControl: FlatTreeControl<DynamicFlatNode>,
private _database: DynamicDatabase,
) {}

connect(collectionViewer: CollectionViewer): Observable<DynamicFlatNode[]> {
this._treeControl.expansionModel.changed.subscribe(change => {
if (
(change as SelectionChange<DynamicFlatNode>).added ||
(change as SelectionChange<DynamicFlatNode>).removed
) {
this.handleTreeControl(change as SelectionChange<DynamicFlatNode>);
}
});

return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data));
}

disconnect(collectionViewer: CollectionViewer): void {}

/** Handle expand/collapse behaviors */
handleTreeControl(change: SelectionChange<DynamicFlatNode>) {
if (change.added) {
change.added.forEach(node => this.toggleNode(node, true));
}
if (change.removed) {
change.removed
.slice()
.reverse()
.forEach(node => this.toggleNode(node, false));
}
}

/**
* Toggle the node, remove from display list
*/
toggleNode(node: DynamicFlatNode, expand: boolean) {
const children = this._database.getChildren(node.item);
const index = this.data.indexOf(node);
if (!children || index < 0) {
// If no children, or cannot find the node, no op
return;
}

node.isLoading.set(true);

setTimeout(() => {
if (expand) {
const nodes = children.map(
name => new DynamicFlatNode(name, node.level + 1, this._database.isExpandable(name)),
);
this.data.splice(index + 1, 0, ...nodes);
} else {
let count = 0;
for (
let i = index + 1;
i < this.data.length && this.data[i].level > node.level;
i++, count++
) {}
this.data.splice(index + 1, count);
}

// notify the change
this.dataChange.next(this.data);
node.isLoading.set(false);
}, 1000);
isExpandable(name: string): boolean {
return this.dataMap.has(name);
}
}

Expand All @@ -144,22 +63,37 @@ export class DynamicDataSource implements DataSource<DynamicFlatNode> {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TreeDynamicExample {
constructor() {
const database = inject(DynamicDatabase);
private _database = inject(DynamicDatabase);

this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
this.dataSource = new DynamicDataSource(this.treeControl, database);
dataSource = this._database.initialData();

this.dataSource.data = database.initialData();
}
childrenAccessor = (node: DynamicNode) => node.children ?? [];

treeControl: FlatTreeControl<DynamicFlatNode>;
hasChild = (_: number, node: DynamicNode) => node.expandable;

dataSource: DynamicDataSource;
/**
* Load children on node expansion.
* Called from template via (expandedChange) output.
*/
onNodeExpanded(node: DynamicNode, expanded: boolean): void {
if (!expanded || node.children) {
// Don't reload if collapsing or already loaded
return;
}

getLevel = (node: DynamicFlatNode) => node.level;
const childNames = this._database.getChildren(node.name);
if (!childNames) {
return;
}

isExpandable = (node: DynamicFlatNode) => node.expandable;
node.isLoading.set(true);

hasChild = (_: number, _nodeData: DynamicFlatNode) => _nodeData.expandable;
// Simulate async data loading
setTimeout(() => {
node.children = childNames.map(name =>
this._database.createNode(name, node.level + 1, this._database.isExpandable(name)),
);
node.isLoading.set(false);
}, 1000);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree #tree [dataSource]="dataSource" [childrenAccessor]="childrenAccessor">
<!-- This is the tree node template for leaf nodes -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<!-- use a disabled button to provide padding for tree leaf -->
Expand All @@ -11,7 +11,7 @@
<button matIconButton matTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
{{tree.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.name}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {FlatTreeControl} from '@angular/cdk/tree';
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {MatTreeFlatDataSource, MatTreeFlattener, MatTreeModule} from '@angular/material/tree';
import {MatTreeModule} from '@angular/material/tree';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';

Expand All @@ -13,13 +12,6 @@ interface FoodNode {
children?: FoodNode[];
}

/** Flat node with expandable and level information */
interface ExampleFlatNode {
expandable: boolean;
name: string;
level: number;
}

/**
* @title Tree with flat nodes
*/
Expand All @@ -30,33 +22,11 @@ interface ExampleFlatNode {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TreeFlatOverviewExample {
private _transformer = (node: FoodNode, level: number) => {
return {
expandable: !!node.children && node.children.length > 0,
name: node.name,
level: level,
};
};

treeControl = new FlatTreeControl<ExampleFlatNode>(
node => node.level,
node => node.expandable,
);

treeFlattener = new MatTreeFlattener(
this._transformer,
node => node.level,
node => node.expandable,
node => node.children,
);

dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
dataSource = EXAMPLE_DATA;

constructor() {
this.dataSource.data = EXAMPLE_DATA;
}
childrenAccessor = (node: FoodNode) => node.children ?? [];

hasChild = (_: number, node: ExampleFlatNode) => node.expandable;
hasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0;
}

const EXAMPLE_DATA: FoodNode[] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree #tree [dataSource]="dataSource" [childrenAccessor]="childrenAccessor">
<!-- This is the tree node template for leaf nodes -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<!-- use a disabled button to provide padding for tree leaf -->
Expand All @@ -7,13 +7,12 @@
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding matTreeNodeToggle
[cdkTreeNodeTypeaheadLabel]="node.name">
<button matIconButton matTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.name">
[cdkTreeNodeTypeaheadLabel]="node.name">
<button matIconButton matTreeNodeToggle [attr.aria-label]="'Toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
{{tree.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.name}}
</mat-tree-node>
</mat-tree>
</mat-tree>
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {FlatTreeControl} from '@angular/cdk/tree';
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {MatTreeFlatDataSource, MatTreeFlattener, MatTreeModule} from '@angular/material/tree';
import {MatTreeModule} from '@angular/material/tree';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';

Expand All @@ -9,12 +8,6 @@ interface Node {
children?: Node[];
}

interface ExampleFlatNode {
expandable: boolean;
name: string;
level: number;
}

/**
* @title Testing with MatTreeHarness
*/
Expand All @@ -25,33 +18,11 @@ interface ExampleFlatNode {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TreeHarnessExample {
private _transformer = (node: Node, level: number) => {
return {
expandable: !!node.children && node.children.length > 0,
name: node.name,
level: level,
};
};

treeControl = new FlatTreeControl<ExampleFlatNode>(
node => node.level,
node => node.expandable,
);

treeFlattener = new MatTreeFlattener(
this._transformer,
node => node.level,
node => node.expandable,
node => node.children,
);

dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
dataSource = EXAMPLE_DATA;

constructor() {
this.dataSource.data = EXAMPLE_DATA;
}
childrenAccessor = (node: Node) => node.children ?? [];

hasChild = (_: number, node: ExampleFlatNode) => node.expandable;
hasChild = (_: number, node: Node) => !!node.children && node.children.length > 0;
}

const EXAMPLE_DATA: Node[] = [
Expand Down
Loading