Search code examples
angulartypescriptangular-dynamic-components

Create dynamic component (ComponentFactoryResolver) with dynamic template


I want to dynamically create a component with a dynamic template, so that interpolations of this template will resolve in the context of the dynamic component.

I know that I can use this code to create a dynamic component (which must be mentioned in entryComponents on the module):

My static component:

@Component({
  selector: 'html-type',
  template: `<ng-template #htmlcontrolcomponent></ng-template>`
})
export class HtmlTypeComponent implements AfterViewInit{

  @ViewChild('htmlcontrolcomponent', { read: ViewContainerRef }) entry: ViewContainerRef;
  constructor(private resolver: ComponentFactoryResolver) {
    super();
   }

  ngAfterViewInit() {
    this.createComponent("<div>{{contextvar}}</div>");
  }

  createComponent(template) {
    this.entry.clear();
    const factory = this.resolver.resolveComponentFactory(HtmlControlComponent);
    const componentRef = this.entry.createComponent(factory);
    componentRef.instance.template = template;       // this doesn't work, is there a way to do it?
  }

Component that should be dynamically added:

import { Component} from '@angular/core';

@Component({
  selector: 'html-control',
  template: '',
})
export class HtmlControlComponent {
   contextvar: string = "This is my current context";
}

Is there a way to reassign the template of a dynamically created component?

What I wanted to achieve: The template of the dynamic component should be dynamic (entered by user and sanitized)


Solution

  • I did it ... with a different approach

    I used a DynamicComponentService

    Important: I had to turn off "aot: false" in angular.json, otherwise I got Runtime compiler is not loaded errors.

    import {
      Compiler,
      Component,
      ComponentFactory,
      Injectable,
      NgModule,
      Type,
      ViewContainerRef,
      ViewEncapsulation
    } from "@angular/core";
    import {CommonModule} from "@angular/common";
    
    @Injectable({
      providedIn: "root"
    })
    export class DynamicComponentService {
    
      protected cacheOfFactories: {[key: string]: ComponentFactory<any>};
      protected componentCache: {[key: string]: Type<any>};
      protected moduleCache: {[key: string]: Type<any>};
    
      constructor(protected compiler: Compiler) {
        this.cacheOfFactories = {};
        this.componentCache = {};
        this.moduleCache = {};
      }
    
      /**
       *
       * @param viewContainerRef
       * @param selector
       * @param template
       */
      createComponentFactory(viewContainerRef: ViewContainerRef, selector: string, template: string) {
        const componentFound = this.componentCache[selector];
        if(componentFound) {
          this.compiler.clearCacheFor(componentFound);
          delete this.componentCache[selector];
        }
        const moduleFound = this.moduleCache[selector];
        if(moduleFound) {
          this.compiler.clearCacheFor(moduleFound);
          delete this.moduleCache[selector];
        }
    
        viewContainerRef.clear();
    
        this.componentCache[selector] = Component({
          selector,
          template,
          encapsulation: ViewEncapsulation.None
        })(class {
        });
    
        this.moduleCache[selector] = NgModule({
          imports: [CommonModule],
          declarations: [this.componentCache[selector]]
        })(class {
        });
    
        return this.compiler.compileModuleAndAllComponentsAsync(this.moduleCache[selector])
          .then((factories) => {
            const foundFactory = factories.componentFactories.find((factory) => {
              return factory.selector === selector;
            });
    
            if(foundFactory) {
              return viewContainerRef.createComponent(foundFactory);
            }
    
            throw new Error("component not found");
          })
          .catch((error) => {
            console.log("error", error);
    
            this.compiler.clearCacheFor(componentFound);
            delete this.componentCache[selector];
            this.compiler.clearCacheFor(moduleFound);
            delete this.moduleCache[selector];
    
            return Promise.reject(error);
          });
      }
    
    }
    

    and changed my html-type component to:

    export class HtmlTypeComponent implements DoCheck {
    
      @ViewChild('htmlcontrolcomponent', { read: ViewContainerRef }) entry: ViewContainerRef;
    
      protected oldTemplate: string = "";
      protected componentRef?: ComponentRef<any>;
    
      constructor(private dynamicComponentService: DynamicComponentService) {}
    
       ngDoCheck() {
        if(this.entry && this.oldTemplate !== this.to.template) {
          this.oldTemplate = this.to.template;
    
          if(this.componentRef) {
            this.componentRef.destroy();
            delete this.componentRef;
          }
    
          this.entry.clear();
    
          this.dynamicComponentService.createComponentFactory(this.entry, 'html-content', this.to.template)
            .then((component) => {
              this.componentRef = component;
              this.componentRef.instance.model = this.model;
            });
        }
      }
    
    }
    

    I could even get rid of the HtmlControlComponent