Reputation: 7609
I am trying to make a custom dropdown, and face the following problem :
I have 3 directives :
dropdown
: which is to set on the dropdown elementmenu
: which is to set inside the dropdown, and over the item that is part of the
the menu that will appearitem
: which is to set inside all items within a menu.Inside the menu, I query for all items using
@ContentChildren(ItemDirective) menuItems: QueryList<ItemDirective>;
And inside the dropdown I check all the items within the menu using
@ContentChild(MenuDirective) private menu: MenuDirective;
onKeyDown(key: string) {
console.log(this.menu.menuItems.length);
}
When it's linear like this :
<button appDropdown>
<div appMenu>
<div *ngFor="let i of [1, 2, 3, 4, 5]" appItem>{{ i }}</div>
</div>
</button>
It works, but there is a case where (in a select) I want the items to be added as ng-content
so
<button appDropdown>
<div appMenu>
<ng-content></ng-content>
</div>
</button>
In this second case, the items are not found.
Stackblitz (click on button then press any key and watch logs)
Is there a way to make this work ?
Upvotes: 0
Views: 1513
Reputation: 629
@ContentChildren is only scoped to the components direct <ng-content></ngcontent>
tag, and will not look up the layout tree to see if the specified content children also live in a parents <ng-content></ng-content>
tag.
Meaning, the @ContentChildren tag will only capture content children within its directly scoped <ng-content></ng-content
, and not from a parents <ng-content></ng-content
.
Directly from Angulars docs: Full Description on Angular Website.
@ContentChildren does not retrieve elements or directives that are in other components' templates, since a component's template is always a black box to its ancestors.
That's why this does work ✅
<button appDropdown>
<div appMenu>
<!-- not a black box to your appMenu directive -->
<!-- therefore, it will be seen by your @ContentChildren tag -->
<div *ngFor="let i of [1, 2, 3, 4, 5]" appItem>{{ i }}</div>
</div>
</button>
That's why this doesn't work ❌
<button appDropdown>
<div appMenu>
<!-- this is a black box to your appMenu directive -->
<!-- because the ng-content tag is scoped to the component -->
<!-- therefore, it WON'T be seen by your @ContentChildren tag -->
<!-- inside the appMenu directive -->
<ng-content></ng-content>
</div>
</button>
This means you can't structure your code the way you want to with the directives that you've created, but will instead have todo something like this if you want your select component to work with ng-content.
select.component.ts
@Component({
selector: 'app-select',
templateUrl: './select.component.html',
styleUrls: ['./select.component.css'],
})
export class SelectComponent {
@ContentChildren(ItemDirective) menuItems: QueryList<ItemDirective>;
@HostListener('keydown', ['$event']) keyDownEvent(event: KeyboardEvent) {
console.log('k');
this.onKeyDown(event.code);
event.preventDefault();
}
constructor() {}
onKeyDown(key: string) {
console.log(this.menuItems.length);
}
}
select.component.html
<button>
<div>
<!-- ng-content scoped to select.component.ts -->
<!-- therefore, your ItemDirective's will be captured by -->
<!-- @ContentChildren if they are direct descendants inside ng-content -->
<ng-content></ng-content>
</div>
</button>
Upvotes: 1
Reputation: 171
Are you looking something like this?
Find out the sample code here. Clone it and run the reusable-ng-content
project by using ng serve --project=reusable-ng-content
. Hope it may helpful.
Upvotes: 0