Crocsx
Crocsx

Reputation: 7609

Query items inside <ng-content> with ContentChild

I am trying to make a custom dropdown, and face the following problem :

I have 3 directives :

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

Answers (2)

Kevin Baker
Kevin Baker

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>

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>

Upvotes: 1

Justwell Solets
Justwell Solets

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

Related Questions