14

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.

Pratap A.K
  • 3,816
  • 6
  • 36
  • 72
  • Please don't do it. It's is insecure. If you do intend to do that then an option is to put them in different files, make an Xhr request to get the HTML file, sanitize then and put them in the content page. Other option is to put them as a different component. Do also check blurys answer – Gary Apr 25 '19 at 16:29

5 Answers5

5

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.

peter554
  • 985
  • 9
  • 21
4

Have you seen this? https://github.com/angular/angular/issues/27503 There is an example there provided by dawidgarus

The suggestion is that if you want to reuse your template in different files, you should convert what is inside the template into a separate component, then you can reuse that component wherever you want.

KlavierCat
  • 148
  • 4
  • 11
0

You can use something like this (template is used from another component):

@Component(
    template: '<ng-container *ngTemplateOutlet="infoMessage.template;"></ng-container>'
)
export class MessageTemplate {
    infoMessage: InfoMessage;    
}

@Component(
    ....
)
export class InfoMessage{    
    @ContentChild('columnTemplate') template: TemplateRef<any>;

    constructor(private messageTemplate: MessageTemplate) {
        messageTemplate.infoMessage = this;
    }
}
bluray
  • 1,629
  • 4
  • 28
  • 55
  • Thanks for the answer. But I don't have 2 component I have one component and a file for ng-template – Pratap A.K Mar 21 '18 at 12:22
  • I think it's not good, you dont have one component and two template files. The basis of the angular are the components. So you could create simple component,which would do nothing just to offer a template – bluray Mar 21 '18 at 12:34
  • Thanks. But even the answer you provided is not working. I am getting error as can't read property template of undefined. – Pratap A.K Mar 21 '18 at 12:49
  • 1
    It's simple example for illustration. Or you can use something like this: `` and in component2 you will have `ContentChild` and component1 `ngTemplateOutlet` – bluray Mar 21 '18 at 12:56
0

If you are loading a separate file, you can define a Component in the separate file (instead of a <ng-template>). And then inject the entire Component into the <ng-container> using the *ngComponentOutlet.

You can find the full sulotion with example here: https://stackoverflow.com/a/59180628/2658683

Gil Epshtain
  • 5,688
  • 5
  • 42
  • 67
0

Expanding on the answer by @peter554 for reasons of explanation and portability. This will let you use a template across components.

To use:

'app.module.ts'
import {NgModule} from '@angular/core';
import {
    IdcPortalDirective, IdcTemplatePortalDirective,
    PortalService
} from './idc-template-portal/idc-template-portal.component';

@NgModule({
    declarations: [
        IdcPortalDirective,
        IdcTemplatePortalDirective
    ],
    imports: [],
    exports: [],
    providers: [
        PortalService
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}
'./idc-template-portal/idc-template-portal.component.ts'
import {
    AfterViewInit,
    Directive,
    Injectable,
    Input,
    OnInit, Output,
    TemplateRef,
    ViewContainerRef
} from '@angular/core';
/*** Input Template ***/
/*** <ng-template idcPortal [outlet]="'outletname'">Template Contents</ng-template> ***/
@Directive({
    selector: '[idcPortal]'
})
export class IdcPortalDirective implements OnInit {
    @Input() outlet: string;
    @Output() inlet: string = this.outlet;

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

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

}
/*** Output Container ***/
/*** <ng-container [idcPortalOutlet]="'outletname'"></ng-container> ***/
@Directive({
    selector: '[idcPortalOutlet]'
})
export class IdcTemplatePortalDirective implements OnInit, AfterViewInit {
    @Input() appPortalOutlet: string;
    @Output() outlet: string = this.appPortalOutlet;

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

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

    ngAfterViewInit() {
        this.portalService.initializePortal(this.appPortalOutlet);
    }

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

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

    registerInlet(inlet: IdcPortalDirective) {
        this.inlets[inlet.inlet] = inlet;
    }

    initializePortal(portal:string) {
        const inlet: IdcPortalDirective = this.inlets[portal];
        const outlet: IdcTemplatePortalDirective = this.outlets[portal];
        outlet.viewContainerRef.clear();
        outlet.viewContainerRef.createEmbeddedView(inlet.templateRef);
    }
}

He,@peter554, mentions reinventing the wheel in regards to the Angular CDK portals package. However, I find his/this implementation to make more sense in the way it's used in the application flow and the ease in which a template can be ported from component to another component that contains the portal outlet (allowing component to component->portal template communication. For example within a component template implementing the Angular Material MatBottomSheet (idcBottomSheet)).

The 'input' element:

<!--
/*
  For example, perhaps you have a mobile view
  where a template is hidden (via css) and ported
  over to a MatBottomSheet component template to be 
  popped up when requested (by button click). 
*/
-->
<button #bottomsheetButton (click)="openBottomSheet(Notes)" mat-button>
    <mat-icon>notes</mat-icon>
</button>
<!--/* hidden in mobile view mode. */-->
<ng-content *ngTemplateOutlet="Notes"></ng-content>
<ng-template #Notes idcPortal [outlet]="'idcBottomSheet'"><!--/* template to port */-->
    <form>
        <mat-form-field class="w-100 h-100">
            <mat-label>A place for your thoughts:</mat-label>
            <textarea matInput
                      cdkTextareaAutosize
                      #autosize="cdkTextareaAutosize"
                      cdkAutosizeMinRows="10"
                      cdkAutosizeMaxRows="10"
                      placeholder="Angular. It makes me feel...">
            </textarea>
        </mat-form-field>
    </form>
</ng-template>

The 'output' element (inside your MatBottomSheet component template):

<ng-container [idcPortalOutlet]="'appIdcBottomSheet'"></ng-container>
Eric Shoberg
  • 83
  • 1
  • 10