Load Dynamic templates within a single component using Angular 4
Asked Answered
U

2

10

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.

Uncrowned answered 29/6, 2017 at 4:26 Comment(4)
Isn't there any solution? No answer even after 39 Views.. i think there should be a github issue for tracking this item.Uncrowned
This answer will also help youMeacham
Hi, @Uncrowned Did you get any specific solution for this? I am having also the same requirement.Kalimantan
@Thakkar, No I've did with four different components.Uncrowned
I
13

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.

Irs answered 27/8, 2017 at 7:3 Comment(5)
I was able to get this working with a service, but not by directly adding the compenents as shown in the example. The directives would instantiate, and I could prove it with log messages, but the templates would not be available. Would view encapsulation effect that? However, I did notice that every time you place the template component into any other component, it re-loads ALL of the templates. So it is much more efficient to use a service anyway, which is working marvelously. Thanks for this answer!Brake
@Brake would u mind sharing how you did this from a service? I'm a bit lostAretha
@Aretha I used Rxjs Subject<TemplateRef<any>> in the service similar to the docs: angular.io/guide/…Brake
@Aretha Use ngAfterViewInit lifecycle hook in the component where you store all of the templates to push the components to the service. I prefer to use local variables instead of directives, just to keep things simple. So <ng-tempalate #myTemplate> in the component html, then @ViewChild('myTemplate', { read: TemplateRef }) template: TemplateRef<any> in component.ts, then send to service in ngAfterViewInit. If you need further example please ask another question and I will answer there.Brake
your plunker is not working can you give example in fiddleFederalist
I
0

I'm actually trying to figure this out as we speak and when I came across the answer above it gave me an idea that I'm about to try. Hopefully I'll have some success with it and be able to update this answer with something more concrete.

I'm always working on my questionnaires and I have questions that are multiple choice questions with worded answers, multiple choice questions with a "scale of 1-10" type of mechanism, then some questions that require a text answer.

I think making a component and wrapping each template in an ngIf condition that connects to a variable in the class which the data will be passed into can trigger the templeate.

So the data can look something like this

Questions:[{
    question:'blahblahblah',
    answers: [..array of answers..],
    template: 'a'
    },
    {
    question: 'yaddayaddayadda',
    answers: [],
    template: 'b'
    },
    {
    etc.
    }
    ]

Then in the component class you can have something like this

@Component ({
    selector: 'question-component',
    templateUrl: './template.html'
})

export class QuestionComponent {

    @input() data: yourDataType;

    constructor() {} 

}

then in the template for this component have something like

<div *ngIf="data.template === a">
    <!-- code for template with binding and everything -->
</div>

<div *ngIf="data.template === b">
    <!-- code -->
</div>

<!-- etc. etc. etc. -->

Then in the main component's template you can do something like

<div *ngFor = "question of Questions">
    <question-component [data]="question"></question-component>
</div>

This is all hypothetical off the top of my head so I may be missing some things but I feel it's worth it to have here as something to start poking around at in the meantime. I'm going to see if I can get it to work for my needs.

Intuitionism answered 27/8, 2017 at 6:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.