Introduction
Say, you had a set of components defined in your Angular application, <your-component-1> <your-component-2> <your-component-3> …. Wouldn’t it be awesome if you could create standalone templates consisting of <your-components> and other “Angular syntaxes” (Inputs, Outputs, Interpolation, Directives, etc.), load them at run-time from back-end, and Angular would render them all — all <your-components> created and all Angular syntaxes evaluated? Wouldn’t that be perfect!?
You can’t have everything, but at least something will work.
Alas! We have to accept the fact that Angular does not support rendering templates dynamically by itself. Additionally, there may be some security considerations too. Do make sure you understand these important factors first.
But, all is not lost! So, let us explore an approach to achieve, at least, one small but significant subset of supporting dynamic template rendering: dynamic HTML containing Angular Components, as the title of this article suggests.
Terminology Notes
-
- I have used terms like “dynamic HTML” or “dynamic template” or “dynamic template HTML” quite frequently in this article. They all mean the same: the description of “standalone templates” in the introduction above.
- The term “dynamic components” means all <your-components> put/used in a dynamic HTML; please do not confuse it for “components defined or compiled dynamically at run-time” — we won’t be doing that in this article.
- “Host Component”. For a component or projected component in a dynamic HTML, its Host Component is the component that created it, e.g. in “Sample dynamic HTML” gist below, the Host Component of [yourComponent6] is the component that will create that dynamic HTML, not <your-component-3> inside which [yourComponent6] is placed in the dynamic HTML.
Knowledge Prerequisites
As a prerequisite to fully understand the proposed solution, I recommend that you get an idea about the following topics if not aware of them already.
-
- Dynamic component loader using ComponentFactoryResolver.
- Content Projection in Angular— pick your favorite article from Google.
Sample dynamic components
Let us define a few of <your-components> that we will be using in our sample “dynamic template HTML” in the next section.
@Component({ selector: 'your-component-1', template: ` <div>This is your component 1.</div> <div [ngStyle]="{ 'color': status }">My name is: {{ name }}</div> `, }) export class YourComponent1 { @Input() name: string = ''; @Input() status: string = 'green'; } @Component({ selector: 'your-component-2', template: ` <div>This is your component 2 - {{ name }}.</div> <div *ngIf="filtering === 'true'">Filtered Id: {{ id }}.</div> `, }) export class YourComponent2 { @Input() id: string = '0'; @Input() name: string = ''; @Input() filtering: 'true' | 'false' = 'false'; } @Component({ selector: 'your-component-3', template: ` <div>This is your component 3 - {{ name }} ({{ ghostName || 'Ghost' }}).</div> <ng-content></ng-content> <div>End of your component 3</div> `, }) export class YourComponent3 implements OnInit { @Input() id: number = 0; // Beware! `number` data-type @Input() name: string = ''; // Initialized - Will work @Input() ghostName: string; // Not initialized - Will not be available in `anyComp` for-in loop ngOnInit(): void { console.log(this.id === 45); // prints false console.log(this.id === '45'); // prints true console.log(typeof this.id === 'number'); // prints false console.log(typeof this.id === 'string'); // prints true } } @Component({ selector: '[yourComponent6]', // Attribute selector based component template: ` <div *ngIf="!offSide || !strongSide"> <div [hidden]="!offSide">This is your component 6.</div> <div [ngStyle]="{ 'color': status }">The official motto is: {{ offSide }} - {{ strongSide }}.</div> </div>`, }) export class YourComponent6 { @Input() offSide: string = ''; @Input() strongSide: string = 'green field'; }
Take a quick look at YourComponent3, the comments against name, ghostName and ngOnInit. This translates to the first restriction of my proposed solution: an @Input property must be initialized to a string value. There are two parts here.
- Inputs must be of string type. I impose this restriction because the value of any attribute of an HTML Element in a dynamic HTML is going to be of string type. So, better to keep your components’ inputs’ data types consistent with that of the value you will be setting on them.
- Inputs must be initialized. Otherwise, Typescript removes that property during transpilation, which causes problems for setComponentAttrs() (see later in the solution) — it cannot find the input property of that component at run-time, hence won’t set that property even if dynamic HTML has the appropriate HTML Element attribute defined.
Sample dynamic HTML
Let us also define a dynamic HTML. All syntaxes mentioned here will work. The process will not support Angular syntaxes that I have NOT covered here.
<div> <p>Any HTML Element</p> <!-- simple component --> <your-component-1></your-component-1> <!-- simple component with "string" inputs --> <your-component-2 id="111" name="Krishnan" filtering="true"></your-component-2> <!-- component containing another content projected component --> <your-component-3 id="45"> <your-component-1 name="George"></your-component-1> <div yourComponent6 offSide="hello" strongSide="world">...</div> </your-component-3> <!-- simple component with string inputs --> <your-component-4 ...></your-component-4> <!-- simple component with string inputs --> <your-component-5 ...></your-component-5> </div>
Let me clarify again the second restriction of my proposed solution: no support for Directives, Pipes, interpolation, two-way data-binding, [variable] data-binding, ng-template, ng-container, etc. in a dynamic template HTML.
To clarify further the “@Input string” restriction, only hard-coded string values are supported, i.e. no variables like [attrBinding]=”stringVariable”. This is because, to support such object binding, we need to parse the HTML attributes and evaluate their values at run-time. Easier said than done!
Alternatives for unsupported syntaxes
- Directives.
If you really want to use an attribute directive, the best alternative here is to create a @Component({ selector: ‘[attrName]’ }) instead. In other words, you can create your component with any Angular-supported selector — tag-name selector, [attribute] selector, .class-name selector, or even a combination of them, e.g. a[href]. - Object/Variable data-binding, Interpolation, etc.
Once you attach your dynamic HTML to DOM, you can easily search for that attribute using host.Element.getElementById|Name|TagName or querySelector|All and set its value before you create the components. Alternatively, you could manipulate the HTML string itself before attaching it to the DOM. (This will become clearer in the next section.)
Attach dynamic template HTML to DOM
It is now time to make the dynamic HTML available in DOM. There are multiple ways to achieve this: using ElementRef, @ViewChild, [innerHTML] attribute directive, etc. The below snippet provides a few examples that subscribe to an Observable<string> representing a template HTML stream and attaching it to the DOM on resolution.
import { AfterViewInit, Component, ElementRef } from '@angular/core'; import { Observable, of } from 'rxjs'; import { delay } from 'rxjs/operators'; @Component({ selector: 'app-binder', template: `<div #divBinder></div>`, }) export class BinderComponent implements AfterViewInit { templateHtml$: Observable<string> = of('load supported-features.html here').pipe(delay(1000)); @ViewChild('divBinder') divBinder: ElementRef<HTMLElement>; constructor(private elementRef: ElementRef) { } ngAfterViewInit(): void { // Technique #1 this.templateHtml$.subscribe(tpl => this.elementRef.nativeElement.innerHTML = tpl); // Technique #2 this.templateHtml$.subscribe(tpl => divBinder.nativeElement.innerHTML = tpl); } }
The dynamic components rendering factory
What do we need to achieve here?
-
- Find Components’ HTML elements in DOM (of the dynamic HTML).
- Create appropriate Angular Components and set their @Input
- Wire them up into the Angular application.
That is exactly what DynamicComponentFactory<T>.create() does below.
import { Injectable, ComponentRef, Injector, Type, ComponentFactory, ComponentFactoryResolver, ApplicationRef } from '@angular/core'; export class DynamicComponentFactory<T> { private embeddedComponents: ComponentRef<T>[] = []; constructor( private appRef: ApplicationRef, private factory: ComponentFactory<T>, private injector: Injector, ) { } //#region Creation process /** * Creates components (of type `T`) as detected inside `hostElement`. * @param hostElement The host/parent Dom element inside which component selector needs to be searched. * _rearrange_ components rendering order in Dom, and also remove any not present in this list. */ create(hostElement: Element): ComponentRef<T>[] { // Find elements of given Component selector type and put it into an Array (slice.call). const htmlEls = Array.prototype.slice.call(hostElement.querySelectorAll(this.factory.selector)) as Element[]; // Create components const compRefs = htmlEls.map(el => this.createComponent(el)); // Add to list this.embeddedComponents.push(...compRefs); // Attach created components into ApplicationRef to include them change-detection cycles. compRefs.forEach(compRef => this.appRef.attachView(compRef.hostView)); // Return newly created components in case required outside return compRefs; } private createComponent(el: Element): ComponentRef<T> { // Convert NodeList into Array, cuz Angular dosen't like having a NodeList passed for projectableNodes const projectableNodes = [Array.prototype.slice.call(el.childNodes)]; // Create component const compRef = this.factory.create(this.injector, projectableNodes, el); const comp = compRef.instance; // Apply ALL attributes inputs into the dynamic component (NOTE: This is a generic function. Not required // when you are sure of initialized component's input requirements. // Also note that only static property values work here since this is the only time they're set. this.setComponentAttrs(comp, el); return compRef; } private setComponentAttrs(comp: T, el: Element): void { const anyComp = (comp as any); for (const key in anyComp) { if ( Object.prototype.hasOwnProperty.call(anyComp, key) && el.hasAttribute(key) ) { anyComp[key] = el.getAttribute(key); // console.log(el.getAttribute('name'), key, el.getAttribute(key)); } } } //#endregion //#region Destroy process destroy(): void { this.embeddedComponents.forEach(compRef => this.appRef.detachView(compRef.hostView)); this.embeddedComponents.forEach(compRef => compRef.destroy()); } //#endregion } /** * Use this Factory class to create `DynamicComponentFactory<T>` instances. * * @tutorial PROVIDERS: This class should be "provided" in _each individual component_ (a.k.a. Host component) * that wants to use it. Also, you will want to inject this class with `@Self` decorator. * * **Reason**: Well, you could have `providedIn: 'root'` (and without `@Self`, but that causes the following issues: * 1. Routing does not work correctly - you don't get the correct instance of ActivatedRoute. */ @Injectable() export class DynamicComponentFactoryFactory { constructor( private appRef: ApplicationRef, private injector: Injector, private resolver: ComponentFactoryResolver, ) { } create<T>(componentType: Type<T>): DynamicComponentFactory<T> { const factory = this.resolver.resolveComponentFactory(componentType); return new DynamicComponentFactory<T>(this.appRef, factory, this.injector); } }
I hope that the code and comments are self-explanatory. So, let me cover only certain parts that require additional explanation.
-
- this.factory.create: This is the heart of this solution— the API provided by Angular to create a component by code.
- The first argument injector is required by Angular to inject dependencies into the instances being created.
- The second argument projectableNodes is an array of all “Projectable Nodes” of the component to be created, e.g. in “Sample dynamic HTML” gist, <your-component-1> and <div yourComponent6> are the projectable nodes of <your-component-3>. If this argument is not provided, then these Nodes inside <your-component-3> will not be rendered in the final view.
- setComponentAttrs(): This function loops through all public properties of the created component’s instance and sets their values to corresponding attributes’ values of the Host Element el, but only if found, otherwise the input holds its default value defined in the component.
- this.appRef.attachView(): This makes Angular aware of the components created and includes them in its change detection cycle.
- destroy(): Angular will not dispose of any dynamically created component for us automatically. Hence, we need to do it explicitly when the Host Component is being destroyed. In our current example, our Host Component is going to be BinderComponent explained in the next section.
- Note that DynamicComponentFactory<T> works for only one component type <T> per instance of that factory class. So, to bind multiple types of Components, you must create multiple such factory instances per Component Type. To make this process easier, we make use of DynamicComponentFactoryFactory class (Sorry, couldn’t think of a better name.) Apart from that, the other reason to have this wrapper class is that you cannot directly inject Angular’s ComponentFactory<T>, which is the second constructor dependency of DynamicComponentFactory<T>. (There must be better ways to manage the factory creation process. Open to suggestions.)
We are now ready to use this factory class to create dynamic components.
Create Angular Components in dynamic HTML
Finally, we create instances of DynamicComponentFactory<T> per “dynamic component” type using DynamicComponentFactoryFactory and call its create(element) methods in the loop, where element is the HTML Node that contains the dynamic HTML. We may also perform custom “initialization” operations on the newly created components. See Lines 55–65.
import { AfterViewInit, ComponentRef, Component, ElementRef, EventEmitter, OnDestroy, Self } from '@angular/core'; import { Observable, of } from 'rxjs'; import { delay } from 'rxjs/operators'; import { DynamicComponentFactory, DynamicComponentFactoryFactory } from './dynamic-component-factory'; @Component({ selector: 'app-binder', template: ``, providers: [ DynamicComponentFactoryFactory, // IMPORTANT! ], }) export class BinderComponent implements AfterViewInit, OnDestroy { private readonly factories: DynamicComponentFactory<any>[] = []; private readonly hostElement: Element; templateHtml$: Observable<string> = of('load supported-features.html here').pipe(delay(1000)); components: any[] = [ YourComponent1, YourComponent2, YourComponent3, // ... add others ]; constructor( // @Self - best practice; to avoid potential bugs if you forgot to `provide` it here @Self() private cmpFactory: DynamicComponentFactoryFactory, elementRef: ElementRef, ) { this.hostElement = elementRef.nativeElement; } ngAfterViewInit(): void { this.templateHtml$.subscribe(tpl => { this.hostElement.innerHTML = tpl this.initFactories(); this.createAllComponents(); }); } private initFactories(): void { components.forEach(c => { const f = this.cmpFactory.create(c); this.factories.push(f); }); } // Create components dynamically private createAllComponents(): void { const el = this.hostElement; const compRefs: ComponentRef<any>[] = []; this.factories.forEach(f => { const comps = f.create(el); compRefs.push(...comps); // Here you can make use of compRefs, filter them, etc. // to perform any custom operations, if required. compRefs .filter(c => c.instance instanceof YourComponent2) .forEach(c => { c.instance.name = 'hello'; c.instance.filtering = 'false'; c.instance.someFoo('welcome'); ); }); } private removeAllComponents(): void { this.factories.forEach(f => f.destroy()); } ngOnDestroy(): void { this.removeAllComponents(); } }
DynamicComponentFactoryFactory Provider (Important!)
Notice in BinderComponent that DynamicComponentFactoryFactory has been provided in its own @Component decorator and is injected using @Self. As mentioned in its JSDoc comments, this is important because we want the correct instance of Injector to be used for creating components dynamically. If the factory class is not provided at the Host Component level and instead providedIn: ‘root’ or some ParentModule, then the Injector instance will be of that level, which may have unintended consequences, e.g. relative link in [routerLink]=”[‘.’, ‘..’, ‘about-us’]” used in, say, YourComponent1 may not work correctly.
That’s it!
Conclusion
If you have made it this far, you may be thinking, “Meh! This is a completely stripped-down version of Angular templates’ capabilities. That’s no good for me!”. Yes, I will not deny that. But, believe me, it is still quite a “power-up”! I have been able to create a full-fledged Website that renders dynamic, user-defined templates using this approach, and it works perfectly well.
Even though we cannot render fully-loaded dynamic templates at run-time, we have seen in this article how we can render at least “components with static string inputs”. This may seem like a crippled solution if you compare it with all the wonderful features that Angular provides at compile-time. But, practically, this may still solve a lot of use cases requiring dynamic template rendering.
Let’s consider this a partial success.
Hope you found the article useful.