angularangular2-ngcontent

How to eliminate inner component when using ng-content


I'm trying to factor out popup menus so I can write this:

<panel-menu>
  <panel-menu-item>Edit input</panel-menu-item>
  <panel-menu-item>Edit mappings</panel-menu-item>
  <panel-menu-item divider-before>Show agent code</panel-menu-item>
</panel-menu>

I have a panel-menu component with this HTML:

<div class="btn-group" [class.open]="...">
  <button type="button" class="btn btn-default" (click)="..."><i class="ion-gear-b icon-lg"></i></button>
  <ul class="dropdown-menu dropdown-menu-right">
    <ng-content select="panel-menu-item"></ng-content>
  </ul>
</div>

and panel-menu-item with this HTML:

<li *ngIf="dividerBefore" class="divider"></li>
<li><a><ng-content></ng-content></a></li>

The problem is that the resulting DOM has a panel-menu-item between the ul and the li, which breaks the third-party CSS.

Is there some way to project just the content of the selected children, and not the children themselves?

This answer suggests to use an attribute on the li instead of a component, but that leaks implementation. Users of panel-menu shouldn't need to know what elements menu items are implemented as.


Solution

  • To project just content you should wrap your content in embedded view like:

    panel-menu-item.component.ts

    @Component({
      selector: 'panel-menu-item',
      template: `
        <ng-template>
          <li *ngIf="dividerBefore" class="divider"></li>
          <li><a><ng-content></ng-content></a></li>
        </ng-template>
      `
    })
    export class PanelMenuItemComponent {
      @ViewChild(TemplateRef) content: TemplateRef<any>;
    }
    

    In the preceding code i'm wrapping template in ng-template tag and getting TemplateRef instance from it by using @ViewChild decorator.

    Having TemplateRef we can easily manage where to insert the template:

    panel-menu.component.ts

    @Component({
      selector: 'panel-menu',
      template: `
        <div class="btn-group" >
          <button type="button" class="btn btn-default">Some button</button>
          <ul class="dropdown-menu dropdown-menu-right">
            <ng-container *ngFor="let item of menuItems">
              <ng-container *ngTemplateOutlet="item.content"></ng-container>
            </ng-container>
          </ul>
        </div>
      `
    })
    export class PanelMenuComponent {
      @ContentChildren(PanelMenuItemComponent) menuItems: QueryList<PanelMenuItemComponent>;
    
      constructor(private cdRef: ChangeDetectorRef) {}
    
      ngAfterViewInit() {
        this.cdRef.detectChanges();
      }
    }
    

    I'm using @ContentChildren to get hold of our panel-menu items and then just use built-in directive NgTemplateOutlet to place content inside ul.

    We have to run the second digest cycle by using this.cdRef.detectChanges(); because we'll get error

    ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: '[object Object]'.

    as soon as @ViewChild(TemplateRef) content updates its value during change detection.

    Stackblitz Example