Acute Angular
Published in

Acute Angular

Angular — How to render HTML containing Angular Components dynamically at run-time

UPDATE: Shortly after writing this article, I came across another one from Netanal Basal that talks about this same topic but using Angular Elements. He is an established expert on Angular and other front-end frameworks. His technique works for directives too; my solution does not. So, try his approach first — looks exciting!

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

  1. The terms “dynamic HTML” or “dynamic template” or “dynamic template HTML” are used quite frequently in this article. They all mean the same: the description of “standalone templates” in the introduction above.
  2. 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.
  3. 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.

  1. Dynamic component loader using ComponentFactoryResolver.
  2. 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.

Sample dynamic components

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.

  1. 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.
  2. 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. Any Angular syntaxes NOT covered here will not be supported.

Sample dynamic HTML

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 would need to parse the HTML attributes and evaluate their values at run-time. Easier said than done!

Alternatives for unsupported syntaxes

  1. Directives.
    If you really do 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].
  2. Object/Variable data-binding, Interpolation, etc.
    Once you attach your dynamic HTML into DOM, you can easily search for that attribute using hostElement.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. 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.

Loading dynamic HTML to component at run-time

The dynamic components rendering factory

What do we need to achieve here?

  1. Find Components’ HTML elements in DOM (of the dynamic HTML).
  2. Create appropriate Angular Components and set their @Input properties.
  3. Wire them up into the Angular application.

That is exactly what DynamicComponentFactory<T>.create() does below.

Dynamic component factory

I hope that the code and comments are self-explanatory. So, let me cover only certain parts that require additional explanation.

  1. this.factory.create: This is the heart of this solution — the API provided by Angular to create a component by code.
  2. The first argument injector is required by Angular to inject dependencies into the instances being created.
  3. 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.
  4. 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.
  5. this.appRef.attachView(): This makes Angular aware of the components created and includes them in its change detection cycle.
  6. destroy(): Angular will not dispose 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.
  7. 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 their create(element) methods in 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.

Final code on how to use component factory to bind components at run-time

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, which it is when compared to 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.

Credit goes to this GitHub comment that acts as the basis of this solution. Also, I read in there, a few comments later, that this technique is also used by angular.io documentation site. That really boosted my confidence w.r.t. the authenticity of this technique.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Krishnan Mudaliar

Krishnan Mudaliar

35 Followers

Loves the web • DIY • DRY • ASP.NET • JavaScript • Angular • NodeJS (i/bitsy) • Believer of great UX