Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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...

like image 404
Frank Nocke Avatar asked Feb 26 '20 16:02

Frank Nocke


People also ask

Can we pass data from child to parent in Angular?

The Angular documentation says "The @Output() decorator in a child component or directive lets data flow from the child to the parent." This is exactly what we want.

How do you pass data to a child component?

To let Angular know that a property in a child component or directive can receive its value from its parent component we must use the @Input() decorator in the said child. The @Input() decorator allows data to be input into the child component from a parent component.


2 Answers

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.

like image 101
HirenParekh Avatar answered Jan 02 '23 11:01

HirenParekh


If each inner component defines a template for the header content and another template for the footer content, and associates these templates to public properties having a specific name (e.g. header and footer), the outer component can access these properties and embed the content of each template in an ngTemplateOutlet.

Define the templates in the inner component:

<ng-template #header1>
  <button (click)="onChild1HeaderClick('A')">Command A</button>
  <button (click)="onChild1HeaderClick('B')">Command B</button>
</ng-template>
<ng-template #footer1>
  <button (click)="onChild1FooterClick('C')">Command C</button>
</ng-template>

and associate them to the properties header and footer:

@ViewChild("header1", { static: true }) header: TemplateRef<any>;
@ViewChild("footer1", { static: true }) footer: TemplateRef<any>;

Assuming that the inner component is inserted in the parent component with a router outlet, associate a template reference variable #inner to the outlet:

<router-outlet #inner="outlet"></router-outlet>

and use that variable to access the header and footer properties of the inner component, and to insert their content in the outer component:

<section>
  ...
  <ng-container *ngTemplateOutlet="inner.isActivated ? inner.component.header : null">
  </ng-container>
</section>
...
<section>
  ...
  <ng-container *ngTemplateOutlet="inner.isActivated ? inner.component.footer : null">
  </ng-container>
</section>

You can take a look this stackblitz for a demo. A simpler case is shown in this other stackblitz, where the inner component is declared directly in the outer component template.

like image 41
ConnorsFan Avatar answered Jan 02 '23 13:01

ConnorsFan