angulartypescripttransclusionangular2-ngcontent

How to loop through section elements to project with ng-content


I'm building a stepper and am using "transclusion" with ng-content to dynamically grab section elements within the stepper tag. The stepper.component view works like this:

<ng-content select=".one"></ng-content>
<ng-content select=".two"></ng-content>
<ng-content select=".three"></ng-content>

Component usage looks like this:

<stepper>
    <section class="one">content here<section>
    <section class="two">content here<section>
    <section class="three">content here<section>
</stepper>

However, I'd like to make it dynamic by recognizing the section elements automatically:

<ng-content *ngFor="let section of sections; index as i;" select="['section:nth-child(' + i + ')']"></ng-content>

How can I:

  1. get a nodeList of the section elements available to transclude?
  2. use ng-content's select to target them incrementally?

Solution

  • I would create a directive like:

    @Directive({
      selector: '[stepper-section]'
    })
    export class StepperSectionDirective {}
    

    then add stepper-section attribute to each of sections:

    <stepper>
        <section stepper-section>content here<section>
        <section stepper-section>content here<section>
        <section stepper-section>content here<section>
    </stepper>
    

    and finally make use of @ContentChildren decorator to query all the sections:

    @ContentChildren(StepperSectionDirective) sections: QueryList<StepperSectionDirective>;
    

    Ng-run Example

    If you want to loop through content and render it dynamically you can wrap your children with ng-template and use ngTemplateOutlet directive to render them in StepperComponent:

    html

    <app-stepper>
      <ng-template stepper-section>Content 1</ng-template>
      <ng-template stepper-section>Content 2</ng-template>
      <ng-template stepper-section>Content 3</ng-template>
    </app-stepper>
    

    stepper-section.directive.ts

    @Directive({
      selector: '[stepper-section]'
    })
    export class StepperSectionDirective {
      hidden = false;
    
      constructor(public templateRef: TemplateRef<any>) {}
    }
    

    stepper.component.ts

    @ContentChildren(StepperSectionDirective) sectionDirs: QueryList<StepperSectionDirective>;
    

    stepper.component.html

    <button *ngFor="let section of sections; index as i;"
           [class.enabled]="activeSection === i" (click)="goTo(i)">
      Step {{ i + 1 }}
    </button>
    <div class="content">
      <ng-container *ngFor="let section of sections">
        <ng-template *ngIf="!section.hidden" 
             [ngTemplateOutlet]="section.templateRef"></ng-template>
      </ng-container>
    </div>
    

    Ng-run Example

    The difference between these two approaches is that in the first case all child content gets rendered and we only manipulate display: none to those steps we want to hide.

    In the second approach we have more control on what we want to render and we render only one step at specific time.