Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it good idea to use ComponentFactoryResolver in Angular7 application?

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

like image 731
A. Zalonis Avatar asked Jun 03 '19 15:06

A. Zalonis


People also ask

What is ComponentFactoryResolver and what does it do?

ComponentFactoryResolverlinkA simple registry that maps Components to generated ComponentFactory classes that can be used to create instances of components. Use to obtain the factory for a given component type, then use the factory's create() method to create a component of that type.

Which of the following directives is used to add a component at runtime in angular5?

To add the component to the template, you call createComponent() on ViewContainerRef .

What is a factory component?

A factory component is a composite activity. Structurally, it contains a set of viewpoints and a production plan. Behaviorally, it delegates its activities to other factory components or tasks.

What is component factory in angular?

ComponentFactorylinkBase class for a factory that can create a component dynamically. Instantiate a factory for a given type of component with resolveComponentFactory() . Use the resulting ComponentFactory. create() method to create a component of that type. Deprecated: Angular no longer requires Component factories.


2 Answers

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 )
like image 125
Elise Patrikainen Avatar answered Oct 20 '22 16:10

Elise Patrikainen


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.

like image 16
AlesD Avatar answered Oct 20 '22 16:10

AlesD