Skip to content

refactor(cdk-experimental/tree): use explicit inputs for passing dependencies #31654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 8, 2025
Merged
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
84 changes: 32 additions & 52 deletions src/cdk-experimental/tree/tree.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Component, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {Direction} from '@angular/cdk/bidi';
Expand Down Expand Up @@ -1336,63 +1337,42 @@ interface TestTreeNode<V = string> {
[(value)]="value"
[nav]="nav()"
[currentType]="currentType()"
#tree="cdkTree"
>
@for (node of nodes(); track node.value) {
<li
cdkTreeItem
[value]="node.value"
[label]="node.label"
[disabled]="!!node.disabled"
[attr.data-value]="node.value"
>
{{ node.label }}
@if (node.children !== undefined && node.children!.length > 0) {
<ul
cdkTreeItemGroup
[value]="node.value"
[preserveContent]="!!node.preserveContent"
[attr.data-group-for]="node.value">
<ng-template cdkTreeItemGroupContent>
@for (node of node.children; track node.value) {
<li
cdkTreeItem
[value]="node.value"
[label]="node.label"
[disabled]="!!node.disabled"
[attr.data-value]="node.value"
>
{{ node.label }}
@if (node.children !== undefined && node.children!.length > 0) {
<ul
cdkTreeItemGroup
[value]="node.value"
[preserveContent]="!!node.preserveContent"
[attr.data-group-for]="node.value">
<ng-template cdkTreeItemGroupContent>
@for (node of node.children; track node.value) {
<li
cdkTreeItem
[value]="node.value"
[label]="node.label"
[disabled]="!!node.disabled"
[attr.data-value]="node.value"
>
{{ node.label }}
</li>
}
</ng-template>
</ul>
}
</li>
}
</ng-template>
</ul>
}
</li>
<ng-template [ngTemplateOutlet]="nodeTemplate" [ngTemplateOutletContext]="{ node: node, parent: tree }" />
}
</ul>

<ng-template #nodeTemplate let-node="node" let-parent="parent">
<li
cdkTreeItem
[value]="node.value"
[label]="node.label"
[disabled]="!!node.disabled"
[parent]="parent"
[attr.data-value]="node.value"
#treeItem="cdkTreeItem"
>
{{ node.label }}
@if (node.children !== undefined && node.children!.length > 0) {
<ul
cdkTreeItemGroup
[ownedBy]="treeItem"
[preserveContent]="!!node.preserveContent"
[attr.data-group-for]="node.value"
#group="cdkTreeItemGroup">
<ng-template cdkTreeItemGroupContent>
@for (node of node.children; track node.value) {
<ng-template [ngTemplateOutlet]="nodeTemplate" [ngTemplateOutletContext]="{ node: node, parent: group }" />
}
</ng-template>
</ul>
}
</li>
</ng-template>
`,
imports: [CdkTree, CdkTreeItem, CdkTreeItemGroup, CdkTreeItemGroupContent],
imports: [CdkTree, CdkTreeItem, CdkTreeItemGroup, CdkTreeItemGroupContent, NgTemplateOutlet],
})
class TestTreeComponent {
nodes = signal<TestTreeNode[]>([
Expand Down
135 changes: 61 additions & 74 deletions src/cdk-experimental/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ export class CdkTree<V> {
/** All CdkTreeItem instances within this tree. */
private readonly _unorderedItems = signal(new Set<CdkTreeItem<V>>());

/** All CdkGroup instances within this tree. */
readonly unorderedGroups = signal(new Set<CdkTreeItemGroup<V>>());

/** Orientation of the tree. */
readonly orientation = input<'vertical' | 'horizontal'>('vertical');

Expand Down Expand Up @@ -144,28 +141,14 @@ export class CdkTree<V> {
this._hasFocused.set(true);
}

register(child: CdkTreeItemGroup<V> | CdkTreeItem<V>) {
if (child instanceof CdkTreeItemGroup) {
this.unorderedGroups().add(child);
this.unorderedGroups.set(new Set(this.unorderedGroups()));
}

if (child instanceof CdkTreeItem) {
this._unorderedItems().add(child);
this._unorderedItems.set(new Set(this._unorderedItems()));
}
register(child: CdkTreeItem<V>) {
this._unorderedItems().add(child);
this._unorderedItems.set(new Set(this._unorderedItems()));
}

deregister(child: CdkTreeItemGroup<V> | CdkTreeItem<V>) {
if (child instanceof CdkTreeItemGroup) {
this.unorderedGroups().delete(child);
this.unorderedGroups.set(new Set(this.unorderedGroups()));
}

if (child instanceof CdkTreeItem) {
this._unorderedItems().delete(child);
this._unorderedItems.set(new Set(this._unorderedItems()));
}
unregister(child: CdkTreeItem<V>) {
this._unorderedItems().delete(child);
this._unorderedItems.set(new Set(this._unorderedItems()));
}
}

Expand All @@ -185,7 +168,7 @@ export class CdkTree<V> {
'[attr.aria-current]': 'pattern.current()',
'[attr.aria-disabled]': 'pattern.disabled()',
'[attr.aria-level]': 'pattern.level()',
'[attr.aria-owns]': 'group()?.id',
'[attr.aria-owns]': 'ownsId()',
'[attr.aria-setsize]': 'pattern.setsize()',
'[attr.aria-posinset]': 'pattern.posinset()',
'[attr.tabindex]': 'pattern.tabindex()',
Expand All @@ -199,29 +182,21 @@ export class CdkTreeItem<V> implements OnInit, OnDestroy, HasElement {
/** A unique identifier for the tree item. */
private readonly _id = inject(_IdGenerator).getId('cdk-tree-item-');

/** The top level CdkTree. */
private readonly _tree = inject(CdkTree<V>);

/** The parent CdkTreeItem. */
private readonly _treeItem = inject(CdkTreeItem<V>, {optional: true, skipSelf: true});

/** The parent CdkGroup, if any. */
private readonly _parentGroup = inject(CdkTreeItemGroup<V>, {optional: true});
/** The owned tree item group. */
private readonly _group = signal<CdkTreeItemGroup<V> | undefined>(undefined);

/** The top level TreePattern. */
private readonly _treePattern = computed(() => this._tree.pattern);

/** The parent TreeItemPattern. */
private readonly _parentPattern: Signal<TreeItemPattern<V> | TreePattern<V>> = computed(
() => this._treeItem?.pattern ?? this._treePattern(),
);
/** The id of the owned group. */
readonly ownsId = computed(() => this._group()?.id);

/** The host native element. */
readonly element = computed(() => this._elementRef.nativeElement);

/** The value of the tree item. */
readonly value = input.required<V>();

/** The parent tree root or tree item group. */
readonly parent = input.required<CdkTree<V> | CdkTreeItemGroup<V>>();

/** Whether the tree item is disabled. */
readonly disabled = input(false, {transform: booleanAttribute});

Expand All @@ -231,46 +206,61 @@ export class CdkTreeItem<V> implements OnInit, OnDestroy, HasElement {
/** Search term for typeahead. */
readonly searchTerm = computed(() => this.label() ?? this.element().textContent);

/** Manual group assignment. */
readonly group = signal<CdkTreeItemGroup<V> | undefined>(undefined);
/** The tree root. */
readonly tree: Signal<CdkTree<V>> = computed(() => {
if (this.parent() instanceof CdkTree) {
return this.parent() as CdkTree<V>;
}
return (this.parent() as CdkTreeItemGroup<V>).ownedBy().tree();
});

/** The UI pattern for this item. */
readonly pattern: TreeItemPattern<V> = new TreeItemPattern<V>({
...this,
id: () => this._id,
tree: this._treePattern,
parent: this._parentPattern,
children: computed(
() =>
this.group()
?.children()
.map(item => (item as CdkTreeItem<V>).pattern) ?? [],
),
hasChildren: computed(() => !!this.group()),
});
pattern: TreeItemPattern<V>;

constructor() {
afterRenderEffect(() => {
const group = [...this._tree.unorderedGroups()].find(group => group.value() === this.value());
if (group) {
this.group.set(group);
}
});

// Updates the visibility of the owned group.
afterRenderEffect(() => {
this.group()?.visible.set(this.pattern.expanded());
this._group()?.visible.set(this.pattern.expanded());
});
}

ngOnInit() {
this._tree.register(this);
this._parentGroup?.register(this);
this.parent().register(this);
this.tree().register(this);

const treePattern = computed(() => this.tree().pattern);
const parentPattern = computed(() => {
if (this.parent() instanceof CdkTree) {
return treePattern();
}
return (this.parent() as CdkTreeItemGroup<V>).ownedBy().pattern;
});
this.pattern = new TreeItemPattern<V>({
...this,
id: () => this._id,
tree: treePattern,
parent: parentPattern,
children: computed(
() =>
this._group()
?.children()
.map(item => (item as CdkTreeItem<V>).pattern) ?? [],
),
hasChildren: computed(() => !!this._group()),
});
}

ngOnDestroy() {
this._tree.deregister(this);
this._parentGroup?.deregister(this);
this.parent().unregister(this);
this.tree().unregister(this);
}

register(group: CdkTreeItemGroup<V>) {
this._group.set(group);
}

unregister() {
this._group.set(undefined);
}
}

Expand Down Expand Up @@ -300,9 +290,6 @@ export class CdkTreeItemGroup<V> implements OnInit, OnDestroy, HasElement {
/** The DeferredContentAware host directive. */
private readonly _deferredContentAware = inject(DeferredContentAware);

/** The top level CdkTree. */
private readonly _tree = inject(CdkTree<V>);

/** All groupable items that are descendants of the group. */
private readonly _unorderedItems = signal(new Set<CdkTreeItem<V>>());

Expand All @@ -318,8 +305,8 @@ export class CdkTreeItemGroup<V> implements OnInit, OnDestroy, HasElement {
/** Child items within this group. */
readonly children = computed(() => [...this._unorderedItems()].sort(sortDirectives));

/** Identifier for matching the group owner. */
readonly value = input.required<V>();
/** Tree item that owns the group. */
readonly ownedBy = input.required<CdkTreeItem<V>>();

constructor() {
// Connect the group's hidden state to the DeferredContentAware's visibility.
Expand All @@ -329,19 +316,19 @@ export class CdkTreeItemGroup<V> implements OnInit, OnDestroy, HasElement {
}

ngOnInit() {
this._tree.register(this);
this.ownedBy().register(this);
}

ngOnDestroy() {
this._tree.deregister(this);
this.ownedBy().unregister();
}

register(child: CdkTreeItem<V>) {
this._unorderedItems().add(child);
this._unorderedItems.set(new Set(this._unorderedItems()));
}

deregister(child: CdkTreeItem<V>) {
unregister(child: CdkTreeItem<V>) {
this._unorderedItems().delete(child);
this._unorderedItems.set(new Set(this._unorderedItems()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,44 @@
>
@if (nav.value) {
@for (node of treeData; track node) {
<example-nav-node [node]="node" />
<ng-template [ngTemplateOutlet]="navNodeTemplate" [ngTemplateOutletContext]="{ node: node, parent: tree }" />
}
} @else {
@for (node of treeData; track node) {
<example-node [node]="node" />
<example-node [node]="node" [parent]="tree" />
}
}
</ul>

<ng-template #navNodeTemplate let-node="node" let-parent="parent">
<li class="example-tree-item">
<a
cdkTreeItem
class="example-tree-item-content example-selectable example-stateful"
[value]="node.value"
[label]="node.label || node.value"
[disabled]="node.disabled"
[parent]="parent"
#treeItem="cdkTreeItem"
href="#{{node.value}}"
(click)="$event.preventDefault()"
>
<mat-icon class="example-tree-item-icon" aria-hidden="true">
@if (treeItem.pattern.expandable()) {
{{ treeItem.pattern.expanded() ? 'expand_less' : 'expand_more' }}
}
</mat-icon>
{{ node.label }}
</a>

@if (node.children !== undefined && node.children!.length > 0) {
<ul cdkTreeItemGroup [ownedBy]="treeItem" #group="cdkTreeItemGroup">
<ng-template cdkTreeItemGroupContent>
@for (child of node.children; track child) {
<ng-template [ngTemplateOutlet]="navNodeTemplate" [ngTemplateOutletContext]="{ node: child, parent: group }" />
}
</ng-template>
</ul>
}
</li>
</ng-template>
Loading
Loading