简体   繁体   中英

Angular dynamic component import with ComponentFactoryResolver: Type passed in is not ComponentType

I am trying to reduce the main bundle size of my Angular application, of which my latest effort involves making a modular component load its "child components" dynamically instead of through importing them statically. The folder structure (and parent-child relation) looks like the following:

io-input/io-input.component
├─ date/date.component
├─ ... more components ...
└─ ... more components ...

This resides in an Angular 10 project, within an Angular library. This library is imported as a module inside an Angular application in the same Angular project. When I compile the application with ng serve , this application runs into errors revolving around the io-input component (specifically, the code that dynamically imports child components).

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

My question is: what's wrong in my new dynamic import approach? I provided my approach below (before dynamic import and after dynamic import).

Old situation: static imports that work

If I statically import all the components in io-input.component , everything just works. I use the ComponentFactoryResolver with resolveComponentFactory , and based on config that I supply through @Input() , a new instance of the chosen component will be generated and inserted in the parent component's dedicated ViewContainer . Example:

import { DateComponent } from './date/date.component';
import { 
  Component, ComponentFactoryResolver,
  AfterViewInit, OnDestroy,
  Input, Output,
  EventEmitter, ViewEncapsulation,
  ViewChild, ViewContainerRef, HostBinding, ElementRef, Injector
} from '@angular/core';

// ... more imports/interfaces/services/etc. here ...

@Component({
  selector: 'app-io-input',
  templateUrl: './io-input.component.html',
  styleUrls: ['./io-input.component.scss'],
  encapsulation : ViewEncapsulation.None,
  providers: [
     // ... some services here
  ]
})
export class IoInputComponent implements AfterViewInit, OnDestroy, MatFormFieldControl<any>, ControlValueAccessor {
  
  @Input() inputConfig : InputConfigInterface;
  private componentRef : any;
  // ... more variables ...
  
  constructor(
    private componentFactoryResolver: ComponentFactoryResolver
  ) {
  }
  ngAfterViewInit() {
    let comp : any;
    switch(this.inputConfig.type){
      case "date":
        comp = DateComponent;
      break;
      // more cases for other components
      // a default (fallback) component in a default case
    }
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp);
    this.componentRef = this.container.createComponent(componentFactory);
  }
}

New situation: dynamically import components

When I dynamically load my component, I get the error:

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

The updated code with dynamic imports looks as follows:

// import { DateComponent } from './date/date.component'; // not used anymore
import { 
  Component, ComponentFactoryResolver,
  AfterViewInit, OnDestroy,
  Input, Output,
  EventEmitter, ViewEncapsulation,
  ViewChild, ViewContainerRef, HostBinding, ElementRef, Injector
} from '@angular/core';

// ... more imports/interfaces/services/etc. here ...

@Component({
  selector: 'app-io-input',
  templateUrl: './io-input.component.html',
  styleUrls: ['./io-input.component.scss'],
  encapsulation : ViewEncapsulation.None,
  providers: [
    // ... some services here
  ]
})
export class IoInputComponent implements AfterViewInit, OnDestroy, MatFormFieldControl<any>, ControlValueAccessor {
  
  @Input() inputConfig : InputConfigInterface;
  private componentRef : any;
  // ... more variables ...
  
  constructor(
    private componentFactoryResolver: ComponentFactoryResolver
  ) {
  }
  ngAfterViewInit() {
    let comp : any;
    switch(this.inputConfig.type){
      case "date":
        comp = import('./date/date.component').then(m => { // new: dynamically imported component
            debugger;                                      // allowed me to know what "m" looks like
            return m.DateComponent;                        // new: dynamically imported component
        });                                                // new: dynamically imported component
      break;
      // more cases for other dynamically imported components
      // a default (fallback) component in a default case
    }
    debugger;                                              // allowed me to know what "comp" looks like
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp);
    this.componentRef = this.container.createComponent(componentFactory);
  }
}

If I inspect my code on runtime with debugger; , m is populated with an object with property DateComponent , which does seem to have a property named literally "ɵcmp" . Am I doing something wrong in my approach to dynamically importing a component?

在动态导入 DateComponent 时检查调试器断点

As @MikeOne suggests, the article by Nethanel Basal provides a good direction. I highly recommend anyone working with dynamic component loading to read it. Here are some links.

Because in my situation, I worked more in a parameterized fashion, so the basic examples in Nethanel's article weren't sufficiently modular.

Some takeaways

  1. I needed to make sure that the typing of my modular setup was correct; loading one component from a set of components based on a parameter requires you to account for any of the types that you want to support! Otherwise the compiler will throw errors.
  2. When loading components with the dynamic import() webpack-function, you need to handle the result in an async fashion (eg with a Promise.then() callback). Account for that as well in your typing!

These takeaways led me to defining a variable with the following type:

let comp : Promise<Type<DateComponent | PlainTextComponent | FileComponent | ToggleComponent>>;

In this case, the list of component types if non-exhaustive. Just define any component that you need here. Type<NAMEOFCOMPONENT> is just an interface of ComponentType typed as the component that you want to use. Type has to imported from @angular/core to make this work.

Here's my updated code. Please note that this is probably a mere starting point; there might be more space for improvement here! Think: cleaning up the code and making the code itself more efficient.

import { DateComponent } from './date/date.component';
import { PlainTextComponent } from './plain-text/plain-text.component';
import { ToggleComponent } from './toggle/toggle.component';

// The components are imported for type definition only.

import { 
  Component, ComponentFactoryResolver,
  AfterViewInit, OnDestroy,
  Input, Output,
  EventEmitter, ViewEncapsulation,
  ViewChild, ViewContainerRef, HostBinding, ElementRef, Injector, Type
} from '@angular/core';

// ... more imports/interfaces/services/etc. here ...

@Component({
  selector: 'app-io-input',
  templateUrl: './io-input.component.html',
  styleUrls: ['./io-input.component.scss'],
  encapsulation : ViewEncapsulation.None,
  providers: [
    // ... some services here
  ]
})
export class IoInputComponent implements AfterViewInit, OnDestroy, MatFormFieldControl<any>, ControlValueAccessor {
  
  @Input() inputConfig : InputConfigInterface;
  private componentRef : any;
  // ... more variables ...
  
  constructor(
    private componentFactoryResolver: ComponentFactoryResolver
  ) {
  }
  ngAfterViewInit() {
    let comp : Promise<Type<DateComponent PlainTextComponent | ToggleComponent>>;
    switch(this.inputConfig.type){
      case "date":
        import('./date/date.component').then(m => {
            this.loadInput(m.DateComponent);                        
        });
      break;
      case "toggle":
        import('./toggle/toggle.component').then((m) => {
          this.loadInput(m.ToggleComponent);
        });
      break;
      // more cases for other dynamically imported components
      default:
        import('./plain-text/plain-text.component').then((m) => {
            this.loadInput(m.PlainTextComponent);
        });
      break;
    }
    debugger;
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp);
    this.componentRef = this.container.createComponent(componentFactory);
  }
  // What you get from the Promise is what you process here.
  // A ComponentType; hence the use of 'Type'
  loadInput(component : Type<DateComponent | ToggleComponent | PlainTextComponent>){
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory<
      DateComponent |
      ToggleComponent |
      PlainTextComponent
    >(component);
    const viewContainerRef = this.ioInputView.viewContainerRef;
    viewContainerRef.clear();
    this.componentRef = viewContainerRef.createComponent<
      DateComponent |
      ToggleComponent |
      PlainTextComponent
    >(componentFactory);
    // As shown above, you just use the component itself as typing in createComponent and resolveComponentFactory, as that is your end result: a component to be rendered!
}

Concluding, I got the dynamic component loading up and running. There were quite a few gotchas for me, so please make sure that you don't fall into the same pitfalls. Take a look at my takeaways.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM