diff --git a/src/cdk-experimental/tree/tree.spec.ts b/src/cdk-experimental/tree/tree.spec.ts index a5d6818b7b20..2934083f9f2d 100644 --- a/src/cdk-experimental/tree/tree.spec.ts +++ b/src/cdk-experimental/tree/tree.spec.ts @@ -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'; @@ -1336,63 +1337,42 @@ interface TestTreeNode { [(value)]="value" [nav]="nav()" [currentType]="currentType()" + #tree="cdkTree" > @for (node of nodes(); track node.value) { -
  • - {{ node.label }} - @if (node.children !== undefined && node.children!.length > 0) { - - } -
  • + } + + +
  • + {{ node.label }} + @if (node.children !== undefined && node.children!.length > 0) { +
      + + @for (node of node.children; track node.value) { + + } + +
    + } +
  • +
    `, - imports: [CdkTree, CdkTreeItem, CdkTreeItemGroup, CdkTreeItemGroupContent], + imports: [CdkTree, CdkTreeItem, CdkTreeItemGroup, CdkTreeItemGroupContent, NgTemplateOutlet], }) class TestTreeComponent { nodes = signal([ diff --git a/src/cdk-experimental/tree/tree.ts b/src/cdk-experimental/tree/tree.ts index 2ad9df8fe258..3470f169b182 100644 --- a/src/cdk-experimental/tree/tree.ts +++ b/src/cdk-experimental/tree/tree.ts @@ -79,9 +79,6 @@ export class CdkTree { /** All CdkTreeItem instances within this tree. */ private readonly _unorderedItems = signal(new Set>()); - /** All CdkGroup instances within this tree. */ - readonly unorderedGroups = signal(new Set>()); - /** Orientation of the tree. */ readonly orientation = input<'vertical' | 'horizontal'>('vertical'); @@ -144,28 +141,14 @@ export class CdkTree { this._hasFocused.set(true); } - register(child: CdkTreeItemGroup | CdkTreeItem) { - 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) { + this._unorderedItems().add(child); + this._unorderedItems.set(new Set(this._unorderedItems())); } - deregister(child: CdkTreeItemGroup | CdkTreeItem) { - 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) { + this._unorderedItems().delete(child); + this._unorderedItems.set(new Set(this._unorderedItems())); } } @@ -185,7 +168,7 @@ export class CdkTree { '[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()', @@ -199,22 +182,11 @@ export class CdkTreeItem 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); - - /** The parent CdkTreeItem. */ - private readonly _treeItem = inject(CdkTreeItem, {optional: true, skipSelf: true}); - - /** The parent CdkGroup, if any. */ - private readonly _parentGroup = inject(CdkTreeItemGroup, {optional: true}); + /** The owned tree item group. */ + private readonly _group = signal | undefined>(undefined); - /** The top level TreePattern. */ - private readonly _treePattern = computed(() => this._tree.pattern); - - /** The parent TreeItemPattern. */ - private readonly _parentPattern: Signal | TreePattern> = 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); @@ -222,6 +194,9 @@ export class CdkTreeItem implements OnInit, OnDestroy, HasElement { /** The value of the tree item. */ readonly value = input.required(); + /** The parent tree root or tree item group. */ + readonly parent = input.required | CdkTreeItemGroup>(); + /** Whether the tree item is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); @@ -231,46 +206,61 @@ export class CdkTreeItem implements OnInit, OnDestroy, HasElement { /** Search term for typeahead. */ readonly searchTerm = computed(() => this.label() ?? this.element().textContent); - /** Manual group assignment. */ - readonly group = signal | undefined>(undefined); + /** The tree root. */ + readonly tree: Signal> = computed(() => { + if (this.parent() instanceof CdkTree) { + return this.parent() as CdkTree; + } + return (this.parent() as CdkTreeItemGroup).ownedBy().tree(); + }); /** The UI pattern for this item. */ - readonly pattern: TreeItemPattern = new TreeItemPattern({ - ...this, - id: () => this._id, - tree: this._treePattern, - parent: this._parentPattern, - children: computed( - () => - this.group() - ?.children() - .map(item => (item as CdkTreeItem).pattern) ?? [], - ), - hasChildren: computed(() => !!this.group()), - }); + pattern: TreeItemPattern; 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).ownedBy().pattern; + }); + this.pattern = new TreeItemPattern({ + ...this, + id: () => this._id, + tree: treePattern, + parent: parentPattern, + children: computed( + () => + this._group() + ?.children() + .map(item => (item as CdkTreeItem).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) { + this._group.set(group); + } + + unregister() { + this._group.set(undefined); } } @@ -300,9 +290,6 @@ export class CdkTreeItemGroup implements OnInit, OnDestroy, HasElement { /** The DeferredContentAware host directive. */ private readonly _deferredContentAware = inject(DeferredContentAware); - /** The top level CdkTree. */ - private readonly _tree = inject(CdkTree); - /** All groupable items that are descendants of the group. */ private readonly _unorderedItems = signal(new Set>()); @@ -318,8 +305,8 @@ export class CdkTreeItemGroup 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(); + /** Tree item that owns the group. */ + readonly ownedBy = input.required>(); constructor() { // Connect the group's hidden state to the DeferredContentAware's visibility. @@ -329,11 +316,11 @@ export class CdkTreeItemGroup implements OnInit, OnDestroy, HasElement { } ngOnInit() { - this._tree.register(this); + this.ownedBy().register(this); } ngOnDestroy() { - this._tree.deregister(this); + this.ownedBy().unregister(); } register(child: CdkTreeItem) { @@ -341,7 +328,7 @@ export class CdkTreeItemGroup implements OnInit, OnDestroy, HasElement { this._unorderedItems.set(new Set(this._unorderedItems())); } - deregister(child: CdkTreeItem) { + unregister(child: CdkTreeItem) { this._unorderedItems().delete(child); this._unorderedItems.set(new Set(this._unorderedItems())); } diff --git a/src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.html b/src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.html index 09b4c3507352..55c66aad4daf 100644 --- a/src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.html +++ b/src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.html @@ -50,11 +50,44 @@ > @if (nav.value) { @for (node of treeData; track node) { - + } } @else { @for (node of treeData; track node) { - + } } + + +
  • + + + {{ node.label }} + + + @if (node.children !== undefined && node.children!.length > 0) { +
      + + @for (child of node.children; track child) { + + } + +
    + } +
  • +
    diff --git a/src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.ts b/src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.ts index b5591b3a9a6e..4a0923d70f13 100644 --- a/src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.ts +++ b/src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.ts @@ -1,4 +1,5 @@ import {Component, model, input} from '@angular/core'; +import {NgTemplateOutlet} from '@angular/common'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -29,6 +30,7 @@ interface ExampleNode { [value]="node().value" [label]="node().label || node().value" [disabled]="node().disabled" + [parent]="parent()" #treeItem="cdkTreeItem" > @if (node().children !== undefined && node().children!.length > 0) { -
    +
    @for (child of node().children; track child) { - + }
    @@ -58,47 +60,8 @@ interface ExampleNode { }) export class ExampleNodeComponent { node = input.required(); -} - -@Component({ - selector: 'example-nav-node', - styleUrl: 'cdk-tree-example.css', - template: ` -
  • - - - {{ node().label }} - - @if (node().children !== undefined && node().children!.length > 0) { -
      - - @for (child of node().children; track child) { - - } - -
    - } -
  • - `, - imports: [MatIconModule, CdkTreeItem, CdkTreeItemGroup, CdkTreeItemGroupContent], -}) -export class ExampleNavNodeComponent { - node = input.required(); + parent = input.required | CdkTreeItemGroup>(); } /** @title Tree using CdkTree and CdkTreeItem. */ @@ -114,9 +77,12 @@ export class ExampleNavNodeComponent { MatFormFieldModule, MatSelectModule, MatIconModule, + NgTemplateOutlet, CdkTree, + CdkTreeItem, + CdkTreeItemGroup, + CdkTreeItemGroupContent, ExampleNodeComponent, - ExampleNavNodeComponent, ], }) export class CdkTreeExample {