10

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!!

Junaid
  • 4,682
  • 1
  • 34
  • 40
A. Zalonis
  • 1,599
  • 6
  • 26
  • 41
  • 6
    Can you please update your question. Describe why you need to dynamically create components. What problem are you trying to solve by doing so, and give an example of what this would look like in your application. As it stands now, you've already picked a solution but not described the problem, and then asked if it's a good solution or not. – Reactgular Jul 16 '19 at 13:14
  • Thanks @Reactgular for your comment and all of you for your answers. I will update my question with more details. Actually I am trying to create a custom wizard with dynamic steps. Each step will be a separate component, some cases complicate component. – A. Zalonis Jul 23 '19 at 09:05
  • @A.Zalonis Any chance you have that code lying around. If so, please do share for others, like me, to take inspiration A repo or sandbox link would work :) – Junaid Mar 30 '21 at 08:03

2 Answers2

16

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.

Aleš Doganoc
  • 11,568
  • 24
  • 40
  • 1
    Thanks @AlesD for your answer. With ComponentFactoryResolver is it possible to use Observable data binding? Is it clear angular way to do staffs or it is more like a workaround ? Thanks – A. Zalonis Jul 23 '19 at 09:19
  • You can do Observable data binding in any component using the [`async` pipe](https://angular.io/guide/observables-in-angular#async-pipe) it does not matter how the component is created. This is a proper angular way to do things if you have Observables. Using the factory creation you need to have `@Input` properties on the component for binding and then set them in code after you create the component using the factory. – Aleš Doganoc Jul 23 '19 at 21:41
  • Ok great! Thanks a lot @AlesD – A. Zalonis Jul 25 '19 at 08:15
11

As a complement to the previous answer, to better compare the two methods, it might be worth adding a few details on what is going on in each case.

Steps to 'create' a component with the FactoryResolver service:

  1. instantiate a component class using the resolveComponentFactory() method: this method takes, as parameter, the component type, and looks for the corresponding 'component factory'.
    Nb: the component factories are classes created by Angular for each declared component with the purpose to instantiate new components
  2. 'append' the new component to the view using the createComponent() method of the ViewContainerRef class

For information: https://angular.io/guide/dynamic-component-loader#resolving-components

Steps applied when a structural directive (ngIf, ngSwitch...) 'creates' a component:

  1. the directive creates an embedded view with the supplied template. For this, it also uses the ViewContainerRef class (the createEmbeddedView() method).
  2. in case where this view contains a component selector, Angular instantiates a new component class, also using the corresponding factory, which will be appended to the view.

=> the two methods go roughly through the same steps (actually the 'structural directive' method adds an additional step, the creation of an embedded view, which, I think, is negligible).

Therefore, in my opinion, the most valuable reason to choose one out of the two options is the use case, which I would summerize as the following:

Structural directive (ngIf, ngSwitch...):

  • useful when there are few components

FactoryResolver service:

  • avoids a long list of components (as mentioned in previous answer)
  • better separation of concerns (the template, or the parent component, may not need to be aware of a list of all components which might be instantiated)
  • required to lazy load the dynamic components (I recommend this for more information: https://blog.angularindepth.com/here-is-what-you-need-to-know-about-dynamic-components-in-angular-ac1e96167f9e )