angulartransclusion

Angular 2 transclusion: Can I pass slot content upward to a parent Component


I have an outer component (blue-green) featuring some flexbox toolbars with a bunch of my own ui-button buttons. And an inner component, mostly doing its thing in the brown area (as you would expect).

However, depending on the inner component (there are several ones what are switched forth and back), a few more contextual buttons must be inserted in that top/bottom bar.

(CSS tricks with absolute positioning stuff on the outside are not an option, depending on size and convenience the outer toolbars can vary pretty much in position, size and so on...)


Now my question is:

Can I somehow pass in a reference to some placeholder (black square brackets) (just like regular content projection/transclusion), and have them filled by content coming from the child component?

With something like ngTemplate[Outlet] perhaps? And/or using @Output?

I want to “pass upwards” more than plain text or simple <b>rich</b> <i>html</i> but ideally true angular template code, including custom components like

    <ui-button icon="up.svg" (click)='...'></ui-button>
    <ui-button icon="down.svg" (click)='...'></ui-button>

...leading in the outer component's top bar to:

<section class='some-flexbox'>
    <ui-button icon="home.svg" (click)='...'></ui-button>

    <ui-button icon="up.svg" (click)='...'></ui-button>
    <ui-button icon="down.svg" (click)='...'></ui-button>

    <ui-button icon="help.svg" (click)='...'></ui-button>
    <ui-button icon="account.svg" (click)='...'></ui-button>
</section>

Thinking about it, the click events of those buttons added should find their way back home into the child, sigh...

update

ngTemplateOutlet sounds pretty interesting

We can also take the template itself and instantiate it anywhere on the page, using the ngTemplateOutlet directive:

Examining, still unsure how...


Solution

  • There are two possible ways you can achieve the desired behavior.

    1. Using angular CDK's PortalModule
    2. By Creating a custom directive which uses javascript dom apis to move an element from child component to the parent component.

    Here is a quick explanation for both the solutions.

    1. Using PortalModule Demo Link

    You can define a template inside child component's template file.

    <ng-template #templateForParent>
        <button (click)="onTemplateBtnClicked('btn from child')">
        Button from Child
      </button>
    </ng-template>
    

    then grab the reference of that template inside your child component file using @viewChid decorator.

    @ViewChild("templateForParent", { static: true }) templateForParent: TemplateRef<any>;
    

    then you can create a TemplatePortal using that template ref and pass that portal to the parent whenever you want.

    ngAfterViewInit(): void {
        const templatePortal = new TemplatePortal(this.templateForParent, this.viewContainerRef);
        setTimeout(() => {
          this.renderToParent.emit(templatePortal);
        });
      }
    

    And the parent component's template file may look like this.

    <div fxLayout="row" fxLayoutAlign="space-between center">
      <button (click)="onBtnClick('one')">one</button>
      <button (click)="onBtnClick('two')">two</button>
      <ng-container [cdkPortalOutlet]="selectedPortal"></ng-container> <!-- pass the portal to the portalOutlet -->
      <app-button-group (renderToParent)="selectedPortal = $event"></app-button-group>
      <button (click)="onBtnClick('five')">five</button>
    </div>
    

    that's it. Now you have rendered a template to the parent component and the component still has bindings for click event. And the click event handler is defined inside the child component.

    The same way you can use ComponentPortal or DomPortal

    2. Using custom directive Demo Link

    You can create one angular directive as follows which will move the elements from its host component to the parent of the host component.

    import {Directive,TemplateRef,ElementRef,OnChanges,SimpleChanges,OnInit,Renderer2,DoCheck} from "@angular/core";
    
    @Directive({
      selector: "[appRenderToParent]"
    })
    export class RenderToParentDirective implements OnInit {
      constructor(private elementRef: ElementRef<HTMLElement>) {}
    
      ngOnInit(): void {
        const childNodes = [];
        this.elementRef.nativeElement.childNodes.forEach(node =>
          childNodes.push(node)
        );
        childNodes.forEach(node => {
          this.elementRef.nativeElement.parentElement.insertBefore(
            node,
            this.elementRef.nativeElement
          );
        });
    
        this.elementRef.nativeElement.style.display = "none";
      }
    }
    

    And then you can use this directive on any component.

    <div fxLayout="row" fxLayoutAlign="space-between center">
      <button (click)="onBtnClick('one')">one</button>
      <button (click)="onBtnClick('two')">two</button>
      <app-button-group appRenderToParent></app-button-group>
      <button (click)="onBtnClick('five')">five</button>
    </div>
    

    here <app-button-group> has following template file.

    <button (click)="onBtnClick('three')">three</button>
    <button (click)="onBtnClick('four')">four</button>
    

    So our directive will move both the button element to the parent component of its host. hare we are just moving DOM nodes in the DOM tree so all the events bound with those elements will still work.

    We can modify the directive to accept a class name or an id and to move only those elements which with that class name or the id.

    I hope this will help. I suggest reading docs for more info on PortalModule.