MarkD
MarkD

Reputation: 4944

Angular- Is it possible to use content projection from within a child component?

NOTE- I realize this example could much more easily be done without using content projection. I am using it as a very simplified example.

Lets say I have the following component that lists of names in two different elements:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-name-list',
  templateUrl: `
  <div class='large'>
    <h1>Large Names</h1>
    <ng-content select="large"></ng-content>
  </div>
  <div class='small'>
    <h1>Small Names</h1>
    <ng-content select="small"></ng-content>
  </div>
`,
  styleUrls: ['./name-list.component.css']
})
export class TestNgContentComponent {
  constructor() { }
}

I can then call this component from a template with two lists of names as follows:

<app-names-list>
  <h1 ngProjectAs="large" *ngFor="let n of names">{{ n }}</h1>
  <h2 ngProjectAs="small" *ngFor="let n of names">{{ n }}</h2>
</app-names-list>

Notice that the same data is used for both lists of names. Is it possible to replace the passed h1 and h2 tags, with a component that contains both, and projects them to the parent? E.g. something like this:

@Component({
  selector: 'app-name',
  template: `
    <h1 ngProjectAs="large">{{name}}</h1>
    <h2 ngProjectAs="small">{{name}}</h2>
  `,
  styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent  {
  @Input() name: string;
}

and then modify the template to look like:

<app-names-list>
  <app-name *ngFor="let n of names" [name]="n"></app-name>
</app-names-list>

Once I embed the ngProjectAs directives in the app-name template, this no longer works. Is there a way to do a projection like this from within a child component?

Upvotes: 2

Views: 4674

Answers (1)

Andrei Gătej
Andrei Gătej

Reputation: 11934

Here is my approach:

I wrapped the header tags in an ng-template so it can be attached to an ng-container as an embedded view.
You can read more about embedded and host views here.

@Component({
  selector: 'app-name',
  template: `
    <ng-template #large>
      <h1>{{name}}</h1>
    </ng-template>

    <ng-template #small>
      <h2>{{name}}</h2>
    </ng-template>
  `,
  styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent  {
  @Input() name: string;
  @ViewChild('large', { static: true, read: TemplateRef }) large: TemplateRef<any>;
  @ViewChild('small', { static: true, read: TemplateRef }) small: TemplateRef<any>;
}

Here I have replaced ng-content with ng-container so that I can attach embedded views.

@Component({
  selector: 'app-name-list',
  template: `
    <div class='large'>
      <h1>Large Names</h1>
      <ng-container #large></ng-container>
    </div>

    <ng-container #foo></ng-container>

    <div class='small'>
      <h1>Small Names</h1>
      <ng-container #small></ng-container>
    </div>
  `,
})
export class TestNgContentComponent {
  @ContentChildren(HelloComponent, { read: HelloComponent }) children: QueryList<HelloComponent>;
  @ViewChild('large', { static: true, read: ViewContainerRef }) largeNamesContainer: ViewContainerRef;
  @ViewChild('small', { static: true, read: ViewContainerRef }) smallNamesContainer: ViewContainerRef;

  constructor() { }

  ngAfterContentInit () {
    this.populateView();
  }

  private populateView () {
    this.largeNamesContainer.clear();
    this.smallNamesContainer.clear();

    this.children.forEach(child => {
      this.largeNamesContainer.createEmbeddedView(child.large);
      this.smallNamesContainer.createEmbeddedView(child.small);
    });
  }
}

Here is a StackBlitz demo.

Upvotes: 4

Related Questions