Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Chaining transclusion in nested components

Tags:

angular

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

like image 774
Steven Lignos Avatar asked Dec 20 '16 16:12

Steven Lignos


2 Answers

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>
like image 198
hevans900 Avatar answered Oct 02 '22 11:10

hevans900


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 :-)

like image 21
Matt Sugden Avatar answered Oct 02 '22 11:10

Matt Sugden