'Query items inside <ng-content> with ContentChild

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 element
  • menu: which is to set inside the dropdown, and over the item that is part of the the menu that will appear
  • item: 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 ?



Solution 1:[1]

@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>

Solution

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>

Solution 2:[2]

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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Justwell Solets