How can I use/create dynamic template to compile dynamic Component with Angular 2.0?
Asked Answered
M

16

226

I want to dynamically create a template. This should be used to build a ComponentType at runtime and place (even replace) it somewhere inside of the hosting Component.

Until RC4 I was using ComponentResolver, but with RC5 I get the following message:

ComponentResolver is deprecated for dynamic compilation.
Use ComponentFactoryResolver together with @NgModule/@Component.entryComponents or ANALYZE_FOR_ENTRY_COMPONENTS provider instead.
For runtime compile only, you can also use Compiler.compileComponentSync/Async.

I found this document (Angular 2 Synchronous Dynamic Component Creation)

And understand that I can use either

  • Kind of dynamic ngIf with ComponentFactoryResolver. If I pass known components inside of @Component({entryComponents: [comp1, comp2], ...}) - I can use .resolveComponentFactory(componentToRender);
  • Real runtime compilation, with Compiler...

But the question is how to use that Compiler? The note above says that I should call: Compiler.compileComponentSync/Async - so how?

For example. I want to create (based on some configuration conditions) this kind of template for one kind of settings

<form>
   <string-editor
     [propertyName]="'code'"
     [entity]="entity"
   ></string-editor>
   <string-editor
     [propertyName]="'description'"
     [entity]="entity"
   ></string-editor>
   ...

and in another case this one (string-editor is replaced with text-editor)

<form>
   <text-editor
     [propertyName]="'code'"
     [entity]="entity"
   ></text-editor>
   ...

And so on (different number/date/reference editors by property types, skipped some properties for some users...). i.e. this is an example, the real configuration could generate much more different and complex templates.

The template is changing, so I cannot use ComponentFactoryResolver and pass existing ones... I need a solution with the Compiler.

Mcalpine answered 11/8, 2016 at 5:37 Comment(4)
SInce the solution I found was so nice I want everybody finding this question to have a look at my answer which is very far down at the very bottom at the moment. :)Pule
The article Here is what you need to know about dynamic components in Angular has great explanation of the dynamic components.Tinishatinker
Here's the problem with every single answer out there and what $compile could actually do that these methods can't -- I'm creating an application where I just want to compile the HTML as it comes in through a 3rd party's page and ajax calls. I can't remove the HTML from the page and place it in my own template. SighAbreaction
@AugieGardner There is a reason why this is not possible by design. Angular is not at fault for bad architectural decisions or legacy systems that some people have. If you want to parse existing HTML-code you are free to use another framework as Angular works perfectly fine with WebComponents. Setting clear boundaries to guide the hordes of inexperienced programmers is more important than allowing dirty hacks for few legacy systems.Tabling
M
173

EDIT - related to 2.3.0 (2016-12-07)

NOTE: to get solution for previous version, check the history of this post

Similar topic is discussed here Equivalent of $compile in Angular 2. We need to use JitCompiler and NgModule. Read more about NgModule in Angular2 here:

In a Nutshell

There is a working plunker/example (dynamic template, dynamic component type, dynamic module,JitCompiler, ... in action)

The principal is:
1) create Template
2) find ComponentFactory in cache - go to 7)
3) - create Component
4) - create Module
5) - compile Module
6) - return (and cache for later use) ComponentFactory
7) use Target and ComponentFactory to create an Instance of dynamic Component

Here is a code snippet (more of it here) - Our custom Builder is returning just built/cached ComponentFactory and the view Target placeholder consume to create an instance of the DynamicComponent

  // here we get a TEMPLATE with dynamic content === TODO
  var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

        component.entity = this.entity;
        //...
    });

This is it - in nutshell it. To get more details.. read below

.

TL&DR

Observe a plunker and come back to read details in case some snippet requires more explanation

.

Detailed explanation - Angular2 RC6++ & runtime components

Below description of this scenario, we will

  1. create a module PartsModule:NgModule (holder of small pieces)
  2. create another module DynamicModule:NgModule, which will contain our dynamic component (and reference PartsModule dynamically)
  3. create dynamic Template (simple approach)
  4. create new Component type (only if template has changed)
  5. create new RuntimeModule:NgModule. This module will contain the previously created Component type
  6. call JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule) to get ComponentFactory
  7. create an Instance of the DynamicComponent - job of the View Target placeholder and ComponentFactory
  8. assign @Inputs to new instance (switch from INPUT to TEXTAREA editing), consume @Outputs

NgModule

We need an NgModules.

While I would like to show a very simple example, in this case, I would need three modules (in fact 4 - but I do not count the AppModule). Please, take this rather than a simple snippet as a basis for a really solid dynamic component generator.

There will be one module for all small components, e.g. string-editor, text-editor (date-editor, number-editor...)

@NgModule({
  imports:      [ 
      CommonModule,
      FormsModule
  ],
  declarations: [
      DYNAMIC_DIRECTIVES
  ],
  exports: [
      DYNAMIC_DIRECTIVES,
      CommonModule,
      FormsModule
  ]
})
export class PartsModule { }

Where DYNAMIC_DIRECTIVES are extensible and are intended to hold all small parts used for our dynamic Component template/type. Check app/parts/parts.module.ts

The second will be module for our Dynamic stuff handling. It will contain hosting components and some providers.. which will be singletons. Therefor we will publish them standard way - with forRoot()

import { DynamicDetail }          from './detail.view';
import { DynamicTypeBuilder }     from './type.builder';
import { DynamicTemplateBuilder } from './template.builder';

@NgModule({
  imports:      [ PartsModule ],
  declarations: [ DynamicDetail ],
  exports:      [ DynamicDetail],
})

export class DynamicModule {

    static forRoot()
    {
        return {
            ngModule: DynamicModule,
            providers: [ // singletons accross the whole app
              DynamicTemplateBuilder,
              DynamicTypeBuilder
            ], 
        };
    }
}

Check the usage of the forRoot() in the AppModule

Finally, we will need an adhoc, runtime module.. but that will be created later, as a part of DynamicTypeBuilder job.

The forth module, application module, is the one who keeps declares compiler providers:

...
import { COMPILER_PROVIDERS } from '@angular/compiler';    
import { AppComponent }   from './app.component';
import { DynamicModule }    from './dynamic/dynamic.module';

@NgModule({
  imports:      [ 
    BrowserModule,
    DynamicModule.forRoot() // singletons
  ],
  declarations: [ AppComponent],
  providers: [
    COMPILER_PROVIDERS // this is an app singleton declaration
  ],

Read (do read) much more about NgModule there:

A template builder

In our example we will process detail of this kind of entity

entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
};

To create a template, in this plunker we use this simple/naive builder.

The real solution, a real template builder, is the place where your application can do a lot

// plunker - app/dynamic/template.builder.ts
import {Injectable} from "@angular/core";

@Injectable()
export class DynamicTemplateBuilder {

    public prepareTemplate(entity: any, useTextarea: boolean){
      
      let properties = Object.keys(entity);
      let template = "<form >";
      let editorName = useTextarea 
        ? "text-editor"
        : "string-editor";
        
      properties.forEach((propertyName) =>{
        template += `
          <${editorName}
              [propertyName]="'${propertyName}'"
              [entity]="entity"
          ></${editorName}>`;
      });
  
      return template + "</form>";
    }
}

A trick here is - it builds a template which uses some set of known properties, e.g. entity. Such property(-ies) must be part of dynamic component, which we will create next.

To make it a bit more easier, we can use an interface to define properties, which our Template builder can use. This will be implemented by our dynamic Component type.

export interface IHaveDynamicData { 
    public entity: any;
    ...
}

A ComponentFactory builder

Very important thing here is to keep in mind:

our component type, build with our DynamicTypeBuilder, could differ - but only by its template (created above). Components' properties (inputs, outputs or some protected) are still same. If we need different properties, we should define different combination of Template and Type Builder

So, we are touching the core of our solution. The Builder, will 1) create ComponentType 2) create its NgModule 3) compile ComponentFactory 4) cache it for later reuse.

An dependency we need to receive:

// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';
    
@Injectable()
export class DynamicTypeBuilder {

  // wee need Dynamic component builder
  constructor(
    protected compiler: JitCompiler
  ) {}

And here is a snippet how to get a ComponentFactory:

// plunker - app/dynamic/type.builder.ts
// this object is singleton - so we can use this as a cache
private _cacheOfFactories:
     {[templateKey: string]: ComponentFactory<IHaveDynamicData>} = {};
  
public createComponentFactory(template: string)
    : Promise<ComponentFactory<IHaveDynamicData>> {    
    let factory = this._cacheOfFactories[template];

    if (factory) {
        console.log("Module and Type are returned from cache")
       
        return new Promise((resolve) => {
            resolve(factory);
        });
    }
    
    // unknown template ... let's create a Type for it
    let type   = this.createNewComponent(template);
    let module = this.createComponentModule(type);
    
    return new Promise((resolve) => {
        this.compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                factory = _.find(moduleWithFactories.componentFactories
                                , { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });
}

Above we create and cache both Component and Module. Because if the template (in fact the real dynamic part of that all) is the same.. we can reuse

And here are two methods, which represent the really cool way how to create a decorated classes/types in runtime. Not only @Component but also the @NgModule

protected createNewComponent (tmpl:string) {
  @Component({
      selector: 'dynamic-component',
      template: tmpl,
  })
  class CustomDynamicComponent  implements IHaveDynamicData {
      @Input()  public entity: any;
  };
  // a component for this particular template
  return CustomDynamicComponent;
}
protected createComponentModule (componentType: any) {
  @NgModule({
    imports: [
      PartsModule, // there are 'text-editor', 'string-editor'...
    ],
    declarations: [
      componentType
    ],
  })
  class RuntimeComponentModule
  {
  }
  // a module for just this Type
  return RuntimeComponentModule;
}

Important:

our component dynamic types differ, but just by template. So we use that fact to cache them. This is really very important. Angular2 will also cache these.. by the type. And if we would recreate for the same template strings new types... we will start to generate memory leaks.

ComponentFactory used by hosting component

Final piece is a component, which hosts the target for our dynamic component, e.g. <div #dynamicContentPlaceHolder></div>. We get a reference to it and use ComponentFactory to create a component. That is in a nutshell, and here are all the pieces of that component (if needed, open plunker here)

Let's firstly summarize import statements:

import {Component, ComponentRef,ViewChild,ViewContainerRef}   from '@angular/core';
import {AfterViewInit,OnInit,OnDestroy,OnChanges,SimpleChange} from '@angular/core';

import { IHaveDynamicData, DynamicTypeBuilder } from './type.builder';
import { DynamicTemplateBuilder }               from './template.builder';

@Component({
  selector: 'dynamic-detail',
  template: `
<div>
  check/uncheck to use INPUT vs TEXTAREA:
  <input type="checkbox" #val (click)="refreshContent(val.checked)" /><hr />
  <div #dynamicContentPlaceHolder></div>  <hr />
  entity: <pre>{{entity | json}}</pre>
</div>
`,
})
export class DynamicDetail implements AfterViewInit, OnChanges, OnDestroy, OnInit
{ 
    // wee need Dynamic component builder
    constructor(
        protected typeBuilder: DynamicTypeBuilder,
        protected templateBuilder: DynamicTemplateBuilder
    ) {}
    ...

We just receive, template and component builders. Next are properties which are needed for our example (more in comments)

// reference for a <div> with #dynamicContentPlaceHolder
@ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef}) 
protected dynamicComponentTarget: ViewContainerRef;
// this will be reference to dynamic content - to be able to destroy it
protected componentRef: ComponentRef<IHaveDynamicData>;

// until ngAfterViewInit, we cannot start (firstly) to process dynamic stuff
protected wasViewInitialized = false;

// example entity ... to be recieved from other app parts
// this is kind of candiate for @Input
protected entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
  };

In this simple scenario, our hosting component does not have any @Input. So it does not have to react to changes. But despite of that fact (and to be ready for coming changes) - we need to introduce some flag if the component was already (firstly) initiated. And only then we can start the magic.

Finally we will use our component builder, and its just compiled/cached ComponentFacotry. Our Target placeholder will be asked to instantiate the Component with that factory.

protected refreshContent(useTextarea: boolean = false){
  
  if (this.componentRef) {
      this.componentRef.destroy();
  }
  
  // here we get a TEMPLATE with dynamic content === TODO
  var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

        component.entity = this.entity;
        //...
    });
}

small extension

Also, we need to keep a reference to compiled template.. to be able properly destroy() it, whenever we will change it.

// this is the best moment where to start to process dynamic stuff
public ngAfterViewInit(): void
{
    this.wasViewInitialized = true;
    this.refreshContent();
}
// wasViewInitialized is an IMPORTANT switch 
// when this component would have its own changing @Input()
// - then we have to wait till view is intialized - first OnChange is too soon
public ngOnChanges(changes: {[key: string]: SimpleChange}): void
{
    if (this.wasViewInitialized) {
        return;
    }
    this.refreshContent();
}

public ngOnDestroy(){
  if (this.componentRef) {
      this.componentRef.destroy();
      this.componentRef = null;
  }
}

done

That is pretty much it. Do not forget to Destroy anything what was built dynamically (ngOnDestroy). Also, be sure to cache dynamic types and modules if the only difference is their template.

Check it all in action here

to see previous versions (e.g. RC5 related) of this post, check the history

Mcalpine answered 11/8, 2016 at 5:37 Comment(22)
I see you still use the directives keyword in @component ( type.builder.ts). Is that not depreciated since RC5? If yes, is there a solution that avoids this? I am thinking of putting @ngmodule's into the app/parts/*.ts, but have not been succesful yet...Shrove
Adapted @Radim's plunker to use dynamic module generation. It would probably be prettier to create separate modules for the two directives and import only one of them selectively... what do you think? plnkr.co/edit/Mrh3CL5qBABkUGl9DU8H?p=previewShrove
this look like such a complicated solution, the deprecated one was very simple and clear, is there other way to do this ?Robbert
@cwyers glad to see it could help someone, sir ;)Beliabelial
I think the same way as @tibbus: this got way more complicated than it used to be with the deprecated code. Thanks for your answer, though.Lexical
@LucioMollinedo I agree, that steps we have to do to make it happen are complex. No doubt... I just tried to show how ;)Beliabelial
To all of those concerned about the complexity of this solution, I would urge you to actually look at the plunker example. There is maybe one extra block of code for this method compared to pre rc.5. There is only one file that is actally important in the plunkr type.builder.ts. Everything else is only useful for the specific example he was using to write this. It is a lot simpler than he wrote it out to be.Laidlaw
@Laidlaw thanks for your note. Let me clarify something. Many other answers try to make it simple. But I am trying to explain it and show it in a scenario, closed to real usage. We would need to cache stuff, we would have to call destroy on re-creation etc. So, while the magic of dynamic building is really in type.builder.ts as you've pointed, I wish, that any user would understand how to place that all into context... Hope it could be useful ;)Beliabelial
@RadimKöhler: it doesn't work for me (anymore?) with TypeScript 2 und Angular 2. The createNewComponent doesn't compile with this error: Return type of public method from exported class has or is using private name 'CustomDynamicComponent'. Any ideas?Simpkins
@Simpkins could you reproduce that in plunker? I am running angular 2.0.1 and TS 2.0.3 (Visual Studio).. and it is working. - I just upgraded to 2.0.2 - and can confirm that this is really working for me.. and on pretty large application with complex dynamic stuffBeliabelial
@RadimKöhler: no, it does work in the plunker :( Too bad, I have no idea, really. I created an Ionic 2 project and copy&pasted the plunker there. In the meantime I found that someone else has the same problem but no working answer: #35314614Simpkins
@Simpkins without more detail, code, plunker it is really hard to help here. I would suggest.. issue new question and refer all the findings you have until now. Hope you'll find your solution, sir!Beliabelial
@RadimKöhler: thank you, and YES, I finally found the solution: in tsconfig.json I had "declaration": true. Setting that to false or deleting it fixes the error!Simpkins
@Simpkins I dived into your issue and tried to explain it here: https://mcmap.net/q/55611/-how-can-i-return-a-class-from-a-typescript-functionBeliabelial
For those who use angular-cli version 1.0.0-beta.22 and above, a small but mean workaround is needed. For details please see my separate answer.Greenburg
@Radim Köhler - I have tried this example. it's working without AOT. But when I tried to do run this with AOT it shows error "No NgModule metadata found for RuntimeComponentModule". can you plz help me to solve this error .Grozny
The answer itself is perfect! But for real life applications not practicable. The angular team should provide a solution for this in the framework, as this is common requirement in business applications. If not, it has to be asked if Angular 2 is the right platform for business applications.Petulia
same error as @Grozny since updating to Angular 4. I would really appreciate if someone found a solutionAnastassia
To me, Module are still not really dynamic since DYNAMIC_DIRECTIVES holds static reference (via import). Am I wrong ?. Real dynamic means that DYNAMIC_DIRECTIVES is set at runtime with Components/Modules that are not known at compilation time. How can we achieve this ?Unseal
@RadimKöhler all solutions with dynamic component has a static Anchor point ( in your example #dynamicContentPlaceHolder). It's possible to make a several dynamic points? Could you change your solution for more flexibility. For instance in a dynamic table. Where the user render the cellsProa
Another option would be to just use Polymer. In Polymer, instead of going through factories, resolvers, modules, etc. to add a dynamic template, you just add it using vanilla JS, let the browser do it's thing and listen for the event when it's done... data-binding, handlers, styles and everything else are taken care of by Polymer or native browser capabilities when available.Scopolamine
I am also facing the same as @maxou and Trusha it's working without AOT. But when I tried to do run this with AOT it shows error "No NgModule metadata found for RuntimeComponentModule". Did anyone got answer for that!?Window
S
59

EDIT (26/08/2017): The solution below works well with Angular2 and 4. I've updated it to contain a template variable and click handler and tested it with Angular 4.3.
For Angular4, ngComponentOutlet as described in Ophir's answer is a much better solution. But right now it does not support inputs & outputs yet. If [this PR](https://github.com/angular/angular/pull/15362] is accepted, it would be possible through the component instance returned by the create event.
ng-dynamic-component may be the best and simplest solution altogether, but I haven't tested that yet.

@Long Field's answer is spot on! Here's another (synchronous) example:

import {Compiler, Component, NgModule, OnInit, ViewChild,
  ViewContainerRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

@Component({
  selector: 'my-app',
  template: `<h1>Dynamic template:</h1>
             <div #container></div>`
})
export class App implements OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(private compiler: Compiler) {}

  ngOnInit() {
    this.addComponent(
      `<h4 (click)="increaseCounter()">
        Click to increase: {{counter}}
      `enter code here` </h4>`,
      {
        counter: 1,
        increaseCounter: function () {
          this.counter++;
        }
      }
    );
  }

  private addComponent(template: string, properties?: any = {}) {
    @Component({template})
    class TemplateComponent {}

    @NgModule({declarations: [TemplateComponent]})
    class TemplateModule {}

    const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === TemplateComponent
    );
    const component = this.container.createComponent(factory);
    Object.assign(component.instance, properties);
    // If properties are changed at a later stage, the change detection
    // may need to be triggered manually:
    // component.changeDetectorRef.detectChanges();
  }
}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

Live at http://plnkr.co/edit/fdP9Oc.

Sapp answered 15/9, 2016 at 9:41 Comment(17)
I'd say, that it is an example how to write as less code as possible to do the same as in my answer https://mcmap.net/q/54166/-how-can-i-use-create-dynamic-template-to-compile-dynamic-component-with-angular-2-0. In case, that it should be useful-case (mostly RE-generating template) when condition changes... the simple ngAfterViewInit call with a const template won't work. But if your task was to reduce the above detailed described approach (create template, create component, create module, compile it, create factory.. create instance)... you probably did itBeliabelial
Thanks for the solution: I am having trouble loading templateUrl and styles though, I get the following error: No ResourceLoader implementation has been provided . Can't read the url localhost:3000/app/pages/pages_common.css, any idea what I am missing?Raeleneraf
Could it possible to compile the html template with data specific for cell in grid like control.? plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview In this plunker, how can I compile the and show the image in last column.? Any help.?Woozy
Thank you for this simplified one page version. It's massively helped me to understand the more thorough explanation above.Pliny
How do you remove the component and module after adding it? I have a case where I need to 'refresh' the module, but its giving me multiple declaration errors.Grovergroves
Doesn't seem to be working - added "{{1+1}}" to template string and it crashes.Paraesthesia
@monnef, I've added {{1 + 1}} to my plunk above and it works fine.Sapp
@ReneHamburger Well, it shows 2 but also crashes on Uncaught (in promise): Error: Error in ./TemplateComponent class TemplateComponent - inline template:0:3 caused by: Expression has changed after it was checked. Previous value: 'CD_INIT_VALUE'. Current value: 'Example template: 2'. It seems like the view has been created after its parent and its children have been dirty checked. Has it been created in a change detection hook ?Paraesthesia
@monnef, you're right. I didn't check the console log. I've adjusted the code to add the component in the ngOnInit rather than the ngAfterViewInit hook, as the former is triggered before and the latter after the change detection. (See github.com/angular/angular/issues/10131 and similar threads.)Sapp
I have a question regarding this answer, posted here.Blume
This is fantastic! I have one question though. How would I go about defining input properties on my TemplateComponent and passing the values to it? For instance, I want my template to contain {{row[columnDef.field]}}. How do I create an @Input for row and columnDef on my TemplateComponent? I think you can assume what I am trying to do...Hendiadys
@WebWanderer, wouldn't it be sufficient to declare those as public variables on TemplateComponent and set them in the parent code after the component has been created? Or are you using the template component somewhere else through a selector you've assigned to it?Sapp
@ReneHamburger Actually, you might be right. That is a really neat approach that I hadn't thought of taking. I was making a table that could be dynamically configured, but creating components for the cells wound up very memory intensive, as I expected. I am trying a different approach entirely now. But thanks for the suggestion!Hendiadys
@ReneHamburger all solutions with dynamic component has a static Anchor point ( in your example #container). It's possible to make a dynamic point? Could you change your solution for more flexibility. For instance in a dynamic table. Where the user render the cellsProa
neat and simple. Worked as expected when serving over browser in dev. But does this work with AOT? When the app is run in PROD after compilation, I get an "Error: Runtime compiler is not loaded" at the moment the component compilation is attempted. (btw, I am using Ionic 3.5)Leisure
@ReneHamburger what about button operations like (click)="actions()" how could I make it and make use of Input and Output here. would be helpful to see these functionality in your plunker.Trever
will this execute scripts inserted into the template?Diversification
A
52

I must have arrived at the party late, none of the solutions here seemed helpful to me - too messy and felt like too much of a workaround.

What I ended up doing is using Angular 4.0.0-beta.6's ngComponentOutlet.

This gave me the shortest, simplest solution all written in the dynamic component's file.

  • Here is a simple example which just receives text and places it in a template, but obviously you can change according to your need:
import {
  Component, OnInit, Input, NgModule, NgModuleFactory, Compiler
} from '@angular/core';

@Component({
  selector: 'my-component',
  template: `<ng-container *ngComponentOutlet="dynamicComponent;
                            ngModuleFactory: dynamicModule;"></ng-container>`,
  styleUrls: ['my.component.css']
})
export class MyComponent implements OnInit {
  dynamicComponent;
  dynamicModule: NgModuleFactory<any>;

  @Input()
  text: string;

  constructor(private compiler: Compiler) {
  }

  ngOnInit() {
    this.dynamicComponent = this.createNewComponent(this.text);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));
  }

  protected createComponentModule (componentType: any) {
    @NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
  }

  protected createNewComponent (text:string) {
    let template = `dynamically created template with text: ${text}`;

    @Component({
      selector: 'dynamic-component',
      template: template
    })
    class DynamicComponent implements OnInit{
       text: any;

       ngOnInit() {
       this.text = text;
       }
    }
    return DynamicComponent;
  }
}
  • Short explanation:
    1. my-component - the component in which a dynamic component is rendering
    2. DynamicComponent - the component to be dynamically built and it is rendering inside my-component

Don't forget to upgrade all the angular libraries to ^Angular 4.0.0

Hope this helps, good luck!

UPDATE

Also works for angular 5.

Aerator answered 6/2, 2017 at 10:18 Comment(17)
Thanks for the heads up on this, might upgrade and then give this a crack - doing it in Angular2 is bound to break in Angular4 anyway.Diep
This worked great for me with Angular4. The only adjustment I had to make was to be able to specify import modules for the dynamically created RuntimeComponentModule.Pentameter
Do you have a working plunkr or something like that? I can't seem to get this to work for me on Angular 4.0.0Renaissance
Here's a quick example starting from the Angular Quickstart: embed.plnkr.co/9L72KpobVvY14uiQjo4pPentameter
this seems to work only if you use template for each component to be injected. templateUrl fails. Is there a way to use templateUrl? Templates can become unwieldy as a stringGravity
with ngComponentOutlet you can't pass data into the child component and from child component to parent , is that true?Brennabrennan
@ChrisTarasovs. That's what I've found as well. I think there is a pull request pending to address this, but no idea when it will be implemented.Unbent
@RahulPatel , Ophir Please look at this plunkr. plnkr.co/edit/fW3jY6VyLCvuR0HBC8Gu?p=preview Is it possible to have 2 way binding between AppComponent and foo-bar component ?Poniard
Hi @RahulPatel, sorry but this post was placed a long time ago, I'll try to look into it, but I'm pretty sparse in time these days, perhaps someone else here can answer....Aerator
Hi, this solution is the best and simple, but can anyone suggest a way not to implement DynamicComponent class inside a function because it maybe long class in my caseCat
Does this solution work with "ng build --prod"? It seems that the compiler class and AoT do not fit together atm.Laager
@OphirStern I also discovered that is approach works well in Angular 5 but NOT with the --prod build flag.Endothelioma
I tested it with angular 5 (5.2.8) using the JitCompilerFactory and using the --prod flag does not work! Does anyone have a solution? (BTW JitCompilerFactory without the --prod flag works flawlessly)Priming
or issues with --prod, which is an issue with AOT, you can exclude certain files/folders/patterns from AOT using this method github.com/angular/angular-cli/issues/… AOT is good for big modules and lots of stuff but excluding one html file displaying a template -this might help you to have bothDiversification
anyone able to find solution for --prod flag.Spark
I was able to get this to build using: ng build --prod --aot=false --build-optimizer=falseCheckerberry
i am able to make a production build but get ERROR Error: No component factory found for e. Did you add it to @NgModule.entryComponents?Lashonlashond
F
26

2019 June answer

Great news! It seems that the @angular/cdk package now has first-class support for portals!

As of the time of writing, I didn't find the above official docs particularly helpful (particularly with regard to sending data into and receiving events from the dynamic components). In summary, you will need to:

Step 1) Update your AppModule

Import PortalModule from the @angular/cdk/portal package and register your dynamic component(s) inside entryComponents

@NgModule({
  declarations: [ ..., AppComponent, MyDynamicComponent, ... ]
  imports:      [ ..., PortalModule, ... ],
  entryComponents: [ ..., MyDynamicComponent, ... ]
})
export class AppModule { }

Step 2. Option A: If you do NOT need to pass data into and receive events from your dynamic components:

@Component({
  selector: 'my-app',
  template: `
    <button (click)="onClickAddChild()">Click to add child component</button>
    <ng-template [cdkPortalOutlet]="myPortal"></ng-template>
  `
})
export class AppComponent  {
  myPortal: ComponentPortal<any>;
  onClickAddChild() {
    this.myPortal = new ComponentPortal(MyDynamicComponent);
  }
}

@Component({
  selector: 'app-child',
  template: `<p>I am a child.</p>`
})
export class MyDynamicComponent{
}

See it in action

Step 2. Option B: If you DO need to pass data into and receive events from your dynamic components:

// A bit of boilerplate here. Recommend putting this function in a utils 
// file in order to keep your component code a little cleaner.
function createDomPortalHost(elRef: ElementRef, injector: Injector) {
  return new DomPortalHost(
    elRef.nativeElement,
    injector.get(ComponentFactoryResolver),
    injector.get(ApplicationRef),
    injector
  );
}

@Component({
  selector: 'my-app',
  template: `
    <button (click)="onClickAddChild()">Click to add random child component</button>
    <div #portalHost></div>
  `
})
export class AppComponent {

  portalHost: DomPortalHost;
  @ViewChild('portalHost') elRef: ElementRef;

  constructor(readonly injector: Injector) {
  }

  ngOnInit() {
    this.portalHost = createDomPortalHost(this.elRef, this.injector);
  }

  onClickAddChild() {
    const myPortal = new ComponentPortal(MyDynamicComponent);
    const componentRef = this.portalHost.attach(myPortal);
    setTimeout(() => componentRef.instance.myInput 
      = '> This is data passed from AppComponent <', 1000);
    // ... if we had an output called 'myOutput' in a child component, 
    // this is how we would receive events...
    // this.componentRef.instance.myOutput.subscribe(() => ...);
  }
}

@Component({
  selector: 'app-child',
  template: `<p>I am a child. <strong>{{myInput}}</strong></p>`
})
export class MyDynamicComponent {
  @Input() myInput = '';
}

See it in action

Felsite answered 24/1, 2019 at 14:4 Comment(7)
Dude, You just nailed. This one will get attention. I couldn't believe how damn difficult is to add a simple dynamic component in Angular until I needed to do one. It's like doing reset and go back to pre-JQuery times.Blossom
@Blossom I know right? Why did it take them this long?Felsite
Nice approach, but do you know how to pass parameters to ChildComponent ?Mangle
@Mangle this may answer your question #47470344Felsite
@StephenPaul How does this Portal approach differ from ngTemplateOutlet and ngComponentOutlet? 🤔Enlightenment
I agree this addresses how to do dynamic components with portal but it's less clear to me how this allows the OP to do dynamic templates with those dynamic components. Seems like the template in MyDynamicComponent is compiled by AOT and the dynamic part of this is just the component / portal part. So it seems like half the answer but not all of the answer.Eslinger
How does this Portal approach differ from this actory = this.resolver.resolveComponentFactory(ButtonComponent); this.componentRef = this.VCR.createComponent(factory, this.counter++, this.VCR.injector); ?Marquesan
F
18

I decided to compact everything I learned into one file. There's a lot to take in here especially compared to before RC5. Note that this source file includes the AppModule and AppComponent.

import {
  Component, Input, ReflectiveInjector, ViewContainerRef, Compiler, NgModule, ModuleWithComponentFactories,
  OnInit, ViewChild
} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

@Component({
  selector: 'app-dynamic',
  template: '<h4>Dynamic Components</h4><br>'
})
export class DynamicComponentRenderer implements OnInit {

  factory: ModuleWithComponentFactories<DynamicModule>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler) { }

  ngOnInit() {
    if (!this.factory) {
      const dynamicComponents = {
        sayName1: {comp: SayNameComponent, inputs: {name: 'Andrew Wiles'}},
        sayAge1: {comp: SayAgeComponent, inputs: {age: 30}},
        sayName2: {comp: SayNameComponent, inputs: {name: 'Richard Taylor'}},
        sayAge2: {comp: SayAgeComponent, inputs: {age: 25}}};
      this.compiler.compileModuleAndAllComponentsAsync(DynamicModule)
        .then((moduleWithComponentFactories: ModuleWithComponentFactories<DynamicModule>) => {
          this.factory = moduleWithComponentFactories;
          Object.keys(dynamicComponents).forEach(k => {
            this.add(dynamicComponents[k]);
          })
        });
    }
  }

  addNewName(value: string) {
    this.add({comp: SayNameComponent, inputs: {name: value}})
  }

  addNewAge(value: number) {
    this.add({comp: SayAgeComponent, inputs: {age: value}})
  }

  add(comp: any) {
    const compFactory = this.factory.componentFactories.find(x => x.componentType === comp.comp);
    // If we don't want to hold a reference to the component type, we can also say: const compFactory = this.factory.componentFactories.find(x => x.selector === 'my-component-selector');
    const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
    const cmpRef = this.vcRef.createComponent(compFactory, this.vcRef.length, injector, []);
    Object.keys(comp.inputs).forEach(i => cmpRef.instance[i] = comp.inputs[i]);
  }
}

@Component({
  selector: 'app-age',
  template: '<div>My age is {{age}}!</div>'
})
class SayAgeComponent {
  @Input() public age: number;
};

@Component({
  selector: 'app-name',
  template: '<div>My name is {{name}}!</div>'
})
class SayNameComponent {
  @Input() public name: string;
};

@NgModule({
  imports: [BrowserModule],
  declarations: [SayAgeComponent, SayNameComponent]
})
class DynamicModule {}

@Component({
  selector: 'app-root',
  template: `
        <h3>{{message}}</h3>
        <app-dynamic #ad></app-dynamic>
        <br>
        <input #name type="text" placeholder="name">
        <button (click)="ad.addNewName(name.value)">Add Name</button>
        <br>
        <input #age type="number" placeholder="age">
        <button (click)="ad.addNewAge(age.value)">Add Age</button>
    `,
})
export class AppComponent {
  message = 'this is app component';
  @ViewChild(DynamicComponentRenderer) dcr;

}

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, DynamicComponentRenderer],
  bootstrap: [AppComponent]
})
export class AppModule {}`
Felsite answered 29/9, 2016 at 14:39 Comment(0)
R
10

I have a simple example to show how to do angular 2 rc6 dynamic component.

Say, you have a dynamic html template = template1 and want to dynamic load, firstly wrap into component

@Component({template: template1})
class DynamicComponent {}

here template1 as html, may be contains ng2 component

From rc6, have to have @NgModule wrap this component. @NgModule, just like module in anglarJS 1, it decouple different part of ng2 application, so:

@Component({
  template: template1,

})
class DynamicComponent {

}
@NgModule({
  imports: [BrowserModule,RouterModule],
  declarations: [DynamicComponent]
})
class DynamicModule { }

(Here import RouterModule as in my example there is some route components in my html as you can see later on)

Now you can compile DynamicModule as: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

And we need put above in app.moudule.ts to load it, please see my app.moudle.ts. For more and full details check: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts and app.moudle.ts

and see demo: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview

Rabat answered 6/9, 2016 at 6:6 Comment(2)
So, you've declared module1, module2, module3. And if you would need another "dynamic" template content, you would need to create a defintion (file) form moudle4 (module4.ts), right? If yes, that does not seem to be dynamic. It is static, is not it? Or do I miss something?Beliabelial
In above " template1" is string of html , you can put anything in it and we call this dynamic template, as this question is askingRabat
M
6

In angular 7.x I used angular-elements for this.

  1. Install @angular-elements npm i @angular/elements -s

  2. Create accessory service.

import { Injectable, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { IStringAnyMap } from 'src/app/core/models';
import { AppUserIconComponent } from 'src/app/shared';

const COMPONENTS = {
  'user-icon': AppUserIconComponent
};

@Injectable({
  providedIn: 'root'
})
export class DynamicComponentsService {
  constructor(private injector: Injector) {

  }

  public register(): void {
    Object.entries(COMPONENTS).forEach(([key, component]: [string, any]) => {
      const CustomElement = createCustomElement(component, { injector: this.injector });
      customElements.define(key, CustomElement);
    });
  }

  public create(tagName: string, data: IStringAnyMap = {}): HTMLElement {
    const customEl = document.createElement(tagName);

    Object.entries(data).forEach(([key, value]: [string, any]) => {
      customEl[key] = value;
    });

    return customEl;
  }
}

Note that you custom element tag must be different with angular component selector. in AppUserIconComponent:

...
selector: app-user-icon
...

and in this case custom tag name I used "user-icon".

  1. Then you must call register in AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. And now in any place of your code you can use it like this:
dynamicComponents.create('user-icon', {user:{...}});

or like this:

const html = `<div class="wrapper"><user-icon class="user-icon" user='${JSON.stringify(rec.user)}'></user-icon></div>`;

this.content = this.domSanitizer.bypassSecurityTrustHtml(html);

(in template):

<div class="comment-item d-flex" [innerHTML]="content"></div>

Note that in second case you must pass objects with JSON.stringify and after that parse it again. I can't find better solution.

Midweek answered 29/1, 2019 at 8:23 Comment(2)
Interresting approach, but you will need to target es2015 (so no support for IE11) in your tsconfig.json, othewise it will failed at document.createElement(tagName);Mangle
Hi, as you mentioned a way to handle inputs, so can outputs of child components can be handled like this as well?Moule
T
6

In 2021 there still NO WAY in Angular to create component using dynamic HTML (loading html template dynamically), just to save your time.

Even there are a lot of voted up solutions and accepted solution, but all of them will not work for recent versions in production/AOT at least for now.

Basically because Angular does not allow you to define component with : template: {variable}

As stated by Angular team they are not going to support this approach!! please find this for reference https://github.com/angular/angular/issues/15275

Tried answered 17/8, 2021 at 15:42 Comment(0)
G
5

Following up on Radmin's excellent answer, there is a little tweak needed for everyone who is using angular-cli version 1.0.0-beta.22 and above.

COMPILER_PROVIDERScan no longer be imported (for details see angular-cli GitHub).

So the workaround there is to not use COMPILER_PROVIDERS and JitCompiler in the providers section at all, but use JitCompilerFactory from '@angular/compiler' instead like this inside the type builder class:

private compiler: Compiler = new JitCompilerFactory([{useDebug: false, useJit: true}]).createCompiler();

As you can see, it is not injectable and thus has no dependencies with the DI. This solution should also work for projects not using angular-cli.

Greenburg answered 4/1, 2017 at 9:26 Comment(3)
Thanks for this suggestion, however, I'm running into "No NgModule metadata found for 'DynamicHtmlModule'". My implementation is based on #40060998Hickory
Anyone have working JitCompiletFactory with AOT sample? I have same error as @HickoryHorthy
It does indeed not seem possible. Please see github.com/angular/angular/issues/11780, medium.com/@isaacplmann/… and #42537638Greenburg
P
5

Solved this in Angular 2 Final version simply by using the dynamicComponent directive from ng-dynamic.

Usage:

<div *dynamicComponent="template; context: {text: text};"></div>

Where template is your dynamic template and context can be set to any dynamic datamodel that you want your template to bind to.

Pule answered 26/4, 2017 at 18:32 Comment(2)
At the time of writing Angular 5 with AOT does not support this since the JIT compiler is not included in the bundle. Without AOT it works like a charm :)Pule
does this still apply to angular 7+ ?Wowser
R
4

I want to add a few details on top of this very excellent post by Radim.

I took this solution and worked on it for a bit and quickly ran into some limitations. I'll just outline those and then give the solution to that as well.

  • First of all I was unable to render dynamic-detail inside a dynamic-detail (basically nest dynamic UIs inside each other).
  • The next issue was that I wanted to render a dynamic-detail inside one of the parts that was made available in the solution. That was not possible with the initial solution either.
  • Lastly it was not possible to use template URLs on the dynamic parts like string-editor.

I made another question based on this post, on how to achieve these limitations, which can be found here:

recursive dynamic template compilation in angular2

I’ll just outline the answers to these limitations, should you run into the same issue as I, as that make the solution quite more flexible. It would be awesome to have the initial plunker updated with that as well.

To enable nesting dynamic-detail inside each other, You'll need to add DynamicModule.forRoot() in the import statement in the type.builder.ts

protected createComponentModule (componentType: any) {
    @NgModule({
    imports: [
        PartsModule, 
        DynamicModule.forRoot() //this line here
    ],
    declarations: [
        componentType
    ],
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
}

Besides that it was not possible to use <dynamic-detail> inside one of the parts being string-editor or text-editor.

To enable that you'll need to change parts.module.ts and dynamic.module.ts

Inside parts.module.ts You'll need to add DynamicDetail in the DYNAMIC_DIRECTIVES

export const DYNAMIC_DIRECTIVES = [
   forwardRef(() => StringEditor),
   forwardRef(() => TextEditor),
   DynamicDetail
];

Also in the dynamic.module.ts you'd have to remove the dynamicDetail as they are now part of the parts

@NgModule({
   imports:      [ PartsModule ],
   exports:      [ PartsModule],
})

A working modified plunker can be found here: http://plnkr.co/edit/UYnQHF?p=preview (I didn’t solve this issue, I’m just the messenger :-D)

Finally it was not possible to use templateurls in the parts created on the dynamic components. A solution (or workaround. I’m not sure whether it’s an angular bug or wrong use of the framework) was to create a compiler in the constructor instead of injecting it.

    private _compiler;

    constructor(protected compiler: RuntimeCompiler) {
        const compilerFactory : CompilerFactory =
        platformBrowserDynamic().injector.get(CompilerFactory);
        this._compiler = compilerFactory.createCompiler([]);
    }

Then use the _compiler to compile, then templateUrls are enabled as well.

return new Promise((resolve) => {
        this._compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                let _ = window["_"];
                factory = _.find(moduleWithFactories.componentFactories, { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

Hope this helps someone else!

Best regards Morten

Renaissance answered 5/10, 2016 at 11:26 Comment(0)
S
3

This is the example of dynamic Form controls generated from server.

https://stackblitz.com/edit/angular-t3mmg6

This example is dynamic Form controls is in add component (This is where you can get the Formcontrols from the server). If you see addcomponent method you can see the Forms Controls. In this example I am not using angular material,but It works (I am using @ work). This is target to angular 6, but works in all previous version.

Need to add JITComplierFactory for AngularVersion 5 and above.

Thanks

Vijay

Salpingectomy answered 5/10, 2018 at 12:24 Comment(0)
L
2

I myself am trying to see how can I update RC4 to RC5 and thus I stumbled upon this entry and new approach to dynamic component creation still holds a bit of mystery to me, so I wont suggest anything on component factory resolver.

But, what I can suggest is a bit clearer approach to component creation on this scenario - just use switch in template that would create string editor or text editor according to some condition, like this:

<form [ngSwitch]="useTextarea">
    <string-editor *ngSwitchCase="false" propertyName="'code'" 
                 [entity]="entity"></string-editor>
    <text-editor *ngSwitchCase="true" propertyName="'code'" 
                 [entity]="entity"></text-editor>
</form>

And by the way, "[" in [prop] expression have a meaning, this indicates one way data binding, hence you can and even should omit those in case if you know that you do not need to bind property to variable.

Longitudinal answered 11/8, 2016 at 7:31 Comment(2)
That would be a way to go.. if the switch/case contains few decisions. But imagine that the generated template could be really large... and differ for each entity, differ by security, differ by entity status, by each property type (number, date, reference... editors) ... In such case, solving this in html template with ngSwitch would create a large, very very large html file.Beliabelial
Oh I agree with you. I have this kind of scenario right here, right now as I'm trying to load a major components of application without knowing prior to compilation particular class to be displayed. Although this particular case do not need dynamic component creation.Longitudinal
C
1

If all you need as a way to parse a dynamic string and load components by their selectors, you may also find the ngx-dynamic-hooks library useful. I initially created this as part of a personal project but didn't see anything like it around, so I polished it up a bit and made it public.

Some tidbids:

  • You can load any components into a dynamic string by their selector (or any other pattern of your choice!)
  • Inputs and outputs can be se just like in a normal template
  • Components can be nested without restrictions
  • You can pass live data from the parent component into the dynamically loaded components (and even use it to bind inputs/outputs)
  • You can control which components can load in each outlet and even which inputs/outputs you can give them
  • The library uses Angular's built-in DOMSanitizer to be safe to use even with potentially unsafe input.

Notably, it does not rely on a runtime-compiler like some of the other responses here. Because of that, you can't use template syntax. On the flipside, this means it works in both JiT and AoT-modes as well as both Ivy and the old template engine, as well as being much more secure to use in general.

See it in action in this Stackblitz.

Cornu answered 26/8, 2020 at 6:54 Comment(7)
Thanks, your component did it exactly like I wanted! Just wondering. It's possible to have <tooltip text="Hello"><tooltip> And then read the "text" attribute without binding? It's not a big issue, just wondering. Thanks!Hayrick
It depends on the kind of HookParser responsible for <tooltip>. But if you are using the standard SelectorHookParser, unfortunatelyt not.Cornu
Its important to understand that the library works by completely replacing a hook with its associated component. The only thing the SelectorHookParser takes away from the found hook are inputs and outputs (marked by []-brakets) to give to the component. Anything else is ignored. I'm not sure what your use case is, but if you just need a way to bind "normal" attributes to component elements, you could perhaps first pass them in as inputs and then apply them as attributes from inside the component via HostBindings: https://mcmap.net/q/55616/-what-could-i-have-like-parameter-to-hostbinding-decoratorCornu
Hi Mvin, I created a HookParser and worked perfectly. Congrats, your library is amazing.Hayrick
Thank you! Always nice to hear it being useful to other people.Cornu
I am trying to use your library but somehow it won't parse this in my string <div class="col-xs-12 col-s-6 col-4 col-l-3"><img (click)="openModal()" [src]="${this.gallery[i]}" class="hover-shadow"></div> ... I'm pretty sure this doesn't need a custom HookParser.? @CornuBedspread
Hm, I think there might be a misunderstanding here. My library is only intended to load components into dynamic strings, not directives (bound attributes). It does so by looking for hooks (pieces of text) to replace with those components, and also register the inputs/outputs of those components, if present. That's it. The library does not parse any other Angular template syntax (like the [src] or (click)-directives on normal elements like <img>).Cornu
A
0

Building on top of the answer by Ophir Stern, here is a variant which works with AoT in Angular 4. The only issue I have is I cant inject any services into the DynamicComponent, but I can live with that.

note: I haven't tested with Angular 5.

import { Component, OnInit, Input, NgModule, NgModuleFactory, Compiler, EventEmitter, Output } from '@angular/core';
import { JitCompilerFactory } from '@angular/compiler';

export function createJitCompiler() {
  return new JitCompilerFactory([{
    useDebug: false,
    useJit: true
  }]).createCompiler();
}

type Bindings = {
  [key: string]: any;
};

@Component({
  selector: 'app-compile',
  template: `
    <div *ngIf="dynamicComponent && dynamicModule">
      <ng-container *ngComponentOutlet="dynamicComponent; ngModuleFactory: dynamicModule;">
      </ng-container>
    </div>
  `,
  styleUrls: ['./compile.component.scss'],
  providers: [{provide: Compiler, useFactory: createJitCompiler}]
})
export class CompileComponent implements OnInit {

  public dynamicComponent: any;
  public dynamicModule: NgModuleFactory<any>;

  @Input()
  public bindings: Bindings = {};
  @Input()
  public template: string = '';

  constructor(private compiler: Compiler) { }

  public ngOnInit() {

    try {
      this.loadDynamicContent();
    } catch (err) {
      console.log('Error during template parsing: ', err);
    }

  }

  private loadDynamicContent(): void {

    this.dynamicComponent = this.createNewComponent(this.template, this.bindings);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));

  }

  private createComponentModule(componentType: any): any {

    const runtimeComponentModule = NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })(class RuntimeComponentModule { });

    return runtimeComponentModule;

  }

  private createNewComponent(template: string, bindings: Bindings): any {

    const dynamicComponent = Component({
      selector: 'app-dynamic-component',
      template: template
    })(class DynamicComponent implements OnInit {

      public bindings: Bindings;

      constructor() { }

      public ngOnInit() {
        this.bindings = bindings;
      }

    });

    return dynamicComponent;

  }

}

Hope this helps.

Cheers!

Awake answered 16/2, 2018 at 12:41 Comment(0)
S
0

For this particular case looks like using a directive to dynamically create the component would be a better option. Example:

In the HTML where you want to create the component

<ng-container dynamicComponentDirective [someConfig]="someConfig"></ng-container>

I would approach and design the directive in the following way.

const components: {[type: string]: Type<YourConfig>} = {
    text : TextEditorComponent,
    numeric: NumericComponent,
    string: StringEditorComponent,
    date: DateComponent,
    ........
    .........
};

@Directive({
    selector: '[dynamicComponentDirective]'
})
export class DynamicComponentDirective implements YourConfig, OnChanges, OnInit {
    @Input() yourConfig: Define your config here //;
    component: ComponentRef<YourConfig>;

    constructor(
        private resolver: ComponentFactoryResolver,
        private container: ViewContainerRef
    ) {}

    ngOnChanges() {
        if (this.component) {
            this.component.instance.config = this.config;
            // config is your config, what evermeta data you want to pass to the component created.
        }
    }

    ngOnInit() {
        if (!components[this.config.type]) {
            const supportedTypes = Object.keys(components).join(', ');
            console.error(`Trying to use an unsupported type ${this.config.type} Supported types: ${supportedTypes}`);
        }

        const component = this.resolver.resolveComponentFactory<yourConfig>(components[this.config.type]);
        this.component = this.container.createComponent(component);
        this.component.instance.config = this.config;
    }
}

So in your components text, string, date, whatever - whatever the config you have been passing in the HTML in the ng-container element would be available.

The config, yourConfig, can be the same and define your metadata.

Depending on your config or input type the directive should act accordingly and from the supported types, it would render the appropriate component. If not it will log an error.

Satchel answered 20/5, 2019 at 19:23 Comment(1)
great answer. Have you got this to work? And also will any event binding be in tact once this component arrives in the DOM?Faucher

© 2022 - 2024 — McMap. All rights reserved.