Skip to content

Ivy: Render HTML from String / dynamic rendering #44660

Open

Description

@nfMalde

Which @angular/* package(s) are relevant/releated to the feature request?

compiler, core, platform-browser-dynamic, platform-browser

Description

As far as I know the old aproach was to inject the JITCompiler and use it to render your HTML in an anonymous component.
This worked so far. I use editable HTML in DB which uses key features as substituting variables and directives like *ngIf or *ngSwitch.

Which does not work anymore in production mode (Angular 12.x)
I browsed stackoverflow and other forums to find an workarround for this and these are not working anymore (like loading the JITCompiler into Providers, which results in "JIT Compiler unavailable".

As I found not soulution for ivy/ng 12 i considered this feature as still missing.

Best,

Malte

Proposed solution

Component to render html strings.

Example:

<render-component [html]="..." [render-data]="{ show: true}></render-component>

[html] has the following value:

"<p *ngIf=\"show\">Hello!</p>"

Which display in the case if show is true:

<p>Hello!</p>

Alternatives considered

An Service to render html on the fly.

E.g.:

p3x-compile
Html Compile

Activity

  1. JoostK commented on Jan 7, 2022

    @JoostK
    Member

    It should still be possible to compile templates at runtime using the JIT compiler, there may be something wrong with your config. Please be aware though that using the JIT compiler in production with dynamic inputs is a security concern.

  2. nfMalde commented on Jan 7, 2022

    @nfMalde
    Author

    I also tried to rebuild the components one by one and fix linter errors / etc.

    My configuration:

    "app": {
          "root": "",
          "sourceRoot": "src",
          "projectType": "application",
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:browser",
              "options": {
                "outputPath": "dist",
                "outputHashing": "all",
                "index": "src/index.html",
                "main": "src/main.ts",
                "tsConfig": "src/tsconfig.app.json",
                "progress": true,
                "polyfills": "src/polyfills.ts",
                "namedChunks": true,
                "commonChunk": true,
                "vendorChunk": true,
                "styles": [
                  {
                    "input": "src/scss/app.scss",
                    "inject": true,
                    "bundleName": "app"
                  }
                ],
                "assets": [
                  "src/assets"
                ],
                "scripts": [
                  "node_modules/chart.js/dist/Chart.js",
                  "node_modules/hammerjs/hammer.min.js"
                ],
                "deployUrl": "/",
                "extractLicenses": false,
                "sourceMap": true
              },
              "configurations": {
                "production": {
                  "budgets": [
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "6kb"
                    }
                  ],
                  "outputHashing": "all",
                  "extractLicenses": true,
                  "fileReplacements": [
                    {
                      "replace": "src/environments/environment.ts",
                      "with": "src/environments/environment.prod.ts"
                    }
                  ]
                }
              },
              "defaultConfiguration": ""
            },
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "options": {
                "browserTarget": "app:build",
                "aot": true
              },
              "configurations": {
                "production": {
                  "browserTarget": "app:build:production"
                }
              }
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n",
              "options": {
                "browserTarget": "app:build"
              }
            },
            "test": {
              "builder": "@angular-devkit/build-angular:karma",
              "options": {
                "main": "src/test.ts",
                "karmaConfig": "./karma.conf.js",
                "polyfills": "src/polyfills.ts",
                "tsConfig": "src/tsconfig.spec.json",
                "styles": [
                  "src/styles.css",
                  "node_modules/bootstrap/dist/css/bootstrap.min.css"
                ],
                "assets": [
                  "src/assets"
                ]
              }
            },
            "lint": {
              "builder": "@angular-eslint/builder:lint",
              "tsConfig": [
                "src/tsconfig.app.json",
                "src/tsconfig.spec.json"
              ],
              "options": {
                "lintFilePatterns": [
                  "src/app/**/*.ts"
                ],
                "eslintConfig": ".eslintrc.js"
              }
            }
          }
        },
    

    App Module:

    import {Compiler, COMPILER_OPTIONS, CompilerFactory, NgModule} from '@angular/core';
    import {JitCompilerFactory} from '@angular/platform-browser-dynamic';
    
    export function createCompiler(compilerFactory: CompilerFactory) {
        return compilerFactory.createCompiler();
    }
    
    @NgModule({
        ...
        providers: [
            { provide: COMPILER_OPTIONS, useValue: {}, multi: true },
            { provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] },
            { provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory] }
            ...
        ]
        ...
    })

    My render component:
    (Based on: HtmlCompiler)

    import { CommonModule } from '@angular/common';
    import { Component, OnInit, ChangeDetectionStrategy, Compiler, ViewContainerRef, NgModule, SimpleChanges, Input, Inject } from '@angular/core';
    
    @Component({
        selector: 'app-render',
        template: ''
    })
    export class RenderComponent implements OnInit {
    
        @Input()
        public template = '';
        @Input()
        public componentClass: object = {};
        @Input()
        public imports = [];
        @Input()
        public module: NgModule = {};
    
    
        constructor(private compiler: Compiler, private container: ViewContainerRef) { }
    
        ngOnInit(): void {
        }
    
        ngOnChanges(changes: SimpleChanges) {
            let currentTemplate = this.template;
            if (changes.template) {
                currentTemplate = changes.template.currentValue;
            }
            let currentComponentClass = this.componentClass;
            if (changes.componentClass) {
                currentComponentClass = changes.componentClass.currentValue;
            }
            let currentImports = [CommonModule];
            if (!!changes.imports && !!changes.imports.currentValue) {
                currentImports = [...currentImports, ...changes.imports.currentValue];
            }
            this.addComponent(currentTemplate, currentComponentClass, currentImports);
        }
    
        private addComponent(template: string, properties: any = {}, imports: any[]) {
            this.container.clear();
    
            const TemplateComponent = Component({template})(class {});
    
            const TemplateModule = NgModule({
                declarations: [TemplateComponent, ...(this.module.declarations ?? [])],
                entryComponents: [TemplateComponent],
                providers: [...(this.module.providers ?? [])],
                imports: [...imports, ...(this.module.imports ?? [])]})(class {});
    
            const module = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
            const factory = module.componentFactories.find((comp) =>
                comp.componentType === TemplateComponent
            );
            const component = this.container.createComponent(factory);
            Object.assign(component.instance, properties);
            if (component.instance.ngOnInit) {
                setTimeout(() => {
                    component.instance.ngOnInit();
                }, 10);
            }
        }
    
    }

    My usage:

    <app-render [template]="myHtml" [module]="[]"></app-render>

    Result in dev mode:

    <p>Hello!</p>

    Result in prod mode:

    core.js:6498 ERROR Error: JIT compiler unavailable
        at fs (core.js:4077:15)
        at Function.get (core.js:26941:31)
        at rr (core.js:1129:25)
        at new yc (core.js:25371:13)
        at bC (core.js:28253:12)
        at o.eS [as compileModuleAndAllComponentsSync] (core.js:28263:29)
        at n.addComponent (render.component.ts:52:38)
        at ngOnChanges (render.component.ts:38:14)
        at n.rl (core.js:1510:14)
        at qa (core.js:2548:18)
    

    Which points directly to this function in angular->core.js:

    function getCompilerFacade(request) {
        const globalNg = _global['ng'];
        if (globalNg && globalNg.ɵcompilerFacade) {
            return globalNg.ɵcompilerFacade;
        }
        if (typeof ngDevMode === 'undefined' || ngDevMode) {
            // Log the type as an error so that a developer can easily navigate to the type from the
            // console.
            console.error(`JIT compilation failed for ${request.kind}`, request.type);
            let message = `The ${request.kind} '${request
                .type.name}' needs to be compiled using the JIT compiler, but '@angular/compiler' is not available.\n\n`;
            if (request.usage === 1 /* PartialDeclaration */) {
                message += `The ${request.kind} is part of a library that has been partially compiled.\n`;
                message +=
                    `However, the Angular Linker has not processed the library such that JIT compilation is used as fallback.\n`;
                message += '\n';
                message +=
                    `Ideally, the library is processed using the Angular Linker to become fully AOT compiled.\n`;
            }
            else {
                message +=
                    `JIT compilation is discouraged for production use-cases! Consider using AOT mode instead.\n`;
            }
            message +=
                `Alternatively, the JIT compiler should be loaded by bootstrapping using '@angular/platform-browser-dynamic' or '@angular/platform-server',\n`;
            message +=
                `or manually provide the compiler with 'import "@angular/compiler";' before bootstrapping.`;
            throw new Error(message);
        }
        else {
            throw new Error('JIT compiler unavailable');
        }
    }

    I see no errors in my configuration so far.

  3. JoostK commented on Jan 7, 2022

    @JoostK
    Member

    Did you update using ng update? In v12 some builder defaults were changed to support production builds by default, which means that things like optimization now defaults to true (together with some more changes). Your configuration does not reflect these changes, and the stacktrace confirms that it's a production build. Production builds with full optimizations are not compatible with JIT mode, as important information has been dropped by the optimizer. You can rerun migrations using

    ng update @angular/cli --migrate-only --from 11 --to 12
    

    You shouldn't need any of the

        providers: [
            { provide: COMPILER_OPTIONS, useValue: {}, multi: true },
            { provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] },
            { provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory] }
            ...
        ]
  4. nfMalde commented on Jan 8, 2022

    @nfMalde
    Author

    Thanks for pointing that out. Am I right with turning off the optimization the bundles are not minified anymore?
    Main bug in my package.json was that somhow (I started the project with angular 5) the devkit builder was installed with version 0.5.x. Something must have beend wrong with my ng update calls...cant tell what but for now this is fixed.

    I dont know if this is even a real version.
    I turned off optimization and the rendering works, but as I pointed out the files are now full size and not minfied. Is there any other solution?

  5. JoostK commented on Jan 8, 2022

    @JoostK
    Member

    The CLI does not support optimizations together with JIT interop: #37216. It's likely possible to post-process the bundled files in order to just apply minification, e.g. using esbuild or babel.

  6. added this to the needsTriage milestone on Jan 9, 2022
  7. nfMalde commented on Jan 10, 2022

    @nfMalde
    Author

    Thanks! I now added terser-folder as npm script to minify it. I know that using dynamic rendering just because of route / click actions in the returned html of the server is not the best soulution and Im aware of the security risks of this. But so far I got no other idea since I have localized content from db like "To undo deletion click " which calls an function in the component.
    Basically im only using this technique so far to use router links and click actions. Ill think of another solution in my next version but for now its working :-) Thanks!

  8. added
    featureLabel used to distinguish feature request from other issues
    on Jan 13, 2022
  9. modified the milestones: needsTriage, Backlog on Jan 13, 2022
  10. angular-robot commented on Jan 14, 2022

    @angular-robot
    Contributor

    This feature request is now candidate for our backlog! In the next phase, the community has 60 days to upvote. If the request receives more than 20 upvotes, we'll move it to our consideration list.

    You can find more details about the feature request process in our documentation.

  11. hydra1983 commented on Jan 19, 2022

    @hydra1983

    @nfMalde For v13, a workaround with minimum cost (bundle size) would be:

    main.ts

    import 'reflect-metadata'; // !! SHOULD BE KEPT IN FIRST LINE !!
    import '@angular/compiler'; // !! TO FIX `Jit compiler unavailable` !!
    
    import { enableProdMode } from '@angular/core';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    import { AppModule } from '@app/app.module';
    import { environment } from '@env/environment';
    
    if (environment.production) {
      enableProdMode();
    }
    
    document.addEventListener('DOMContentLoaded', () => {
      platformBrowserDynamic()
        .bootstrapModule(AppModule)
        .catch((err) => console.error(err));
    });

    angular.json

                  "production": {
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "2mb",
                      "maximumError": "5mb"
                    },
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "6kb",
                      "maximumError": "10kb"
                    }
                  ],
                  "fileReplacements": [
                    {
                      "replace": "src/environments/environment.ts",
                      "with": "src/environments/environment.prod.ts"
                    }
                  ],
                  "outputHashing": "all",
                  // Default is true, use false to fix jit compiler unavailable
                  "buildOptimizer": false
                },
  12. hydra1983 commented on Feb 8, 2022

    @hydra1983

    For angular v13 without injecting 'Compiler'

    adhoc.type.ts

    @Component({
      selector: '[dg-adhoc-html]',
      template: ``,
      changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class DgAdhocComponent implements OnChanges, OnInit, OnDestroy {
      constructor(private vcr: ViewContainerRef) {
      }
    
      @Input('dg-adhoc-html')
      html: string;
    
      @Input('dg-adhoc-styles')
      styles: string[];
    
      @Input('dg-adhoc-context')
      context: LcdpSafeAny;
    
      @Input('dg-adhoc-module')
      module: NgModule;
    
      @Input('dg-adhoc-imports')
      imports: Array<Type<LcdpSafeAny> | ModuleWithProviders<LcdpSafeAny> | LcdpSafeAny[]>;
    
      @Input('dg-adhoc-error-handler')
      errorHandler: (...args: LcdpSafeAny[]) => LcdpSafeAny = undefined;
    
      visibleEmitter = new Subject<boolean>();
    
      get isComponentVisible(): boolean {
        return typeof this.html === 'string' && this.html.trim() !== '';
      }
    
      private _componentRef: ComponentRef<LcdpSafeAny> | null = null;
      private _moduleRef: NgModuleRef<LcdpSafeAny> | null = null;
      private visible = false;
    
      ngOnChanges(changes: SimpleChanges) {
        try {
          this.update();
        } catch (e) {
          if (_.isNil(this.errorHandler)) {
            throw e;
          } else {
            this.errorHandler(e);
          }
        }
      }
    
      // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method
      ngOnInit(): void {
        // nothing
      }
    
      ngOnDestroy(): void {
        if (this._componentRef) {
          this._componentRef.destroy();
        }
    
        if (this._moduleRef) {
          this._moduleRef.destroy();
        }
      }
    
      update() {
        this.vcr.clear();
        this._componentRef = null;
    
        if (this._moduleRef) {
          this._moduleRef.destroy();
          this._moduleRef = null;
        }
    
        if (_.isNil(this.html) || this.html.trim() === '') {
          return;
        }
    
        const componentType = this.createComponentType(
          this.html,
          this.styles,
          this.context,
        );
        const moduleType = this.createModuleType(componentType);
        this._moduleRef = createNgModuleRef(moduleType, this.vcr.injector);
        this._componentRef = this.vcr.createComponent(componentType);
      }
    
      toggle() {
        // console.log(this.vcr.injector.get(AsyncPipe));
        this.visible = !this.visible;
        this.visibleEmitter.next(this.visible);
      }
    
      private createModuleType(
        componentType: Type<LcdpSafeAny>,
      ): Type<LcdpSafeAny> {
        let metadata: NgModule = {};
    
        if (!_.isNil(this.module)) {
          metadata = _.cloneDeep(this.module);
        }
    
        metadata.imports = metadata.imports || [];
        metadata.imports = metadata.imports.concat(this.imports || []);
    
        metadata.declarations = metadata.declarations || [];
        metadata.declarations = metadata.declarations.concat([componentType]);
    
        const moduleType = class AdhocModule {
          // nothing
        };
        const moduleTypeDecorator: TypeDecorator = NgModule(metadata);
        // noinspection UnnecessaryLocalVariableJS
        const decoratedModuleType = moduleTypeDecorator(moduleType);
        return decoratedModuleType;
      }
    
      // noinspection JSMethodCanBeStatic
      private createComponentType(
        html: string,
        styles: string[],
        context: LcdpSafeAny,
      ): Type<LcdpSafeAny> {
        const metadata: Component = {};
        metadata.selector = RandomUtil.nextSelector();
        metadata.template = html;
        metadata.styles = [...styles];
        metadata.changeDetection = ChangeDetectionStrategy.OnPush;
    
        const componentType = class AdhocComponent {
          context: LcdpSafeAny = context;
        };
        const componentTypeCreator: TypeDecorator = Component(metadata);
        // noinspection UnnecessaryLocalVariableJS
        const decoratedComponentType = componentTypeCreator(componentType);
        return decoratedComponentType;
      }
    }

    tsconfig.json

    ...
      "angularCompilerOptions": {
        ...
        "compilationMode": "partial"
      }
  13. 22 remaining items

  14. PH-Lars commented on Mar 14, 2023

    @PH-Lars

    BTW, you need also these options in angular.json

    "buildOptimizer": false,
    "optimization": false,
    

    under { ptojects: { <your_project_name>: { architect: { build: { options: {

    This does work indeed but for our project at the moment the bundle size, for example of main, goes from 300 kb to over 10MB. Good to know this is an option though, thanks for that.

    For now Il wait if this ever becomes availible for build optimized.

  15. PH-Lars commented on Mar 15, 2023

    @PH-Lars

    Okay so I managed to find a way to make this work for now in our optimal scenario.

    All buildoptimizers enabled etc, ivy compiling full.

    So for me, I don't need this to work on every page or component. I made a stand-alone component which renders my html on pages where i need it.

    I used the code above to create a anonymous module type, component type etc.

    Then I added

    import {ɵglobal} from '@angular/core';

    on top of the stand-alone component imports.

    Then we gonne lazy load the compiler and publish the facade and after that we (lazy)loaded the jitcompiler to be used.

        // Lazy load that bad boy
       import('@angular/compiler').then((compiler) => {
          compiler.publishFacade(ɵglobal); // Publish to global so its visible
    
          // Example code.
          const componentType = this.createComponentType(
            this.html!,
            this.styles ?? [],
            this.context,
          );
          const moduleType = this.createModuleType(componentType);
          this._moduleRef = createNgModule(moduleType, this._viewContainerRef.injector);
          this._componentRef = this._viewContainerRef.createComponent(componentType);
        });
    

    So one might, with this knowledge, create for example a service that just lazyloads the JIT compiler within the start up or in whatever way u want.

    Now ur default app is Ivy with build optimized and u can get JIT without the initial bloat.

    async confirmJitLoaded() {
      if(!DynamicContentDirective.isJitLoaded) {
        await import('@angular/compiler').then((compiler) => {
          compiler.publishFacade(ɵglobal);
          DynamicContentDirective.isJitLoaded = true;
        });
      }
    }
    
  16. marvinerkes commented on Apr 25, 2023

    @marvinerkes

    I am attempting to dynamically render content containing a component from an external library, but the tag remains empty. Does somebody have a clue on this?
    Bildschirm­foto 2023-04-25 um 13 12 15

  17. jamend commented on May 11, 2023

    @jamend

    Okay so I managed to find a way to make this work for now in our optimal scenario.

    All buildoptimizers enabled etc, ivy compiling full.

    So for me, I don't need this to work on every page or component. I made a stand-alone component which renders my html on pages where i need it.

    I used the code above to create a anonymous module type, component type etc.

    Then I added

    import {ɵglobal} from '@angular/core';

    on top of the stand-alone component imports.

    Then we gonne lazy load the compiler and publish the facade and after that we (lazy)loaded the jitcompiler to be used.

        // Lazy load that bad boy
       import('@angular/compiler').then((compiler) => {
          compiler.publishFacade(ɵglobal); // Publish to global so its visible
    
          // Example code.
          const componentType = this.createComponentType(
            this.html!,
            this.styles ?? [],
            this.context,
          );
          const moduleType = this.createModuleType(componentType);
          this._moduleRef = createNgModule(moduleType, this._viewContainerRef.injector);
          this._componentRef = this._viewContainerRef.createComponent(componentType);
        });
    

    So one might, with this knowledge, create for example a service that just lazyloads the JIT compiler within the start up or in whatever way u want.

    Now ur default app is Ivy with build optimized and u can get JIT without the initial bloat.

    async confirmJitLoaded() {
      if(!DynamicContentDirective.isJitLoaded) {
        await import('@angular/compiler').then((compiler) => {
          compiler.publishFacade(ɵglobal);
          DynamicContentDirective.isJitLoaded = true;
        });
      }
    }
    

    This seems to have the same issue as with loading JitCompilerFactory with aot off. The runtime module's module imports (ex. CommonModule) don't seem to work (#47160, #48034, angular/angular-cli#22570) so ngFor etc. are not available.

  18. Strazz1337 commented on Jul 9, 2023

    @Strazz1337

    Okay so I managed to find a way to make this work for now in our optimal scenario.
    All buildoptimizers enabled etc, ivy compiling full.
    So for me, I don't need this to work on every page or component. I made a stand-alone component which renders my html on pages where i need it.
    I used the code above to create a anonymous module type, component type etc.
    Then I added
    import {ɵglobal} from '@angular/core';
    on top of the stand-alone component imports.
    Then we gonne lazy load the compiler and publish the facade and after that we (lazy)loaded the jitcompiler to be used.

        // Lazy load that bad boy
       import('@angular/compiler').then((compiler) => {
          compiler.publishFacade(ɵglobal); // Publish to global so its visible
    
          // Example code.
          const componentType = this.createComponentType(
            this.html!,
            this.styles ?? [],
            this.context,
          );
          const moduleType = this.createModuleType(componentType);
          this._moduleRef = createNgModule(moduleType, this._viewContainerRef.injector);
          this._componentRef = this._viewContainerRef.createComponent(componentType);
        });
    

    So one might, with this knowledge, create for example a service that just lazyloads the JIT compiler within the start up or in whatever way u want.
    Now ur default app is Ivy with build optimized and u can get JIT without the initial bloat.

    async confirmJitLoaded() {
      if(!DynamicContentDirective.isJitLoaded) {
        await import('@angular/compiler').then((compiler) => {
          compiler.publishFacade(ɵglobal);
          DynamicContentDirective.isJitLoaded = true;
        });
      }
    }
    

    This seems to have the same issue as with loading JitCompilerFactory with aot off. The runtime module's module imports (ex. CommonModule) don't seem to work (#47160, #48034, angular/angular-cli#22570) so ngFor etc. are not available.

    I have no issues when using the standalone imports from @angular/common.

  19. hiepxanh commented on Aug 5, 2023

    @hiepxanh
    Contributor

    with this issue #51157
    and https://github.com/MTobisch/ngx-dynamic-hooks
    we can achive this in SSR Angular Universal
    ref here too: Angular-Dynamic-Hooks/ngx-dynamic-hooks#34

  20. ng-sp7 commented on Oct 26, 2023

    @ng-sp7

    +1

  21. ildarnabiev commented on Dec 22, 2023

    @ildarnabiev

    +1

  22. NoelDeMartin commented on Mar 4, 2024

    @NoelDeMartin

    We have been working on this in our app, and I want to share the workaround we've come up with.

    TLDR, you need to apply the following patch to @angular/compiler (we do it using patch-package):

    diff --git a/node_modules/@angular/compiler/fesm2022/compiler.mjs b/node_modules/@angular/compiler/fesm2022/compiler.mjs
    index 5831cd3..6860059 100755
    --- a/node_modules/@angular/compiler/fesm2022/compiler.mjs
    +++ b/node_modules/@angular/compiler/fesm2022/compiler.mjs
    @@ -6072,13 +6072,19 @@ var R3NgModuleMetadataKind;
         R3NgModuleMetadataKind[R3NgModuleMetadataKind["Global"] = 0] = "Global";
         R3NgModuleMetadataKind[R3NgModuleMetadataKind["Local"] = 1] = "Local";
     })(R3NgModuleMetadataKind || (R3NgModuleMetadataKind = {}));
    +const jitModules = ['CommonModule','FormsModule','ReactiveFormsModule','ɵInternalFormsSharedModule','RadioControlRegistryModule'];
     /**
      * Construct an `R3NgModuleDef` for the given `R3NgModuleMetadata`.
      */
     function compileNgModule(meta) {
         const statements = [];
         const definitionMap = new DefinitionMap();
    +    const moduleName = meta.type.value?.node.loc?.identifierName;
         definitionMap.set('type', meta.type.value);
    +    if (moduleName && jitModules.includes(moduleName)) {
    +        // Force JIT compilation.
    +        meta.selectorScopeMode = R3SelectorScopeMode.Inline;
    +    }
         // Assign bootstrap definition. In local compilation mode (i.e., for
         // `R3NgModuleMetadataKind.LOCAL`) we assign the bootstrap field using the runtime
         // `ɵɵsetNgModuleScope`.

    I realize how bad this looks. Patching the compiled code of @angular/compiler, really o.O? Hopefully someone who knows more will let us know if we're doing something really dumb. But after spending some time working on this problem, this is the best solution I could come up with.

    I am not very familiar with the way Angular compilation works, so I had to do some source-diving and look at the compiled code in different scenarios. As far as I could gather, one of the things that AOT does is remove the imports from core modules (such as CommonModule) and add them directly to components. That's why importing CommonModule in your dynamic components doesn't seem to work, but importing the individual directives or app modules does.

    For example, if you have MyButtonComponent declared in a module that imports CommonModule, that's how you're able to use directives like *ngIf in its template. But using AOT, the imports array of CommonModule is left empty, and the NgIf directive is added directly to the dependencies array of MyButtonComponent.

    I assume this happens to improve runtime performance. When rendering component templates, only the directives that are actually used will be loaded. However, the actual code of the directives is still within the JavaScript bundle, it's not tree-shaken. That's why just by patching that line to include the imports, everything seems to work. You can see some of this logic documented in the R3SelectorScopeMode enum I'm using in the patch.

    Knowing that, I'm aware that this workaround is not perfect, because it probably degrades de performance of non-dynamic components. But we've compared disabling AOT altogether vs this approach, and the performance difference is noticeable (this is better than disabling AOT).

    Anyways, I just wanted to share what we've done in case it's useful to someone else. Or if someone who knows more about Angular internals can confirm or deny whether this is a good idea.

    If you want to use this patch, do it at your own risk. But keep in mind that you may also need to add other external modules in your application. For example, we use Ionic and we included IonicModule in the jitModules array of the patch as well.

  23. vojtechmarek-dev commented on Mar 27, 2024

    @vojtechmarek-dev

    Interesting. I have seen a lot of approaches but this is the first to tackle it on compiler level. I am not sure about patching angular's own complier and would be interested what others think about this approach.

    We have been trying to get a hybrid AOT+JIT (dynamic rendering on demand) trying to work for quite some time, since our app depends on this feature.

    We are in a process of upgrading from Angular 12 where such thing was possible, but alas on higher versions the dynamic rendering while having AOT enabled seems to be purpousefuly disabled and discouraged (according to this comment .

    I can understand the reasoning behind this. However I still think there should be a way to have hybrid aot+jit app running.

  24. alxhub commented on Mar 29, 2024

    @alxhub
    Member

    @NoelDeMartin you shouldn't need to do any fancy compiler patching. For the interop to work, ngJitMode needs to be defined to true when bundling the application. This is the part that the CLI doesn't support today.

  25. NoelDeMartin commented on Apr 2, 2024

    @NoelDeMartin

    Thanks @alxhub, can you please provide some example of how I can set ngJitMode to true if the CLI doesn't support it?

    I tried patching the following line to use true instead and that doesn't seem to work:

    It's not that I want to patch dependencies, of course. I'd much rather set some configuration options somewhere. Or even modify our application's code to support this. But I don't know much about the Angular codebase, and with my limited knowledge the only way I've managed to get it working is patching the compiler (I explained my rationale in #44660 (comment)). None of the other solutions in this thread seem to work.

  26. added a commit that references this issue on Apr 2, 2024
  27. JeanMeche commented on Jul 14, 2025

    @JeanMeche
    Member

    Because I was asked elsewhere for an example, here is one : https://stackblitz.com/edit/stackblitz-starters-2bgkkszp
    This example is an AOT app that imports the compiler to allow JIT compilation for dynamically created components.

    With the caveat that JIT compilation isn't recommended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: compilerIssues related to `ngc`, Angular's template compilerarea: coreIssues related to the framework runtimecompiler: jitfeatureLabel used to distinguish feature request from other issuesfeature: under considerationFeature request for which voting has completed and the request is now under consideration

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions