@@ -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' ;
2323import { _IdGenerator } from '@angular/cdk/a11y' ;
2424import { Directionality } from '@angular/cdk/bidi' ;
2525import { DeferredContent , DeferredContentAware } from '@angular/cdk-experimental/deferred-content' ;
2626import { 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} )
12759export 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,32 @@ 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. */
229169 private readonly _parentPattern : Signal < TreeItemPattern < V > | TreePattern < V > | undefined > =
230- computed ( ( ) => this . _cdkTreeItem ?. pattern ?? this . _treePattern ( ) ) ;
170+ computed ( ( ) => this . _treeItem ?. pattern ?? this . _treePattern ( ) ) ;
171+
172+ /** The host native element. */
173+ readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
231174
232175 /** The value of the tree item. */
233176 readonly value = input . required < V > ( ) ;
@@ -239,16 +182,15 @@ export class CdkTreeItem<V> extends BaseGroupable {
239182 readonly label = input < string > ( ) ;
240183
241184 /** Search term for typeahead. */
242- readonly searchTerm = computed ( ( ) => this . label ( ) ?? this . _element ( ) . textContent ) ;
185+ readonly searchTerm = computed ( ( ) => this . label ( ) ?? this . element ( ) . textContent ) ;
243186
244187 /** Manual group assignment. */
245- readonly group = signal < CdkGroup < V > | undefined > ( undefined ) ;
188+ readonly group = signal < CdkTreeGroup < V > | undefined > ( undefined ) ;
246189
247190 /** The UI pattern for this item. */
248191 pattern : TreeItemPattern < V > = new TreeItemPattern < V > ( {
249192 ...this ,
250193 id : ( ) => this . _id ,
251- element : this . _element ,
252194 tree : this . _treePattern ,
253195 parent : this . _parentPattern ,
254196 children : computed (
@@ -261,11 +203,109 @@ export class CdkTreeItem<V> extends BaseGroupable {
261203 } ) ;
262204
263205 constructor ( ) {
264- super ( ) ;
206+ afterRenderEffect ( ( ) => {
207+ const group = [ ...this . _tree . unorderedGroups ( ) ] . find ( group => group . value ( ) === this . value ( ) ) ;
208+ if ( group ) {
209+ this . group . set ( group ) ;
210+ }
211+ } ) ;
265212
266213 // Updates the visibility of the owned group.
267214 afterRenderEffect ( ( ) => {
268215 this . group ( ) ?. visible . set ( this . pattern . expanded ( ) ) ;
269216 } ) ;
270217 }
218+
219+ ngOnInit ( ) {
220+ this . _tree . register ( this ) ;
221+ this . _parentGroup ?. register ( this ) ;
222+ }
223+
224+ ngOnDestroy ( ) {
225+ this . _tree . deregister ( this ) ;
226+ this . _parentGroup ?. deregister ( this ) ;
227+ }
271228}
229+
230+ /**
231+ * Container that designates content as a group.
232+ */
233+ @Directive ( {
234+ selector : '[cdkTreeGroup]' ,
235+ exportAs : 'cdkTreeGroup' ,
236+ hostDirectives : [
237+ {
238+ directive : DeferredContentAware ,
239+ inputs : [ 'preserveContent' ] ,
240+ } ,
241+ ] ,
242+ host : {
243+ 'class' : 'cdk-tree-group' ,
244+ 'role' : 'group' ,
245+ '[id]' : 'id' ,
246+ '[attr.inert]' : 'visible() ? null : true' ,
247+ } ,
248+ } )
249+ export class CdkTreeGroup < V > implements OnInit , OnDestroy , HasElement {
250+ /** A reference to the group element. */
251+ private readonly _elementRef = inject ( ElementRef ) ;
252+
253+ /** The DeferredContentAware host directive. */
254+ private readonly _deferredContentAware = inject ( DeferredContentAware ) ;
255+
256+ /** The top level CdkTree. */
257+ private readonly _tree = inject ( CdkTree < V > ) ;
258+
259+ /** All groupable items that are descendants of the group. */
260+ private readonly _unorderedItems = signal ( new Set < CdkTreeItem < V > > ( ) ) ;
261+
262+ /** The host native element. */
263+ readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
264+
265+ /** Unique ID for the group. */
266+ readonly id = inject ( _IdGenerator ) . getId ( 'cdk-tree-group-' ) ;
267+
268+ /** Whether the group is visible. */
269+ readonly visible = signal ( true ) ;
270+
271+ /** Child items within this group. */
272+ readonly children = computed ( ( ) => [ ...this . _unorderedItems ( ) ] . sort ( sortDirectives ) ) ;
273+
274+ /** Identifier for matching the group owner. */
275+ readonly value = input . required < V > ( ) ;
276+
277+ constructor ( ) {
278+ // Connect the group's hidden state to the DeferredContentAware's visibility.
279+ afterRenderEffect ( ( ) => {
280+ this . _deferredContentAware . contentVisible . set ( this . visible ( ) ) ;
281+ } ) ;
282+ }
283+
284+ ngOnInit ( ) {
285+ this . _tree . register ( this ) ;
286+ }
287+
288+ ngOnDestroy ( ) {
289+ this . _tree . deregister ( this ) ;
290+ }
291+
292+ register ( child : CdkTreeItem < V > ) {
293+ this . _unorderedItems ( ) . add ( child ) ;
294+ this . _unorderedItems . set ( new Set ( this . _unorderedItems ( ) ) ) ;
295+ }
296+
297+ deregister ( child : CdkTreeItem < V > ) {
298+ this . _unorderedItems ( ) . delete ( child ) ;
299+ this . _unorderedItems . set ( new Set ( this . _unorderedItems ( ) ) ) ;
300+ }
301+ }
302+
303+ /**
304+ * A structural directive that marks the `ng-template` to be used as the content
305+ * for a `CdkTreeGroup`. This content can be lazily loaded.
306+ */
307+ @Directive ( {
308+ selector : 'ng-template[cdkTreeGroupContent]' ,
309+ hostDirectives : [ DeferredContent ] ,
310+ } )
311+ export class CdkTreeGroupContent { }
0 commit comments