Skip to content

Commit f29b098

Browse files
committed
refactor: register/deregister children instead of using contentChildren
1 parent 5e53d97 commit f29b098

File tree

8 files changed

+219
-233
lines changed

8 files changed

+219
-233
lines changed

src/cdk-experimental/tree/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export {CdkGroup, CdkGroupContent, CdkTree, CdkTreeItem} from './tree';
9+
export {CdkTreeGroup, CdkTreeGroupContent, CdkTree, CdkTreeItem} from './tree';

src/cdk-experimental/tree/tree.ts

Lines changed: 150 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -12,100 +12,32 @@ import {
1212
afterRenderEffect,
1313
booleanAttribute,
1414
computed,
15-
contentChildren,
16-
forwardRef,
1715
inject,
1816
input,
1917
model,
2018
signal,
2119
Signal,
20+
OnInit,
21+
OnDestroy,
2222
} from '@angular/core';
2323
import {_IdGenerator} from '@angular/cdk/a11y';
2424
import {Directionality} from '@angular/cdk/bidi';
2525
import {DeferredContent, DeferredContentAware} from '@angular/cdk-experimental/deferred-content';
2626
import {TreeItemPattern, TreePattern} from '../ui-patterns/tree/tree';
2727

28-
/**
29-
* Base class to make a Cdk item groupable.
30-
*
31-
* Also need to add the following to the `@Directive` configuration:
32-
* ```
33-
* providers: [
34-
* { provide: BaseGroupable, useExisting: forwardRef(() => CdkSomeItem) },
35-
* ],
36-
* ```
37-
*
38-
* TODO(ok7sai): Move it to a shared place.
39-
*/
40-
export class BaseGroupable {
41-
/** The parent CdkGroup, if any. */
42-
groupParent = inject(CdkGroup, {optional: true});
28+
interface HasElement {
29+
element: Signal<HTMLElement>;
4330
}
4431

4532
/**
46-
* Generic container that designates content as a group.
47-
*
48-
* TODO(ok7sai): Move it to a shared place.
33+
* Sort directives by their document order.
4934
*/
50-
@Directive({
51-
selector: '[cdkGroup]',
52-
exportAs: 'cdkGroup',
53-
hostDirectives: [
54-
{
55-
directive: DeferredContentAware,
56-
inputs: ['preserveContent'],
57-
},
58-
],
59-
host: {
60-
'class': 'cdk-group',
61-
'role': 'group',
62-
'[id]': 'id',
63-
'[attr.inert]': 'visible() ? null : true',
64-
},
65-
})
66-
export class CdkGroup<V> {
67-
/** The DeferredContentAware host directive. */
68-
private readonly _deferredContentAware = inject(DeferredContentAware);
69-
70-
/** All groupable items that are descendants of the group. */
71-
private readonly _items = contentChildren(BaseGroupable, {descendants: true});
72-
73-
/** Identifier for matching the group owner. */
74-
readonly value = input.required<V>();
75-
76-
/** Whether the group is visible. */
77-
readonly visible = signal(true);
78-
79-
/** Unique ID for the group. */
80-
readonly id = inject(_IdGenerator).getId('cdk-group-');
81-
82-
/** Child items within this group. */
83-
readonly children = signal<BaseGroupable[]>([]);
84-
85-
constructor() {
86-
afterRenderEffect(() => {
87-
this.children.set(this._items().filter(item => item.groupParent === this));
88-
});
89-
90-
// Connect the group's hidden state to the DeferredContentAware's visibility.
91-
afterRenderEffect(() => {
92-
this._deferredContentAware.contentVisible.set(this.visible());
93-
});
94-
}
35+
function sortDirectives(a: HasElement, b: HasElement) {
36+
return (a.element().compareDocumentPosition(b.element()) & Node.DOCUMENT_POSITION_PRECEDING) > 0
37+
? 1
38+
: -1;
9539
}
9640

97-
/**
98-
* A structural directive that marks the `ng-template` to be used as the content
99-
* for a `CdkGroup`. This content can be lazily loaded.
100-
*
101-
* TODO(ok7sai): Move it to a shared place.
102-
*/
103-
@Directive({
104-
selector: 'ng-template[cdkGroupContent]',
105-
hostDirectives: [DeferredContent],
106-
})
107-
export class CdkGroupContent {}
108-
10941
/**
11042
* Makes an element a tree and manages state (focus, selection, keyboard navigation).
11143
*/
@@ -126,15 +58,10 @@ export class CdkGroupContent {}
12658
})
12759
export class CdkTree<V> {
12860
/** All CdkTreeItem instances within this tree. */
129-
private readonly _cdkTreeItems = contentChildren<CdkTreeItem<V>>(CdkTreeItem, {
130-
descendants: true,
131-
});
132-
133-
/** All TreeItemPattern instances within this tree. */
134-
private readonly _itemPatterns = computed(() => this._cdkTreeItems().map(item => item.pattern));
61+
private readonly _unorderedItems = signal(new Set<CdkTreeItem<V>>());
13562

13663
/** All CdkGroup instances within this tree. */
137-
private readonly _cdkGroups = contentChildren(CdkGroup, {descendants: true});
64+
readonly unorderedGroups = signal(new Set<CdkTreeGroup<V>>());
13865

13966
/** Orientation of the tree. */
14067
readonly orientation = input<'vertical' | 'horizontal'>('vertical');
@@ -169,20 +96,34 @@ export class CdkTree<V> {
16996
/** The UI pattern for the tree. */
17097
pattern: TreePattern<V> = new TreePattern<V>({
17198
...this,
172-
allItems: this._itemPatterns,
99+
allItems: computed(() =>
100+
[...this._unorderedItems()].sort(sortDirectives).map(item => item.pattern),
101+
),
173102
activeIndex: signal(0),
174103
});
175104

176-
constructor() {
177-
// Binds groups to tree items.
178-
afterRenderEffect(() => {
179-
const groups = this._cdkGroups();
180-
const treeItems = this._cdkTreeItems();
181-
for (const group of groups) {
182-
const treeItem = treeItems.find(item => item.value() === group.value());
183-
treeItem?.group.set(group);
184-
}
185-
});
105+
register(child: CdkTreeGroup<V> | CdkTreeItem<V>) {
106+
if (child instanceof CdkTreeGroup) {
107+
this.unorderedGroups().add(child);
108+
this.unorderedGroups.set(new Set(this.unorderedGroups()));
109+
}
110+
111+
if (child instanceof CdkTreeItem) {
112+
this._unorderedItems().add(child);
113+
this._unorderedItems.set(new Set(this._unorderedItems()));
114+
}
115+
}
116+
117+
deregister(child: CdkTreeGroup<V> | CdkTreeItem<V>) {
118+
if (child instanceof CdkTreeGroup) {
119+
this.unorderedGroups().delete(child);
120+
this.unorderedGroups.set(new Set(this.unorderedGroups()));
121+
}
122+
123+
if (child instanceof CdkTreeItem) {
124+
this._unorderedItems().delete(child);
125+
this._unorderedItems.set(new Set(this._unorderedItems()));
126+
}
186127
}
187128
}
188129

@@ -204,30 +145,33 @@ export class CdkTree<V> {
204145
'[attr.aria-posinset]': 'pattern.posinset()',
205146
'[attr.tabindex]': 'pattern.tabindex()',
206147
},
207-
providers: [{provide: BaseGroupable, useExisting: forwardRef(() => CdkTreeItem)}],
208148
})
209-
export class CdkTreeItem<V> extends BaseGroupable {
149+
export class CdkTreeItem<V> implements OnInit, OnDestroy, HasElement {
210150
/** A reference to the tree item element. */
211151
private readonly _elementRef = inject(ElementRef);
212152

213-
/** The host native element. */
214-
private readonly _element = computed(() => this._elementRef.nativeElement);
215-
216153
/** A unique identifier for the tree item. */
217154
private readonly _id = inject(_IdGenerator).getId('cdk-tree-item-');
218155

219156
/** The top level CdkTree. */
220-
private readonly _cdkTree = inject(CdkTree<V>, {optional: true});
157+
private readonly _tree = inject(CdkTree<V>);
221158

222159
/** The parent CdkTreeItem. */
223-
private readonly _cdkTreeItem = inject(CdkTreeItem<V>, {optional: true, skipSelf: true});
160+
private readonly _treeItem = inject(CdkTreeItem<V>, {optional: true, skipSelf: true});
161+
162+
/** The parent CdkGroup, if any. */
163+
private readonly _parentGroup = inject(CdkTreeGroup<V>, {optional: true});
224164

225165
/** The top lavel TreePattern. */
226-
private readonly _treePattern = computed(() => this._cdkTree?.pattern);
166+
private readonly _treePattern = computed(() => this._tree.pattern);
227167

228168
/** The parent TreeItemPattern. */
229-
private readonly _parentPattern: Signal<TreeItemPattern<V> | TreePattern<V> | undefined> =
230-
computed(() => this._cdkTreeItem?.pattern ?? this._treePattern());
169+
private readonly _parentPattern: Signal<TreeItemPattern<V> | TreePattern<V>> = computed(
170+
() => this._treeItem?.pattern ?? this._treePattern(),
171+
);
172+
173+
/** The host native element. */
174+
readonly element = computed(() => this._elementRef.nativeElement);
231175

232176
/** The value of the tree item. */
233177
readonly value = input.required<V>();
@@ -239,16 +183,15 @@ export class CdkTreeItem<V> extends BaseGroupable {
239183
readonly label = input<string>();
240184

241185
/** Search term for typeahead. */
242-
readonly searchTerm = computed(() => this.label() ?? this._element().textContent);
186+
readonly searchTerm = computed(() => this.label() ?? this.element().textContent);
243187

244188
/** Manual group assignment. */
245-
readonly group = signal<CdkGroup<V> | undefined>(undefined);
189+
readonly group = signal<CdkTreeGroup<V> | undefined>(undefined);
246190

247191
/** The UI pattern for this item. */
248192
pattern: TreeItemPattern<V> = new TreeItemPattern<V>({
249193
...this,
250194
id: () => this._id,
251-
element: this._element,
252195
tree: this._treePattern,
253196
parent: this._parentPattern,
254197
children: computed(
@@ -261,11 +204,109 @@ export class CdkTreeItem<V> extends BaseGroupable {
261204
});
262205

263206
constructor() {
264-
super();
207+
afterRenderEffect(() => {
208+
const group = [...this._tree.unorderedGroups()].find(group => group.value() === this.value());
209+
if (group) {
210+
this.group.set(group);
211+
}
212+
});
265213

266214
// Updates the visibility of the owned group.
267215
afterRenderEffect(() => {
268216
this.group()?.visible.set(this.pattern.expanded());
269217
});
270218
}
219+
220+
ngOnInit() {
221+
this._tree.register(this);
222+
this._parentGroup?.register(this);
223+
}
224+
225+
ngOnDestroy() {
226+
this._tree.deregister(this);
227+
this._parentGroup?.deregister(this);
228+
}
271229
}
230+
231+
/**
232+
* Container that designates content as a group.
233+
*/
234+
@Directive({
235+
selector: '[cdkTreeGroup]',
236+
exportAs: 'cdkTreeGroup',
237+
hostDirectives: [
238+
{
239+
directive: DeferredContentAware,
240+
inputs: ['preserveContent'],
241+
},
242+
],
243+
host: {
244+
'class': 'cdk-tree-group',
245+
'role': 'group',
246+
'[id]': 'id',
247+
'[attr.inert]': 'visible() ? null : true',
248+
},
249+
})
250+
export class CdkTreeGroup<V> implements OnInit, OnDestroy, HasElement {
251+
/** A reference to the group element. */
252+
private readonly _elementRef = inject(ElementRef);
253+
254+
/** The DeferredContentAware host directive. */
255+
private readonly _deferredContentAware = inject(DeferredContentAware);
256+
257+
/** The top level CdkTree. */
258+
private readonly _tree = inject(CdkTree<V>);
259+
260+
/** All groupable items that are descendants of the group. */
261+
private readonly _unorderedItems = signal(new Set<CdkTreeItem<V>>());
262+
263+
/** The host native element. */
264+
readonly element = computed(() => this._elementRef.nativeElement);
265+
266+
/** Unique ID for the group. */
267+
readonly id = inject(_IdGenerator).getId('cdk-tree-group-');
268+
269+
/** Whether the group is visible. */
270+
readonly visible = signal(true);
271+
272+
/** Child items within this group. */
273+
readonly children = computed(() => [...this._unorderedItems()].sort(sortDirectives));
274+
275+
/** Identifier for matching the group owner. */
276+
readonly value = input.required<V>();
277+
278+
constructor() {
279+
// Connect the group's hidden state to the DeferredContentAware's visibility.
280+
afterRenderEffect(() => {
281+
this._deferredContentAware.contentVisible.set(this.visible());
282+
});
283+
}
284+
285+
ngOnInit() {
286+
this._tree.register(this);
287+
}
288+
289+
ngOnDestroy() {
290+
this._tree.deregister(this);
291+
}
292+
293+
register(child: CdkTreeItem<V>) {
294+
this._unorderedItems().add(child);
295+
this._unorderedItems.set(new Set(this._unorderedItems()));
296+
}
297+
298+
deregister(child: CdkTreeItem<V>) {
299+
this._unorderedItems().delete(child);
300+
this._unorderedItems.set(new Set(this._unorderedItems()));
301+
}
302+
}
303+
304+
/**
305+
* A structural directive that marks the `ng-template` to be used as the content
306+
* for a `CdkTreeGroup`. This content can be lazily loaded.
307+
*/
308+
@Directive({
309+
selector: 'ng-template[cdkTreeGroupContent]',
310+
hostDirectives: [DeferredContent],
311+
})
312+
export class CdkTreeGroupContent {}

src/cdk-experimental/ui-patterns/accordion/accordion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class AccordionTriggerPattern {
121121
...inputs,
122122
expansionId: inputs.value,
123123
expandable: () => true,
124-
expansionManager: () => inputs.accordionGroup().expansionManager,
124+
expansionManager: inputs.accordionGroup().expansionManager,
125125
});
126126
this.expandable = this.expansionControl.isExpandable;
127127
this.expansionId = this.expansionControl.expansionId;

0 commit comments

Comments
 (0)