0

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?

sepp2k
  • 363,768
  • 54
  • 674
  • 675
Wojciech
  • 9
  • 3
  • I think Ivy, by default, uses AOT. And when AOT is used, the compiler have no default provider (since it would be dead weight anyway). If you want to compile templates at runtime, you'll have to either disable AOT or explicitly provide Compiler in the providers of your module that wants to use the compiler. – TotallyNewb Apr 15 '21 at 13:34

2 Answers2

0

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));
  }
Wojciech
  • 9
  • 3
0

This works but can not inject into TemplateComponent.

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

     constructor(private service: SomeService) { }

 }

core.js:6210 ERROR Error: Uncaught (in promise): Error: This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid. This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.

see my question here in detail: Angular 11 DI for dynamically compiled components

Revivius
  • 1
  • 4