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).
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);
}
}
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?
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.
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.