[Fixed] Is it good idea to use ComponentFactoryResolver in Angular7 application?

Issue

I want to create an angular 7 web application that dynamically loads different components, as demonstrated in this official documentation:
https://angular.io/guide/dynamic-component-loader

But I am not sure if it’s a good idea to use ComponentFactoryResolver.
I never used it and I don’t know if it is stable and I don’t know about the performance either.

I would like some opinions about it and if anyone knows any alternatives.
I don’t want to use native innerHTML

I am trying to create a custom and generic wizard with dynamic steps.
This wizard has

  • header component
  • wizard steps
  • a "container". Right now I am using ng-template to display the content of each step(a separate component, in some cases a complicated component)
  • wizard buttons (next & previous) and in the last step action buttons like save etc

The steps are dynamic. Based on some business logic like user’s inputs from previous steps.

My current implementation:
I will show only the part where I am using the ComponentFactoryResolver to make it understandable and readable 🙂

export class WizComponent implements OnInit { 
    
  public wizContentItems: WizContentItem[] = undefined;
  public currentContentItem: WizContentItem = undefined;
  public currentContentItemNumber: number  = -1;

  public currentWizContentComponent: WizContentComponent = undefined;

  private componentRef: any;

  @Output() public onStepChanged = new EventEmitter<StepPosition>();

  private _position: StepPosition = StepPosition.First;

  constructor(private componentFactoryResolver: ComponentFactoryResolver, private viewContainerRef: ViewContainerRef) { }

  public ngOnInit() {
  } 

    public onSelectStep(contentItem: WizContentItem) {
        console.log("step was clicked");
        console.log(contentItem);
    
        if (this.currentContentItem !== undefined &&
          !this.validateStep(this.currentContentItem)) {
          return;
        }
    
        if (this.currentWizContentComponent !== undefined ) {
          this.currentContentItem.stepProgressStatus = this.currentWizContentComponent.stepProgressStatus;
      }
    
        contentItem.stepState = StepState.Active;
        const componentFactory = this.componentFactoryResolver.resolveComponentFactory(contentItem.component);
    
        this.viewContainerRef.clear();
        this.componentRef = this.viewContainerRef.createComponent(componentFactory);
        (<WizContentComponent>this.componentRef.instance).data = contentItem.data;
        (<WizContentComponent>this.componentRef.instance).stepState = contentItem.stepState;
    
        this.currentWizContentComponent = (<WizContentComponent>this.componentRef.instance);
    
        if (this.currentContentItem != null) {
          this.currentContentItem.stepState = StepState.Empty;
        }
    
        this.currentContentItem = contentItem;
        this.currentContentItem.stepState = StepState.Active;
    
        // Get currentContentItemNumber based currentContentItem
        this.currentContentItemNumber = this.wizContentItems.findIndex(wizContentItem => wizContentItem === this.currentContentItem);
    
        this.stepChanged();
      }

   public onNextClick(event: Event) {

    if ((this.currentContentItemNumber + 1) < this.wizContentItems.length) {
      let nextContentItem = this.wizContentItems[this.currentContentItemNumber + 1];
      if (nextContentItem.stepState === StepState.Disabled) {
        nextContentItem = this.getNextActiveItem(this.currentContentItemNumber + 1);
      }
      if (nextContentItem != null) {
        this.onSelectStep(nextContentItem);
      }
    }
  }

  public onPreviousClick(event: Event) {
    if ((this.currentContentItemNumber - 1) >= 0) {
      let previousContentItem = this.wizContentItems[this.currentContentItemNumber - 1];
      if (previousContentItem.stepState === StepState.Disabled) {
        previousContentItem = this.getPreviousActiveItem(this.currentContentItemNumber - 1);
      }
      if (previousContentItem !== null) {
        this.onSelectStep(previousContentItem);
      }
    }
  }

  public getCurrentStepPosition(): StepPosition {
    return this._position;
  }

  private validateStep(contentItem: WizContentItem): boolean {
    return (<WizContentImplComponent>this.componentRef.instance).isValid();
  }

  private stepChanged(): void {

    this._position = undefined;
    if (this.currentContentItemNumber <= 0) {
      this._position = StepPosition.First;
    } else if (this.currentContentItemNumber >= this.wizContentItems.length) {
      this._position = StepPosition.Last;
    } else {
      this._position = StepPosition.Middle;
    }

    if ((<WizContentComponent>this.componentRef.instance).isSummary) {
      this._position = StepPosition.Summary;
    }
    this.onStepChanged.emit(this._position);
  }

  private getNextActiveItem(itemNumber: number): WizContentItem {

    if (this.wizContentItems.length <= (itemNumber + 1)) {
      return null;
    }

    let nextContentItem = null;
    for (let i = (itemNumber); i < this.wizContentItems.length; i++) {
      if ( this.wizContentItems[i].stepState !== StepState.Disabled ) {
        nextContentItem = this.wizContentItems[i];
        break;
      }
    }

    return nextContentItem;
  }

  private getPreviousActiveItem(itemNumber: number): WizContentItem {
    if ((itemNumber - 1) < 0 ) {
      return null;
    }

    let previousContentItem = null;
    for (let i = (itemNumber - 1); i >= 0; i--) {
      if ( this.wizContentItems[i].stepState !== StepState.Disabled ) {
        previousContentItem = this.wizContentItems[i];
        break;
      }
    }

    return previousContentItem;
  }
}

Thank you!!

Solution

Yes it is good to use the ComponentFactoryResolver that is why it is in the official documentation. It is stable it is inside since Angular 2. It has no significant performance hit.

Many Angular libraries use it internally also the Angular Material library.
Check the Portal inside the Component Development Kit (CDK) and its source in GitHub where you can see it being used for displaying dynamic content inside it.

Regarding your question if it is better to do NgSwitch or create components using the ComponetFactoryResolver is difficult to answer since it depends on what you are trying to do and you did not explain what exactly is your scenario. I would say that in most cases you should use the ComponentFactoryResolver since it allows you to add any component dynamically and you don’t have a big component with a huge NgSwitch for all possible dynamic components. Only in the case you have a very small number of dynamic components and you don’t expect new ones will be added it might be more easy to create them using NgSwitch.

Leave a Reply

(*) Required, Your email will not be published