Dynamically loaded Angular 10 component cannot access CommonModule

Viewed 809

I'm currently working on an Angular application. In one of my methods, I dynamically create a component, however I am unable to use ngClass, ngIf and other such directives from the CommonModule in the component.

Below is an example of the error:

Error when I use ngIf or ngClass inside the dynamically loaded logo component

WHAT I DID ALREADY:

  • I've imported commonModule in my project app.module.ts
  • I've imported commonModule in my display component and it works as I'm able to use ngIf and ngClass in every other component without any problem
  • Also I'm able to import any component without errors as long as I'm not using any directive from the CommonModule in my html
  • I've tried importing an instance of the NgModule through component factory createComponent function as shown in the angular documentation:

Quick view of angular documentation on component factory createcomponent

I have spent hours on this and believe it's related to my use of the createComponent method.

Please Help!

Here's my app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule, HammerGestureConfig, HammerModule, 
 HAMMER_GESTURE_CONFIG } from '@angular/platform-browser';
import { GoogleAnalyticsService } from 
 './shared/services/googleanalytics'; // import our Google Analytics 
 service
import { BrowserAnimationsModule } from '@angular/platform- 
browser/animations';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CommonModule } from '@angular/common';


@NgModule({
  declarations: [AppComponent],
  imports: [
   CommonModule,
   BrowserModule,
   AppRoutingModule,
   HttpClientModule,
   BrowserAnimationsModule,
   ],
  providers: [GoogleAnalyticsService], 
  bootstrap: [AppComponent],
 })
 export class AppModule {}

Here's my Display component (Bdoc-a.component.ts)

import {
  Component, OnInit, Input, Output, Renderer2, AfterViewInit, 
 AfterContentChecked, ViewChild, ElementRef,
  ComponentFactoryResolver, ViewContainerRef, ViewChildren, QueryList
  } from '@angular/core';
 import { StorageService } from 
 '../../../../shared/services/storage.service';
import { AuthService } from '../../../../auth/auth.service';

 @Component({
  selector: 'app-bdoc-a',
  templateUrl: './bdoc-a.component.html',
  styleUrls: ['./bdoc-a.component.scss'],
 })
 export class BdocAComponent implements OnInit, AfterViewInit, 
 AfterContentChecked {
  @Input() docConfig = { aspectRatio: '4:3', width: 800, height: 500 };
  @Input() bgStyles = { shadow: true, bgClr: '#ffcc00' };
  @Input() showBtns = false;

  fWidth = this.docConfig.width;
  fMaxWidth = this.docConfig.width;
  fHeight = this.docConfig.height;
  fMaxHeight = this.docConfig.height;

  @Input() url = '';
  @Input() settings = { ...  };


  ...

  @ViewChildren('loadDynAssetElEditItms', { read: ViewContainerRef }) 
  biEditEls: QueryList<ViewContainerRef>;
  @ViewChildren('loadDynAssetElViewItms', { read: ViewContainerRef }) 
  biViewEls: QueryList<ViewContainerRef>;
  loadDynAssetEl: any;


  @ViewChild('bieditorFloat', { read: ElementRef }) bieditorFloat: 
  ElementRef;
  @ViewChild('bieditorFloatViewer', { read: ElementRef }) 
  bieditorFloatViewer: ElementRef;

  // variable to hold all document page elements
  allDocPages: any;
  allDocPageComp = [];


  constructor(private storage: StorageService,
          private resolver: ComponentFactoryResolver,
          private authService: AuthService,
          private elmRef: ElementRef) {
   this.url = 'templates/logo/1/logo1a/logo1a.component';
   }


   ngAfterViewInit(): void {
    this.initBrandAsset();
   }

  initBrandAsset(): void {
    this.allDocPageComp = [];

    setTimeout(() => {
      for (let i = 0; i < this.pages; i++) {
        this.loadDynAsset(this.url, i);
      }
      this.setDocPageVar();
    }, 200);
  }

  async loadDynAsset(url, pgIndex) {
    const impEl = await import( 'src/app/' + url);
    const allKeys = Object.keys(impEl);


    this.biEditEls.forEach((itm, i) => {
      if (i === pgIndex) {
      itm.clear();
      const newComp = itm.createComponent(
      this.resolver.resolveComponentFactory(impEl[allKeys[0]]));
      newComp.instance['bol']['test'] = 'LOGO TEST TEXT HERE... ' + pgIndex;

      this.allDocPageComp.push(newComp.instance);
    }
    });

    }
   }

Here's my Logo1a.component.html

   <div class="baItem biLogo logo1a edit">
    <div class="null bilNull">
      <ng-container>
        <div class="bilSymb">
          <div class="null">
           <div #forTxtLogo class="forTxtLogo" *ngIf="config.symb.mode === 
         'txt'">
        <div class="symbItm"><div class="nl">B</div></div><div 
     class="symbItm"><div class="nl">S</div></div>
      </div>
      <div #forImgSvgLogo class="forImgSvgLogo" *ngIf="config.symb.mode 
     === 'svg' || config.symb.mode === 'img'">
        <div class="symbItm"><div class="nl"></div></div>
      </div>
    </div>
   </div><!-- end of bilSymb -->
   </ng-container>
   <ng-container *ngIf="config.body.show">
    <div class="bilBody">
    <div class="null">
    <ng-container *ngIf="config.body.txt.show">
      <div #forTxtArea class="forTxtArea">
        <div class="null">Logo Body Text Area</div>
      </div>
    </ng-container>
    <ng-container *ngIf="config.body.tag.show">
      <div #forTagArea class="forTagArea"><div class="null">Logo Tag 
   Area...</div></div>
    </ng-container>
  </div>
   </div><!-- end of bilBody -->
   </ng-container>
   </div><!-- end of biNull -->
   </div><!-- end of biLogo-->

Here's my Logo1a.component.ts

import { NgModule, Component, OnInit, Input, Output, Renderer2, 
AfterViewInit, AfterContentChecked, ViewChild, ElementRef,
 ComponentFactoryResolver, ViewContainerRef, ViewChildren, QueryList
} from '@angular/core';
 import { StorageService } from 
'../../../../shared/services/storage.service';
 import { AuthService } from '../../../../auth/auth.service';

 @Component({
  selector: "app-logo1a",
   templateUrl: "./logo1a.component.html",
  styleUrls: ["./logo1a.component.scss"],
})


export class Logo1aComponent implements OnInit, AfterViewInit {
  @ViewChild('loadExtra', { read: ViewContainerRef }) loadExtra: 
 ViewContainerRef;

 @Input() bol = { test: 'LOGO 1A LOADED!' }; // breadth of life

 public config = {
   symb: {
   show: true,
   mode: 'txt', // 'txt', 'symb', 'img'
  },
 body: {
  show: true,
  txt: { show: true },
  tag: { show: true }
  }
 }

  constructor(private storage: StorageService,
          private resolver: ComponentFactoryResolver,
          private vcRef: ViewContainerRef,
          private authService: AuthService,
          private elmRef: ElementRef) { }

     ngOnInit(): void {}

     ngAfterViewInit() {}
  }

Here's my Logo1a.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Logo1aComponent } from './logo1a.component';

@NgModule({
  imports: [CommonModule],
  declarations: [Logo1aComponent],
  exports: [Logo1aComponent]
})
export class Logo1aModule {}

But this was the error that was thrown after I dynamically imported the module instead of the component in my Angular 10 app as @jburtondev suggested:

ERROR Error: Uncaught (in promise): Error: ASSERTION ERROR: Type passed in is not ComponentType, it does not have 'ɵcmp' property. ....

2 Answers

The component needs to be in its own module which declares the CommonModule. Otherwise, Angular cannot associate it with the CommonModule at runtime.

1. Create a component module

@NgModule({
  declarations: [ YourDynamicComponent ],
  imports: [ CommonModule ] // THIS IS WHAT WILL TELL ANGULAR TO LOAD IT INTO YOUR COMPONENT
  exports: [ YourDynamicComponent ]
})

export class YourDynamicModule { }

2. Load the component

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


createDynamicComponent(): void {
  const componentModule = this.compiler.compileModuleAndAllComponentsSync(YourDynamicModule);
  const factory = componentModule.componentFactories.find(c => comp.componentType === YourDynamicComponent);  
  this.viewContainerRef.createComponent(factory);
}

3. It should now be be decorated with the CommonModule

So I finally got a not-so-clean working approach to my question.

  1. I created a new module, LogoModules, where I imported all the logo modules I will be displaying in my display component

    import { NgModule } from '@angular/core'; import { Logo1aModule } from '../templates/logo/1/logo1a/logo1a.module'; import { Logo2aModule } from '../templates/logo/2/logo2a/logo2a.module'; import { Logo3aModule } from '../templates/logo/3/logo3a/logo3a.module';

    @NgModule({ // imports: [], // exports: [], })

    export class LogoModules {}

This was modified a bit from @jburtondev's suggestion to pre-import all the modules but I still think there should be a cleaner way to do this part. Dynamically loading the modules and components without pre-importing the modules still works but also fails sometimes. I think this is due to some settings with the compiler and webpack in tsconfig. I need more research to clarify this.

  1. I imported the LogoModules module in my display component

    import { LogoModules } from '../templates/logo/logos.module'; ...

BUT ... So I am able to store the relative paths of each logo template in my database and display them dynamically based on user clicks, I updated my dynamic component loader in my Display component (Bdoc-a.component.ts) as shown below:

....
async loadDynAsset(url, pgIndex) {
  const mObj = await import('src/app/' + url2);
  const mKeys = Object.keys(mObj);
  const mName = mKeys[0];

  this.compiler.compileModuleAndAllComponentsAsync(mObj[mName])
    .then((factories) => {
      const f = factories.componentFactories[0];
      const newComp = this.testDynComp.createComponent(f);
      newComp.instance['bol']['test'] = 'LOGO TEST TEXT HERE... ' + pgIndex;
    });

  .... 

}
....

And then I was able to get a consistent result without errors. Thanks again to @jburtondev for some suggestions. I'll update my answer as soon as I discover a better approach to my question.

Related