2

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)

2 Answers 2

3

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

Sign up to request clarification or add additional context in comments.

Comments

0

You could try to use <ng-content> with projectableNodes parameter of the createComponent() function. Try the following

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

@Component({
  selector: 'html-control',
  template: `<ng-content></ng-content>`,      // <-- add `<ng-content>` here
})
export class HtmlControlComponent {
   contextvar: string = "This is my current context";
}

static component

createComponent(template) {
  this.entry.clear();
  const factory = this.resolver.resolveComponentFactory(HtmlControlComponent);
  const componentRef = this.entry.createComponent(factory, 0, undefined, [[template]]);    // <-- add `template` here
}

Here [[template]] argument is sent to projectableNodes parameter of type any[][]


Untested code. Might need to iron out some kinks.

3 Comments

Thank you for the answer ... but the content objects of projectableNodes obviously must be nodes of some kind rather than a string. I get the error "Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'"
And if I create a node out of my dynamic html template, it works, but it doesn't resolve included interpolations, it just shows them as plain text
@devnull69 I know it's passed so much time, but maybe you've got the right solution for that issue with interpolations? The marked answer is a bad solution, because turning the AOT off is a shitty thing)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.