Written by Lewis Cianci✏️
This post was updated by Lewis Cianci on 12 November 2024 to explain how to fix the deprecation warning for TreeControl
which you can find in the migration section below.
There could be a few different reasons you’re here right now reading this article. Maybe you simply saw the title and thought it’d be interesting. Or maybe you’ve been trying to get the Angular <cdk-tree>
to work for what feels like months, your efforts peaking in what can only be described as a hyperfixation.
If the latter case is true, I understand your frustration. You’ve become determined to not let the tree “beat you,” but no matter what you do, bouncing between the various StackOverflow topics, the documentation, and CodePens from ten years ago, the tree just won’t work for you.
Your phone has tens of missed calls from friends checking on your wellbeing. When other people look outside, they see a beautiful sky, birds, wildlife. When you look outside, you see nature as an expanding and collapsing hierarchy. There is no life. There is only tree.
It’s a little dramatic. But the Angular tree is not easy to use or understand. The technical implementation is good — great, even. But the available documentation makes using the tree nigh on impossible.
I wrote this nearly 6,000 word behemoth article for one reason — to attempt to resolve this. Everything in this article has been tested with Angular 18, and even uses updated bits like signals to make life a little easier. To that end, we’re going to take it slow, starting with simple examples and building up from there.
What makes the Angular tree so difficult to use?
Clearly, it seems like I’m ragging on the Angular <cdk-tree>
. That’s not the intent. However, after trying to use the Angular tree for days and being flat-out bewildered, I did wonder: how did it get to this point?
Well, it starts with the official documentation, which I found simultaneously light on details and overly verbose. Reading the docs felt something like reading a pizza recipe that goes into pages of detail about where pizza came from, but stops short of telling you whether to put the cheese on first or last.
The final descent into chaos came from desperately searching online for anyone else who had done this successfully. Instead of answers, I got hit with examples for all versions of Angular and GitHub issues in the Angular repository opened by people saying their tree didn’t work.
Put simply — there’s just no comprehensive guide online on how to use the Angular tree.
Despite this, there are many good reasons to use first-party components like the tree view in Angular! First, writing a new tree from scratch is reinventing the wheel.
Second, whenever you upgrade to the next major version of Angular, using a first-party component from the Angular CDK or Angular Material will (probably 😬) upgrade just fine. You won’t be stuck with a component that hasn’t been updated since it was written five years ago, with dependency issues that you have to sort out.
As an actual component, the tree view can be made to work well in Angular. It works with everything you could ever desire from a tree, like checkboxes, asynchronous loading, the list goes on.
But first, a warning. If you’ve read the Angular CDK documkentation on the tree or the Angular Material overview of the tree component, basically, it’s not relevant to this article.
Unfortunately, the official documentation makes the tree harder to use than it actually is, and doesn’t explain some of the trickier concepts. If you read the official documentation and it didn’t make sense to you, you’re in good company.
And now, because of the sudden deprecation of TreeControl
without pre-warning or migration guide, all of the guides you can search for today will give you outdated results that use TreeControl
. So, yeah, that’s going to hurt.
In the beginning, there was the CDK Tree
Most Angular developers likely know what Angular Material is. But they might give you puzzled stares if you start talking about the Angular CDK, which underpins a lot of the functionality in Angular Material, like scrolling or dragging-and-drop.
The first obvious question that springs to mind is, why? Well, you might love Angular Material, but not everyone likes the styling, and other developers have a certain corporate styling they must work with.
So, the CDK tree gives you the implementation of a tree view in Angular, but without all of the styling, so you can make it look how you want. Meanwhile, the Material tree brings a styled tree view that you could throw into your Angular Material application.
It would be fair to say that they are essentially the same thing. Their main difference is just that the Angular Material tree view has some nice styling on it.
By the end of this article, we’ll have an app that uses the tree, but will also:
- Dynamically load more data into an existing tree
- Have nodes that are selectable, or have checkboxes
- Load children of nodes asynchronously
- Have a hierarchical view of nodes to help you see the relationships between collapsed and expanded nodes
Let’s get into it.
Barking up the wrong (Angular) tree
The first thing we have to address with the tree is that there are two types of trees to use:
- Flat tree: Each node appears sequentially in the DOM. Even when nodes are expanded, they only ever appear “alongside” other nodes
- Nested tree: Each node can contain other trees of nodes. When a node is expanded, the expanded tree is displayed within the parent node
To expand on this, within the browser, the flat tree would look like this:
Did you notice how Node 2 is expanded, but the children are still on individual lines? That’s because each node appears sequentially in the browser, regardless of level. The nested tree, on the other hand, would look more like this:
Ah, wow — it looks exactly the same, apart from that green box. The green box is an outlet for the tree. When you expand a node, its children is rendered into the outlet. If those nodes have children, the process repeats.
So, what one should you use? It will depend based on your use case. But, if I’m being honest, I don’t think the flat tree should even be an option. It’s more complex than the nested tree, with no benefits or advantages. But I’ll get into this in greater detail later on.
Setting up an Angular project with a tree view
Most guides online, including the official Angular documentation, use static data in their tree. However, it’s unlikely that you’ll always know **what you want to be in your tree when you stand it up. Once your app and API start serving up even the most non-trivial amount of data, it will make sense to lazy-load the children in your tree.
To facilitate this requirement, I’ve generated a simple API in Node.js, which provides a long list of data, and other API actions that get details on Pokémon to show more of a practical use case.
Let’s look at the “long list” API first. If we were to call this API endpoint with no parameters, we’ll receive:
[
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
"Item 8",
"Item 9",
"Item 10"
]
If we skip five items and take five items, we’ll receive:
[
"Item 6",
"Item 7",
"Item 8",
"Item 9",
"Item 10"
]
Note that in my example, I’m using OpenAPI (previously Swagger) on the API for documentation, but I’m also using the OpenAPI tooling to generate an API client for my Angular app. Ultimately, however you receive data into your application is up to you. Whether you’re using HTTP get requests or reading from the filesystem, you should be able to adapt the tree for your needs.
One notable thing about the server code: I’ve introduced a delay on responses of three seconds on line 12 of the index.js
file. The reason is because operations that run on the server may be naturally delayed due to a database lookup or similar.
For this tutorial, I’ve artificially created this delay to give the feel of a more realistic real-world application, as well as so that we can see how to set nodes to loading
. The server code is in this GitHub repository for you to review or fork as needed.
Creating the data models
In our case, the server will return LongDataItem
object, which will contain some text and an index number. A standard LongDataItem
object would look like this:
{
text: 'Item 1'
index: 1
}
This is all the information the server has on the node, but it lacks information that we need to make our tree view work. For example, we need to know if a node is expandable, or if it’s loading.
To that end, let’s create a class that has the LongDataItem
object, but also contains the information that we need:
export class TreeNode {
expandable = signal(true);
loading = signal(false);
options = signal<Set<TreeOption>>(new Set<TreeOption>())
constructor(public level: number, public data: LongDataItem) {
}
}
export enum TreeOption {
Last,
Highlighted
}
For our sample, every single node will be expandable. On properties that we expect to change, we use signals, first introduced in Angular 16, so we can update those values in the future. We also use Set
to add or remove options to a given node — for example, if it’s the last node in a tree, or if it’s been highlighted.
Creating a pipe
to help with types
Whether you use FlatTree
or NestedTree
, both options require a dataSource
, and iterate through items in an array using a *cdkTreeNodeDef
attribute. In our component, it looks like this:
<cdk-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding
class="tree-node">
The problem with this, is that every time we use node
in our view, it loses its type information. If we ever update the TreeNode
class with new properties, we risk mistyping the property name.
To resolve this, create a simple pipe and call it AsTreeNode
. This pipe’s objective is to transform whatever it receives into a TreeNode
, which will give us our type information back:
export class AsTreeNodePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]) {
return value as TreeNode;
}
}
While it’s true that this will require a bit more boilerplate on our view (as we’ll have to type node | asTreeNode
instead of just node
), we’ll always know that we’re accessing valid properties on our object. And, with the new @let
syntax, this will become quite ergonomic.
Using a flat tree in Angular
In order to use a flat tree, we first need to define a DataSource
. A DataSource
defines two methods: a connect
method and a disconnect
method.
In a flat tree, the DataSource
is responsible for loading the data, but it’s also responsible for what to do when the tree expands or collapses. Because our tree nodes are just an Array, we need to manually put things in at various places in this array when nodes are expanded, and then remove them when they are collapsed.
If that sounds gross to you, that’s a good sign. As a developer who wants to write quality software, splicing things into arrays isn’t a fun proposition, which is one of the reasons why I don’t like the flat tree. But we’re here to learn, so lets see how it works anyway. So, how do we achieve this? Let’s think through it:
- The
tree
is created. We specify a datasource for the tree. The datasource definesconnect
anddisconnect
methods, which we can use if we have setup or teardown to do (like connecting to a database for our nodes) - Within the datasource, we define functions to load the initial data set, and also what to do when a node is expanded or collapsed
- On each tree node, as defined within the template, we want to subscribe to the
(expandedChange)
event, so we can expand and collapse nodes as required
Creating the DataSource
for our flat tree
Let’s dive into the skeleton of our data source:
class FlatTreeDataSource implements DataSource<TreeNode> {
dataChange = new BehaviorSubject<TreeNode[]>([]);
constructor(private _api: DataService) {
}
get data(): TreeNode[] {
return this.dataChange.value;
}
set data(value: TreeNode[]) {
this._treeControl.dataNodes = value;
this.dataChange.next(value);
}
connect(collectionViewer: CollectionViewer): Observable<TreeNode[]> {
return this.dataChange;
}
disconnect(collectionViewer: CollectionViewer): void {
}
}
It’s pretty basic. Now let’s handle the expansion and contraction of nodes. This is where things get complicated and we have to start splicing things in at certain places in the array.
async toggleNode(node: TreeNode, expand: boolean) {
// Retrieve the index of the node that is asking for expansion
const index = this.data.indexOf(node);
// Set loading to true (show loading indicator)
node.loading.set(true);
// If we are expanding the node...
if (expand) {
// Retrieve nodes from API
let children = await firstValueFrom(this._api.longDataGet(node.data.index, 10));
// Map them to our TreeNode type
let nodes = children.map(x => new TreeNode(node.level + 1, x));
// For the last node in our retrieved list, set the last node option of TreeOption.Last (to show the "Load more..." button)
nodes[nodes.length - 1].options.update(x => x.add(TreeOption.Last));
if (!children || index < 0) {
// If no children, or cannot find the node, no op
return;
}
// Remove existing "last" nodes from existing data
this.data.forEach(x => x.options.update(y => {
y.delete(TreeOption.Last);
return y;
}))
// Insert the newly retrieved data at the right index
this.data.splice(index + 1, 0, ...nodes);
} else {
// Otherwise, if the node is being collapsed, work out how many nodes are children and remove them from the array
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 BehaviourSubject that the data has changed
this.dataChange.next(this.data);
// Set the loading flag back to false
node.loading.set(false);
}
As you can see, every time we want to add or remove nodes from a flat tree, we have to scan the node array for the node we want, and then either insert or remove nodes via splice
from the array. Even our Load more… button, which should be fairly simple, requires the same kind of muck-about:
async loadMore(node: TreeNode){
node.loading.set(true);
let moreNodes = await firstValueFrom(this._api.longDataGet(node.data.index, 10));
let treeNodes = moreNodes.map(x => new TreeNode(node.level, x));
this.data.splice(this.data.indexOf(node), 1, ...treeNodes);
this.dataChange.next(this.data);
node.loading.set(false);
}
Oof — points lost, flat tree. But we’ve come this far, so we can’t just throw it out and do something else. Let’s continue with designing our view for the flat tree.
Designing the flat tree view
To use our tree, we can use the cdk-tree
or mat-tree
with our datasource
specified. We also then use the cdk-tree-node
to specify a template for a given node.
Some other examples — including the official documentation on cdk-tree
or mat-tree
— use two cdk-tree-node
nodes. One is for when the node has children, and the other is for when the node doesn’t have children. There’s simply no need for this, and it will only cause code duplication and some level of confusion.
Instead, we can render every node the same way. Then, if a node has children, we add a button to expand the node. If it doesn’t, we just add an empty disabled button with cdkTreeNodePadding
to give the node appropriate spacing.
Annoyingly, despite Angular’s use of a type-safe(ish) language, the type is typically stripped away when we use let node
. Fortunately, we can add this type information back in with a @let
operator and a pipe.
@if (loading()) {
Loading initial data....
}
<div
style="right: 0; bottom: 0; height: 200px; width: 200px; position: fixed; display: flex; flex-direction: column; justify-items: center; background-color: bisque">
<i style="text-align: center">Selected Nodes</i>
{{ selectedNodes() | json }}
</div>
<cdk-tree [dataSource]="datasource" #tree [levelAccessor]="treeLevelAccessor">
<!-- Below, 'let _node' removes type information :(-->
<cdk-tree-node *cdkTreeNodeDef="let _node"
cdkTreeNodePadding
class="tree-node" (expandedChange)="handleNodeExpansionStateChange($event, _node)" >
<!-- We can re-add the type information with a pipe and @let :) -->
@let node = _node | asTreeNode;
<div style="display: flex; flex-direction: row; align-items: center; justify-items: end">
<!-- All calls to 'node' are now typed as FlatTreeNode -->
@if (hasChild(node)) {
<button mat-icon-button cdkTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.data.item"
[style.visibility]="node.expandable() ? 'visible' : 'hidden'">
<mat-icon class="mat-icon-rtl-mirror">
{{ tree.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</button>
} @else {
<button mat-icon-button disabled cdkTreeNodePadding></button>
}
<input type="checkbox" [value]="node.selected()" (click)="toggleNodeSelect(node)">
{{ node.data.item }}
@if (node.loading()) {
Loading...
}
</div>
@if (node.options().has(TreeOption.Last)) {
<button (click)="loadMore(node)">Load more...</button>
}
</cdk-tree-node>
</cdk-tree>
The result is this: Hey, our tree view works, and our data is loading from the server! Unfortunately, using the FlatTree
in this instance has introduced some problems into our code:
- Heavy reliance on indexes and magic numbers: Everything works as long as you are completely immaculate with your use of indexes and accumulators. The moment you count objects incorrectly, or your math's is off, nodes will get inserted at the wrong position and your entire tree will turn to gobbledygook
- Will scale poorly: A new entry in the DOM for each node is okay for a few nodes, but if you start adding hundreds or thousands of nodes, the browser will gobble up memory and begin to chug. Array operations on a a huge array of nodes will also take a long time
- Difficult to show hierarchical relationships between data: Because each node exists within a flat array, it’s not easy to show dotted lines between nodes to indicate their relationship
For these reasons, I would not recommend the use of FlatTree
for anything but the simplest Angular treeview implementations. Before long, maintaining it could become difficult.
Exploring a better choice: The nested tree
With a nested tree in Angular, you immediately benefit from the fact that — unlike the flat tree — you don’t have to engage in any kind of flattening to produce the tree. Instead, each expanded node is rendered into an outlet that is only rendered if the node is expanded.
The only difficulty in the nested tree comes from the fact that it’s recursive. Each node essentially renders itself, over and over again, as the node is expanded. But we benefit from some clarity and ease-of-understanding in this approach.
We’ll go into this with a new class to represent our nested node, the appropriately named NestedTreeNode
:
export class NestedTreeNode {
children = new BehaviorSubject<Array<NestedTreeNode>>([]);
expandable = signal(true);
loading = signal(false);
options = signal<Set<TreeOption>>(new Set<TreeOption>())
selected = signal(false);
constructor(public level: number, public data: LongDataItem, public parent?: NestedTreeNode) {
}
}
It’s mostly the same, except:
- It has a new
children
property, which is aBehaviorSubject
and is initially empty - The constructor also accepts an optional
parent
argument
Because our nodes are now nested_,_ they each have children of their own. All we have to do is explain to Angular where to find the children and how to render them when it gets there.
The most important thing about this change is that children
is an Observable
, so its value can change over time. This dovetails nicely into our actual component setup, which looks like this:
export class NestedTreeComponent implements OnInit {
nestedDataSource: MatTreeNestedDataSource<NestedTreeNode>;
private subscription?: Subscription;
constructor(private data: DataService) {
this.nestedDataSource = new MatTreeNestedDataSource<NestedTreeNode>();
}
}
Let’s dig in to the nested data source.
The nested data source
In our initial example with the flat tree, it was the data source’s responsibility to respond to nodes expanding or collapsing. It did so by finding the node in an array, loading more nodes from the server, and then splicing those new nodes into the existing tree array.
This approach wasn’t optimal because it required us to keep juggling an index to figure out where our new nodes should be inserted. It also caused us to write code that would be hard to maintain.
With the nested tree, we can just use a MatTreeNestedDataSource<T>
as our datasource. This is possible because our data source isn’t running amok trying to find array nodes by index and jam arrays into other arrays at weird spots.
The responsibility of expanding nodes (and loading more nodes from the server) comes to the component itself, which is a better place for it logically. We also avoid writing our own class that implements the DataSource
. Considering that the FlatTreeDataSource
in our last example was 114 lines long, that’s quite a lot of time and effort saved.
Nested tree initialization
This brings us to our initial tree setup, where we receive a list of nodes from the server and assign them as the nestedDataSource
's data property.
async ngOnInit() {
let rootNodes = await firstValueFrom(this.data.longDataGet(0, 10));
this.nestedDataSource.data = rootNodes.map(x => new NestedTreeNode(0, x));
}
Our handleTreeControl
function remains essentially the same. Again, multiple nodes could be expanded or collapsed at the same time.
The real payoff in the nested tree occurs in the toggleNode
function:
private async toggleNode(node: NestedTreeNode, expand: boolean) {
// If the node is asking to be expanded...
if (expand) {
// And the node hasn't already had its children loaded...
if (node.children.value.length == 0) {
// Set the loading indicator to true
node.loading.set(true);
// Retrieve the new nodes from the server
let children = await firstValueFrom(this.data.longDataGet(node.data.index, 10));
// Convert them to our NestedTreeNode
let nodes = children.map((x, index) => new NestedTreeNode(node.level + 1, x, node));
// Set the last node on the set to have the "last node" property, so the "load more" button is shown
nodes[nodes.length - 1].options.update(x => x.add(TreeOption.Last));
// Send the updated nodes into the BehaviourSubject
node.children.next(nodes);
// Set the loading indicator to false
node.loading.set(false);
}
}
}
Amazing! Children are loaded into the tree without so much as having to play around with indexes. This is a huge improvement to readability and intuitiveness over the flat tree.
Our loadMore
function benefits from these improvements also:
async loadMore(node: NestedTreeNode) {
// Set the loading indicator to true for the node
node.loading.set(true);
// Retrieve the next set of nodes from the server
let childData = await firstValueFrom(this.data.longDataGet(node.data.index! + 1, 10));
// Convert them to NestedTreeNode. Set the parent of the new nodes (not this node, this nodes parent)
let childNodes = childData.map(x => new NestedTreeNode(node.level, x, node.parent));
// Retrieve the existing children array
let existingChildren = node.parent?.children.value;
if (existingChildren) {
// Remove any "last node" option from existing nodes in this array
existingChildren.forEach(x => x.options.update(y => {
y.delete(TreeOption.Last);
return y;
}));
// Build the new array from the old nodes, and the new nodes we just received
let newChildArray = [...existingChildren, ...childNodes];
// Set the new data of the parent, and notify the tree that the nodes have updated
node.parent?.children.next(newChildArray);
}
// Set the loading indicator back to false
node.loading.set(false);
}
One last thing that we want to add to our nested tree is a function that lets the tree uniquely identify nodes within the tree. This will let it know which nodes require an update and which nodes can be left alone. In our case, that’s as simple as specifying a node level and an index, which will be unique to each node:
trackBy(_: number, node: NestedTreeNode){
return `${node.level}${node.data.index}`
}
With our component wired up, now let’s move on to working on the component view:
<cdk-tree [dataSource]="nestedDataSource" [childrenAccessor]="childrenAccessor"
style="display: flex; flex-direction: column" [trackBy]="trackBy" #tree>
<cdk-nested-tree-node *cdkTreeNodeDef="let _node" class="example-tree-node" (expandedChange)="handleNodeExpansion($event, _node)">
@let node = _node | asNestedTreeNode;
<div style="flex-direction: row">
@if (node.expandable()) {
<button mat-icon-button cdkTreeNodeToggle>
<mat-icon>
@if (tree.isExpanded(node)) {
expand_more
} @else {
chevron_right
}
</mat-icon>
</button>
}
{{ node.data.item }}
@if (node.loading()) {
Loading...
}
</div>
@if (node.options().has(TreeOption.Last)){
<button (click)="loadMore(node)">Load more</button>
}
@if (tree.isExpanded(node)) {
<div style="display: flex; flex-direction: column">
<ng-container cdkTreeNodeOutlet>
</ng-container>
</div>
}
</cdk-nested-tree-node>
</cdk-tree>
We still have our cdk-tree
, but now we have a cdk-nested-tree-node
. If nodes are expandable, we render a button to undertake the expanding or collapsing, as well as to show a loading indicator and a Load more... button as required. As above, we listen to the expansionChange
event and expand/contract nodes as required.
Finally, if a node is expanded, we use a ng-container
with a cdkTreeNodeOutlet
to render the node children. This causes the node children to render within the outlet via the cdk-nested-tree-node
. Every time a node is expanded, this continues over and over again, essentially recursing into itself to render each subsequent node within the view.
Making the nodes selectable
With the tree functional, now let’s make it so our nodes are selectable. Because each node appears in an array, and the array could be deeply nested within other arrays of nodes, it makes sense to make each node responsible for telling the data model if it has been selected or not.
Within our node template, adding a checkbox to the node is as simple as this:
<input type="checkbox" [checked]="(node | asTreeNode).selected()" (change)="handleNodeSelectionChange(node, $any($event.target).checked)" >
The only wrinkle is that the property that tells us whether a node has been checked or not exists in $event.target
, and the type information for that object isn’t fully recognized by TypeScript. So, we have to use $any
to strip $event.target
of its known type information before accessing that property.
The upside is that our handleNodeSelectionChange
function can be strongly typed, like so:
handleNodeSelectionChange(node: NestedTreeNode, checked: boolean) {
if (checked){
this.selectedNodes.update(x => {
x.push(node);
return x;
});
}
else{
this.selectedNodes.update(x => {
let nodeIndex = x.indexOf(node);
x.splice(nodeIndex, 1);
return x;
})
}
}
It’s simple — add the node when the checkbox is ticked, or remove it when it’s unticked. At this stage, our tree looks like this:
A practical Angular tree example: The Pokémon tree
It’s all well and good to have an expanding tree view that shows indexes. But what about more advanced cases, like where you have a tree that has children, but the children may retrieve nodes and data from several disparate API sources? Fortunately, that’s very possible to achieve with the Angular CDK/Material tree.
We’ll now create a tree that has a list of Pokémon. The tree view will display the data related to the Pokémon, but also have expandable nodes that relate to which movies, TV shows, or other media formats the Pokémon has appeared in. Our finished example will look like this:
First thing to answer: what’s with the black borders? They demonstrate the outlet for the nodes, so we can easily see what nodes are rendered within a node outlet.
We start with the model for our Pokémon data, which is similar to the nested example, with the notable addition of a type
parameter:
export class PokemonTreeNode {
children = new BehaviorSubject<Array<PokemonTreeNode>>([]);
loading = signal(false);
constructor(public level: number,
public label: string,
public type: PokemonNodeType,
public expandable: boolean,
public parent?: PokemonTreeNode,
public data?: PokemonDetails | string | Array<string>,) {
}
}
export enum PokemonNodeType {
PokemonDetailsNode = 'Details',
InformationalNode = 'Informational',
GamesNode = 'Games',
TvShowsNode = 'TVShows',
BooksNode = 'Books',
PostersNode = 'Posters',
}
The type
parameter is there so we can specify what type of information this node is, as different nodes will have different conditions.
We want the topmost node with the Pokémon name to be expandable, as well as the nodes that relate to which movies the Pokémon has been in. However, we don’t want the informational nodes to be expandable — the ones that give us data on the Pokémon such as their height, weight, etc.
Our toggle node code changes substantially, because we’re making decisions based on what node is being expanded, etc.
private async toggleNode(node: PokemonTreeNode, expand: boolean) {
// If the node already has children, then don't re-retrieve them. Cached nodes will be displayed instead.
if (node.children.value.length) return;
// If expansion has been requested...
if (expand) {
// Set the loading indicator true for the node
node.loading.set(true);
// Consider the type of node that is being expanded
switch (node.type) {
// If it's a details node (the node that has the Pokemon name in it)...
case PokemonNodeType.PokemonDetailsNode:
// Retreive the pokemon details from the server
let data = await firstValueFrom(this.data.pokemonDetailsByNameGet(node.label));
// Manually construct nodes to display Pokemon information
let treeNodes = [
new PokemonTreeNode(1, `Color: ${data.color}`, PokemonNodeType.PokemonDetailsNode, false, node),
new PokemonTreeNode(1, `Weight: ${data.weight}`, PokemonNodeType.PokemonDetailsNode, false, node),
new PokemonTreeNode(1, `Height: ${data.height}`, PokemonNodeType.PokemonDetailsNode, false, node),
new PokemonTreeNode(1, `Type: ${data.type}`, PokemonNodeType.PokemonDetailsNode, false, node),
new PokemonTreeNode(1, `Category: ${data.category}`, PokemonNodeType.PokemonDetailsNode, false, node),
// And the expandable nodes
new PokemonTreeNode(1, `Games`, PokemonNodeType.GamesNode, true, node),
new PokemonTreeNode(1, 'TV Shows', PokemonNodeType.TvShowsNode, true, node),
new PokemonTreeNode(1, 'Books', PokemonNodeType.BooksNode, true, node),
new PokemonTreeNode(1, 'Posters', PokemonNodeType.PostersNode, true, node),
];
// Tell the node children property that new values are available
node.children.next([...treeNodes]);
break;
case PokemonNodeType.GamesNode:
// If it's a games node that is being expanded, retrieve games for the pokemon and set them as the nodes children
// ...repeat the same thing for other types of node (Tv Shows/Books/etc.)
let games = await firstValueFrom(this.data.pokemonGamesByNameGet(node.parent?.label!, 0, 10));
node.children.next([...games.map(x => new PokemonTreeNode(2, x, PokemonNodeType.InformationalNode, false, node))]);
break;
case PokemonNodeType.TvShowsNode:
let shows = await firstValueFrom(this.data.pokemonTvshowsByNameGet(node.parent?.label!, 0, 10));
node.children.next([...shows.map(x => new PokemonTreeNode(2, x, PokemonNodeType.InformationalNode, false, node))]);
break;
case PokemonNodeType.BooksNode:
let books = await firstValueFrom(this.data.pokemonBooksByNameGet(node.parent?.label!, 0, 10));
node.children.next([...books.map(x => new PokemonTreeNode(2, x, PokemonNodeType.InformationalNode, false, node))]);
break;
case PokemonNodeType.PostersNode:
let posters = await firstValueFrom(this.data.pokemonPostersByNameGet(node.parent?.label!, 0, 10));
node.children.next([...posters.map(x => new PokemonTreeNode(2, x, PokemonNodeType.InformationalNode, false, node))]);
// debugger;
break;
default:
throw (`Unknown Node type ${node.type}`);
}
// Set the loading indicator back to false for the node
node.loading.set(false);
}
}
The main point here is how we manually build our tree nodes for display, and how we mark individual nodes as expandable or not. When our toggleNode
function receives different types of nodes to expand, it can choose the right API action to execute, and fill the tree view with the correct values.
Migration guide (AKA: I updated Angular and now I have angry deprecation warnings and I don’t know how to fix it PLEASE SEND HELP 😢😢😭)
It’s possible that I just missed all the drama surrounding the tree that necessitated such a substantial change, but, the reasons behind why the tree was updated are not clear to me.
I leafed through quite a few GitHub issues and Pull Requests, and could see what was changed, and who had submitted the pull request…but there was no detail to why this change happened in the first place.
Why does it even matter? Well, some of us use Angular to ship production apps in jobs that we get paid to do, and at some point (after about a year), the tree control stuff is going to get removed after it gets deprecated.
So, if we want to ship an update in the future and suddenly we have to rework our complex trees in the app, and the reason is, “The old tree wasn’t dolphin friendly, and this one is,” then we can validate our time and effort against the rationale given.
But if things are changing for the sake of changing, and we have to invest time and effort to change something that was previously working, then that shakes our confidence in the framework overall.
After all, if things change based on someone’s whimsy and not a real defined need with a real defined benefit, how many other parts of Angular will change for no observable reason, with no observable benefit?
The point of all this is to validate what you are feeling, which might be, “This thing changed and I don’t know why it changed.” I don’t know why it changed either. Hopefully the Angular team does. Also, if you’re reading the samples on the angular website and they make no sense, that’s not a comprehension issue for you. At the time of writing they are, unfortunately, a bit all over the place.
So here’s how to make your existing tree work with the new bits. We’re going to talk current state and future state so you understand how your thing works today and how it needs to work.
Current state
Your tree has a tree controller. You use the tree controller to subscribe to tree changes, such as nodes expanding or collapsing. When nodes expand or collapse, you call your toggleNode
function to expand or collapse the node accordingly.
Future state
Your tree (in the component) now has a reference, like #tree
. Your nodes now have the (expandedChange)="handleNodeExpansion(_$event_, _node)
function on them. The function just looks like this:
handleNodeExpansion($event: boolean, node: YourNodeType) {
this.toggleNode(node, $event);
}
If you have a flat tree, your expansion will occur within the data source. If you have a nested tree, your expansion will occur within your component code (the Pokemon example above does a good job of demonstrating this).
The last thing to cover is the childrenAccessor/levelAccessor functionality. It sounds hard, but basically, if you have a flat tree, you want to use the levelAccessor. This should have the current level of the node:
treeLevelAccessor =(dataNode: FlatTreeNode) => dataNode.level;
If you have a nested tree, you want to use the childrenAccessor
. This must be an Observable<_YourNodeType[]_>
. For me, I use a BehaviourSubject
. The definition of this would look like this:
childrenAccessor = (dataNode: NestedTreeNode) => dataNode.children;
Do not use both together.
You should only have to use one or the other, and your use should be defined by whether you are creating a flat tree or a nested tree. The full diff is available here, so you can see what changed from the old tree to the new tree.
Hopefully you find the new tree a-treeable to your tastes.
Conclusion
Hopefully, from reading this guide, you’ve come to understand the tree in a lot more detail. I haven’t delved into every single possible visual representation that you could have in a tree, but I’ve hopefully helped you to understand how the foundations of the tree work.
The Angular tree can be hard to get right, but once you understand it, it can be quite a powerful visual representation.
Don’t feel bad if you find it confusing or if you don’t get it right on the first go-round. When implemented correctly, the tree does follow a logical procession and is a high quality component, similar to what you may already be used to in the CDK or Material.
You can clone the project here. To run it locally, navigate into the server
directory, run npm i
and then node index.js
, and finally run ng serve
from the client
directory.
Experience your Angular apps exactly how a user does
Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.
Modernize how you debug your Angular apps — start monitoring for free.
Top comments (0)