9

I have the following structure of nested components.

<app-root>
    <app-comp-1>
      <app-comp-2>
      </app-comp-2>
   </app-comp-1>
  <app-root>

I want to transclude any content into the last child (app-comp-2). So, I need something like this.

<app-comp-1>
    <app-comp-2>
        <ng-content></ng-content>
    </app-comp-2>
</app-comp-1>

But in the app-root component is available only the app-comp-1 component. So, this is the place where I have to transclude my contents.

<app-root>
    <app-comp-1>
        <content-I-want-to-transclude></content-I-want-to-transclude>
    </app-comp-1>
</app-root>
 ---------------------------
<app-comp-1>
    <ng-content></ng-content>
    <app-comp-2>
        ...
    </app-comp-2>
</app-comp-1>

So I need a solution to get the content that has been transcluded into the first component and pass it down to the second one.

Plunker

Steven Lignos
  • 217
  • 3
  • 10

3 Answers3

14

This Github issue provides a nice solution to this problem, using the ngProjectAs attribute.

In a situation with 2 layers of content projection, the first layer uses an ng-content element with an ngProjectAs attribute, with the next selector.

The second layer uses another ng-content, and selects the value of the first layer's ngProjectAs attribute:

Level 1 (parent component):

<ng-content select="my-component" ngProjectAs="arbitrary-selector"></ng-content>

Level 2 (nested child component):

<ng-content select="arbitrary-selector"></ng-content>

Usage:

<my-app>
  <my-component>My Projected Content</my-component>
</my-app>

Resultant DOM:

<my-app>
  <my-component>
      <nested-component>My Projected Content</nested-component>
  </my-component>
</my-app>
hevans900
  • 847
  • 7
  • 15
  • Thanks @hevans900 but I fail to reproduce it. Notably, since transclusion is not supported at root level (before ivy), how do you manage to have the `my-app` root component not replacing its whole contents (with whatever its template)? Maybe an online example could clarify this. – Jérôme Beau Feb 16 '19 at 12:55
  • 1
    Sorry it was just a pseudocode example - I didn't actually try it with the root (bootstrapped) component - and I see no reason why anyone would want to btw! – hevans900 Sep 11 '19 at 09:10
  • 1
    `ngProjectAs` is **very** important. – herve Jun 08 '22 at 15:52
4

I had a similar problem, whereby I have a card component, which has a child card-header component as well as a selector for the card body.

The card-header component has a toggle button that dispatches actions for which cards are open / closed.

I then needed the ability to pass extra buttons into the card-header component from the parent component via the card component

I solved it by adding selectors at each level.

First I created a common card-header component, allowing me to have a single piece of code that handled toggling card content by dispatching actions to the NgRx store, which holds an array of cards that are hidden (using the supplied name input property).

The card-header component subscribes to the store and emits an event to the parent component when the toggled status changes

@Component({
  selector: 'po-card-header',
  template: `
    <div class="card-header">

      <span class="text-uppercase">{{ header }}</span>

      <div class="header-controls">
        <ng-content select=[card-header-option]></ng-content>
        <ng-content select=[header-option]></ng-content>

        <span class="header-action" (click)="onTogglePanel()">
         <i class="fa" [ngClass]="{ 'fa-caret-up': !collapsed, 'fa-caret-down': collapsed}"></i>
        </span>

      </div>

    </div>
  `
})
export class CardHeaderComponent implements OnInit, OnDestroy {
  ...
  @Input() name: string;
  @Output() togglePanel = new EventEmitter<boolean>();

  collapsed$: Observable<boolean>;
  collapsedSub: Subscription;

  constructor(private store: Store<State>) {
    this.collapsed$ = this.store.select(state => getPanelCollapsed(state, this.name);
  }

  ngOnInit(): void {
    this.collapsedSub = this.collapsed$.subscribe(collapsed => {
      this.collapsed = collapsed;
      this.togglePanel.emit(collapsed);
    });
  }

  .... unsubscribe on destroy.
}

Notice the header has 2 ng-content sections.

The header-option selector is for any other icons I want to add when I explicitly use this component e.g.

<div class="card">

  <po-card-header>
    ...
    <span header-option class="fa fa-whatever" (click)="doSomething()"></span>
  </po-card-header>

  ...

</div>

My new icon will sit alongside the default toggle icon in the header.

The second card-header-option selector is for root components, that use the card component, not the card-header component, but still want to pass extra icons into the header.

@Component({
  selector: 'po-card',
  template: `

    <div class="card">
      <po-card-header>
        ...
        <ng-content select="[card-header-option] header-option></ng-content>
      </po-card-header>

      <div class="card-block">
        <ng-content select="[card-body]"></ng-content>
      </div>

    </div>
  `
})
...

The [card-header-option] selector will select any elements with that attribute, then pass them down into the card-header component using the header-option attribute

The final usage of my card component looks like this.

<div>
   Some component that uses the card
   <po-card 
     header="Some text to go in the card header" 
     name="a-unique-name-for-the-card">

     <span card-header-option class='fa fa-blah header-action'></span>

     <div card-body>
       Some content that gets put inside the [card-body] selector on the card component.
     </div>

   </po-card>
</div>

The final result is that I can use my custom card component, and get the benefits of the toggle functionality that the card-header component gives, but also supply my own custom actions, which will also get rendered in the header

Hope you find this helpful :-)

Matt Sugden
  • 844
  • 6
  • 12
0

Transclusion is not supported in root element, see this.

Hoewever you may add ng-content in all the componnets so as to pass the content.

@Component({
  selector: 'my-app',
  template: `
  <app-comp-1>
    <app-comp-2>
      <app-comp-3>
         Hello World
      </app-comp-3>
    </app-comp-2>
  </app-comp-1>
  `
})
export class AppComponent { name = 'Angular'; }

@Component({
  selector: 'app-comp-1',
  template: `
   app-comp-1
   <ng-content></ng-content>
  `
})
export class AppComponent1 {  }

@Component({
  selector: 'app-comp-2',
  template: `
   app-comp-2
   <ng-content></ng-content>
  `
})
export class AppComponent2 {  }

@Component({
  selector: 'app-comp-3',
  template: `
   app-comp-3
   <ng-content></ng-content>
  `
})
export class AppComponent3 {  }

See this Plunker.

Madhu Ranjan
  • 17,334
  • 7
  • 60
  • 69
  • Thanks for the plunker! But I have to deal with a case where the "Hello world" is available only in app-comp-1 and I want to get it from there and pass it down to app-comp-3. The content must be available only in the first component – Steven Lignos Dec 20 '16 at 20:49
  • can you fork and update the plunker with your use case and add it to the question? – Madhu Ranjan Dec 20 '16 at 20:53
  • Unfortunately the plunker seems to not be working any more. – theycallmemorty Dec 01 '21 at 16:19