Load Dynamic templates within a single component using Angular 4

BVS picture BVS · Jun 29, 2017 · Viewed 23.6k times · Source

My requirement is to build a component which has 2 or more html templates where as each html template has atleast 20 controls and based on few conditions load that specific template.

Note: I chose 3 different templates because controls vary based on the templateType where as single ts file as the inputs and logic for driving get and save of values in the template remains same. Hence I decided to go with 3 templates and a ts file as a single component.

//sub.component.ts
@Component({
     selector: 'sub-component',
     template: `
               <div [ngSwitch]="templateType">
                    <ng-template *ngSwitchCase="'1'"> ${require('./sub1.component.html')} </template>
                    <ng-template *ngSwitchCase="'2'"> ${require('./sub2.component.html')} </template>
                    <ng-template *ngSwitchCase="'3'"> ${require('./sub3.component.html')} </template>
                    <ng-template ngSwitchDefault> ${require('./sub1.component.html')} </template>
</div>
    `
})

I've tried above alternative as it appears as a simple solution to achieve the behavior but compilation failing with cannot find require. In AngularJS, we have ng-Include to populate any template but it looks ng-template doesn't support to load external html content.

Please do not mark this as duplicate as there appears many queries similar to this but most of the solutions are deprecated or not applicable for Angular 4. Please advise an alternative instead of attaching different links.

Answer

diopside picture diopside · Aug 27, 2017

I know this question is old, but hopefully this helps people who are attempting similar things.

The unfortunate thing about what you want is that the easiest way to do it is to simply make each template its own component. Otherwise, you have to inject and sanitize HTML. They removed ng-include and similar capabilities because of security risks of injecting un-sanitized HTML. Its wouldn't be that much of a P.I.T.A if you didn't have to specifically import and declare all those additional components in your module, but alas...

You can create a simple directive that will get templateRefs, and then query elements on your page that have those directives, get the template ref from it, and insert them elsewhere. This would at least let you keep all the templates in a separate file. I usually put 3 or 4 templates in a separate component and include them in the component that wants to render them with . I'll describe how to do that.

Directive to get template refs

import { Directive, TemplateRef, Input } from '@angular/core';

@Directive({
 selector: 'get-template',
})
export class GetTemplateDirective {
  @Input() name: string;
  constructor(public template: TemplateRef<any>) {  }
}

Then for the templates, create a super simple component that has them all

@Component({
  selector: 'sub-component-templates',
  template: `

<ng-template get-template [name]="tpl1">
  Put Whatever here, including other components if you please
</ng-template> 

<ng-template get-template [name]="tpl2">
  Different template here
</ng-template> 

 ... and so on and so on...
`
})
export class Templates { }

Import all the relevant new components into your module, then include them inside your main component that will render the templates

I usually do it with ng-content so its clear in the parent component that this component is referencing another one for its templates.

For example, in the parent..

<sub-component>
    <sub-component-templates></sub-component-templates>
</sub-component>

Then in the sub component

 import { Component, ViewChild, ContentChildren, QueryList } from '@angular/core';
import { GetTemplateDirective } from 'wherever';

@Component({
selector: 'sub-component',
template: `

<ng-content></ng-content>

 <div #templateRenderer></div>
`
})
export class SubComponent {

@ViewChild('templateRenderer',{read:ViewContainerRef}) anchor: ViewContainerRef;
@ContentChildren(GetTemplateDirective) templates: QueryList<GetTemplateDirective>;

ngAfterContentInit()  {

  ... at this stage, you will have access to all the included templates with that directive on them. You can perform your logic to choose which one you want. Once you have selected the proper one, you can embed it like so ... 
 let desiredTemplateName = 'whatever';

     for (let t of this.templates.toArray()) {
       if(t.name === desiredTemplateName) {
          this.anchor.createEmbeddedView(t.template);
          break;        
       } 
     }  
   }

}

You can see that this is ridiculously complicated for what you are trying to do. It would be easier to just create them as separate components, and use the ngSwitchCase to choose the proper one. The advantage to the method I've described above is that it lets you keep your templates wherever you want really, and you could include 100 in the same external component (which is really no more than the bare minimum decorated component with a template) if you wanted, or move them around with a service, or whatever.

See here for a working example of how to use the compiler - https://plnkr.co/edit/fdP9Oc?p=info

Still quite complicated...

If you store the template as a property of the class, you could change it later as needed. Just add a template ref import...

 import { Component, ViewChild, ContentChildren, QueryList, TemplateRef } from '@angular/core';

and then create a property

 template: TemplateRef<any>;

Then later you could switch it out with one from your querylist and create the embeddedview again using the view container's methods.

Angular 2 / 4 made certain things easier... and made certain things waaaaaay more difficult. But I guess in this case, its in the name of security.