[Fixed] What is the recommended way to dynamically compile component in Angular 11?

Issue

In our app we have the following directive which is used in order to display dynamic Angular components:

import {Compiler, Component, Directive, Input, ModuleWithComponentFactories, NgModule, OnDestroy, ViewContainerRef} from '@angular/core';
import {Render} from './render';

@Directive({
  selector: '[appRender]',
  exportAs: 'appRender'
})
export class RenderDirective implements OnDestroy {

  @Input()
  public set model(model: Render) {
    this.compile(model);
  }

  constructor(private readonly viewContainerRef: ViewContainerRef,
              private readonly compiler: Compiler) {
  }

  private compile(model: Render) {
    @Component({template: model.template})
    class TemplateComponent {
    }

    @NgModule({
      imports: model.imports,
      declarations: [TemplateComponent]
    })
    class TemplateModule {
    }

    this.compiler.compileModuleAndAllComponentsAsync(TemplateModule).then((factories: ModuleWithComponentFactories<any>) => {
      const factory = factories.componentFactories.find(component => component.componentType === TemplateComponent);
      const componentRef = this.viewContainerRef.createComponent(factory);
      Object.assign(componentRef.instance, model.instance);
      componentRef.hostView.detectChanges();
    });
  }

  ngOnDestroy(): void {
    this.viewContainerRef.clear();
  }
}

We are in the middle of Angular update from 8.2 to 11. After the update we are facing the following error:

ERROR Error: Angular JIT compilation failed: '@angular/compiler' not loaded!
  - JIT compilation is discouraged for production use-cases! Consider AOT mode instead.
  - Did you bootstrap using '@angular/platform-browser-dynamic' or '@angular/platform-server'?
  - Alternatively provide the compiler with 'import "@angular/compiler";' before bootstrapping.
    at getCompilerFacade (core.js:4086)
    at Function.get (core.js:26924)
    at getNgModuleDef (core.js:1139)
    at new NgModuleFactory$1 (core.js:25317)
    at Compiler_compileModuleSync__POST_R3__ (core.js:28165)
    at Compiler_compileModuleAndAllComponentsSync__POST_R3__ (core.js:28175)
    at Compiler_compileModuleAndAllComponentsAsync__POST_R3__ [as compileModuleAndAllComponentsAsync] (core.js:28188)
    at RenderDirective.compile (common.js:4560)
    at RenderDirective.set model [as model] (common.js:4541)
    at setInputsForProperty (core.js:10961)

I believe this is related to the IVY compiler. The question is what is the recommended way to achieve same result in Angular 11?

Solution

I managed to fix that issue by:

  1. Providing JitCompilerFactory in RenderModule
@NgModule({
  declarations: [RenderDirective],
  exports: [RenderDirective],
  providers: [{provide: COMPILER_OPTIONS, useValue: {useJit: true}, multi: true},
    {provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS]},
    {provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory]}]
})
export class RenderModule {
}
  1. Replacing annotated component and module with defined by function:
// BEFORE
  private compile(model: Render) {
    @Component({template: model.template})
    class TemplateComponent {
    }

    @NgModule({
      imports: model.imports,
      declarations: [TemplateComponent]
    })
    class TemplateModule {
    }

    this.compiler.compileModuleAndAllComponentsAsync(TemplateModule).then((factories: ModuleWithComponentFactories<any>) => {
      const factory = factories.componentFactories.find(component => component.componentType === TemplateComponent);
      const componentRef = this.viewContainerRef.createComponent(factory);
      Object.assign(componentRef.instance, model.instance);
      componentRef.hostView.detectChanges();
    });
  }
// AFTER
  private compile(model: Render) {
    const templateComponent = Component({template: model.template})(class {});
    const templateModule = NgModule({
        imports: model.imports,
        declarations: [templateComponent]
    })(class {});

    this.compiler.compileModuleAndAllComponentsAsync(templateModule).then((factories: ModuleWithComponentFactories<any>) => {
      const factory = factories.componentFactories.find(component => component.componentType === templateComponent);
      const componentRef = this.viewContainerRef.createComponent(factory);
      Object.assign(componentRef.instance, model.instance);
      componentRef.hostView.detectChanges();
    }).catch(err => console.error(err));
  }

Leave a Reply

(*) Required, Your email will not be published