angular2 ng-template in a separate file

Pratap A.K picture Pratap A.K · Mar 21, 2018 · Viewed 9.8k times · Source

angular2 how to use ng-template from a different file? When I place the ng-template within the same HTML where I use it works but when I move ng-template into a separate file then it won't work. Is there a way to move ng-template into its own file and use it in different html file?

info-message.html

<ng-template #messageTemplate>
    Hi
</ng-template>

<ng-container *ngTemplateOutlet="messageTemplate;"></ng-container>

above is working fine because ng-template and the usage is in same file

message-template.html

<ng-template #messageTemplate>
    Hi
</ng-template>

info-message.html

<ng-container *ngTemplateOutlet="messageTemplate;"></ng-container>

This is not working. Is there a way to use "messageTemplate" which is in a separate file inside another html(Eg: info-message.html)

Thanks in advance.

Answer

peter554 picture peter554 · Apr 25, 2019

This behaviour can be achieved via a 'portal'. This is a useful and fairly common pattern in Angular applications. For example you may have a global sidebar outlet living near the top app level and then child components may specify a local <ng-template/>, as part of their overall template, to be rendered at this location.

Note that while the <ng-template/> may be defined outside of the file where the desired outlet is defined, it is still necessary to place the <ng-template/> inside the template of some component. This can be a minimalist component which is only responsible for wrapping the <ng-template/>, however it could equally be a complicated component where the <ng-template/> of interest only plays a minor part.

This code illustrates one possible basic implementation of a portal.

@Directive({
  selector: '[appPortal]'
})
export class PortalDirective implements AfterViewInit {
  @Input() outlet: string;

  constructor(private portalService: PortalService, private templateRef: TemplateRef<any>) {}

  ngAfterViewInit(): void {
    const outlet: PortalOutletDirective = this.portalService.outlets[this.outlet];
    outlet.viewContainerRef.clear();
    outlet.viewContainerRef.createEmbeddedView(this.templateRef);
  }
}

@Directive({
  selector: '[appPortalOutlet]'
})
export class PortalOutletDirective implements OnInit {
  @Input() appPortalOutlet: string;

  constructor(private portalService: PortalService, public viewContainerRef: ViewContainerRef) {}

  ngOnInit(): void {
    this.portalService.registerOutlet(this);
  }
}

@Injectable({
  providedIn: 'root'
})
export class PortalService {
  outlets = new Map<string, PortalOutletDirective>();

  registerOutlet(outlet: PortalOutletDirective) {
    this.outlets[outlet.appPortalOutlet] = outlet;
  }
}

It works using three parts:

  • A 'portal' directive. This lives on the desired <ng-template/> and takes as input the name of the outlet at which the content should be rendered.
  • A 'portal outlet' directive. This lives on an outlet, e.g. an <ng-container/>, and defines the outlet.
  • A 'portal' service. This is provided at the root level and stores references to the portal outlets so they can be accessed by the portals.

This may seem like a lot of work for something quite simple but once this plumbing is in place it is easy to (re)use.

<div class="container">
  <div class="row">
    <div class="col-6">
      <app-foo></app-foo>
    </div>
    <div class="col-6">
      <ng-container [appPortalOutlet]="'RightPanel'"></ng-container>
    </div>
  </div>
</div>

// foo.component.html
<h1>Foo</h1>
<ng-template appPortal [outlet]="'RightPanel'">
 <h1>RIGHT</h1>
</ng-template>

In general it's not a great idea to reinvent the wheel though when there are already well-tested, documented and stable implementations available. The Angular CDK provides such an implementation and I'd advise to use that one rather than your own in practice.