peterc
peterc

Reputation: 7863

Angular Material Stepper - how to dynamically create components to load into steps

I have seen a number of similar posts. I wish to be able to use @ngFor to create components to then load into steps of the Material Stepper

I have been following an example of dynamic loading, and some other posts (which are incomplete), and come up with the following.

First I have a number of components I want to load into the steps. They are just empty apart from the string (eg "component is working"

eg

export class StepPage1Component implements OnInit, StepPage {
      constructor() { }
      ngOnInit() {
      }
}

So I have 3 of these.

The "root" component is as follows.

export class WizardStepperComponent implements OnInit {
  @ViewChild('stepper') private myStepper: MatStepper;
  totalStepsCount: number;

  public steps: StepPage[];

  constructor(private stepsService: StepPagesService) { 
    this.steps = [
      new StepPage1Component,
      new StepPage2Component,
      new StepPage3Component
    ]
  }

Markup:

<mat-horizontal-stepper #stepper>
  <mat-step *ngFor='let step of steps'>
    <app-step-page-wrapper item='step'></app-step-page-wrapper>
  </mat-step>     
</mat-horizontal-stepper>

<!-- second option -->
<div>
  <button (click)="goBack(stepper)" type="button" [disabled]="stepper.selectedIndex === 0">Back</button>
  <button (click)="goForward(stepper)" type="button"
    [disabled]="stepper.selectedIndex === stepper._steps.length-1">Next</button>

  <!-- using totalStepsCount -->
  <!-- <button (click)="goForward(stepper)" type="button" [disabled]="stepper.selectedIndex === totalStepsCount-1">Next</button> -->
</div>

So in markup above, I have the ngFor that I am having problems with.

The wrapper is as follows...

import { Component, OnInit, Input, ViewChild, ComponentFactoryResolver } from '@angular/core';
import { directiveDef } from '@angular/core/src/view';
import { PageDirective } from '../page.directive';

@Component({
  selector: 'app-step-page-wrapper',
  template: '<ng-template pageDirective></ng-template>',
})
export class StepPageWrapperComponent implements OnInit {
  @Input() item?: any;
  @ViewChild(PageDirective) pageHost: PageDirective;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

  ngOnInit() {
    if (this.item == undefined) {
      console.error('Item undefined');
      return 
    }

    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.item);
    const viewContainerRef = this.pageHost.viewContainerRef;
    viewContainerRef.clear();
    const componentRef = viewContainerRef.createComponent(componentFactory);
  }
}

and finally the directive it is using is..

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[pageDirective]'
})
export class PageDirective {

  constructor(public viewContainerRef: ViewContainerRef) { }
}

So, I am trying to use the StepPageWrapperComponent to take each component as an input (item), and then use the componentFactoryResolver to create the conponent and then "attach" it to the directive in the template, ie pageDirective in <ng-template pageDirective></ng-template>

However, when I run this, I get the following error..

        ERROR Error: No component factory found for step. Did you add it to @NgModule.entryComponents?
        at noComponentFactoryError (core.js:9877)
        at CodegenComponentFactoryResolver.push../node_modules/@angular/core/fesm5/core.js.CodegenComponentFactoryResolver.resolveComponentFactory (core.js:9915)
        at StepPageWrapperComponent.push../src/app/step-page-wrapper/step-page-wrapper.component.ts.StepPageWrapperComponent.ngOnInit (step-page-wrapper.component.ts:21)
        at checkAndUpdateDirectiveInline (core.js:22099)
        at checkAndUpdateNodeInline (core.js:23363)
        at checkAndUpdateNode (core.js:23325)
        at debugCheckAndUpdateNode (core.js:23959)
        at debugCheckDirectivesFn (core.js:23919)
        at Object.eval [as updateDirectives] (WizardStepperComponent.html:3)
        at Object.debugUpdateDirectives [as updateDirectives] (core.js:23911)

If I put a break point in StepPageWrapperComponent, this.item just seems to be a string!

enter image description here

So, this string "step" is meant to be the ngFor variable...

enter image description here

But it is just coming in as a string, rather than the component.

Does anyone have any ideas on what I am doing wrong here, or even if there is a better way of doing this?

For any more details the source for this sample is can be cloned from https://github.com/pjc2007/wizard-stepper1.git

Update 30 Aug 19

The reason the input was not working was I forgot the square brackets when passing the variable to the input.. ie I had

<app-step-page-wrapper item='step'></app-step-page-wrapper>

instead of

    <app-step-page-wrapper [item]='step'></app-step-page-wrapper>

I now have the component as the input for StepPageWrapperComponent as expected.

enter image description here

So, my problem now is the following error that is thrown when executing the line const componentFactory=this.componentFactoryResolver.resolveComponentFactory(this.item)

ngComponent: StepPage1Component {}
message: "No component factory found for [object Object]. Did you add it to @NgModule.entryComponents?"
stack: "Error: No component factory found for [object Object]. Did you add it to @NgModule.entryComponents?↵    at noComponentFactoryError (http://localhost:4200/vendor.js:71338:17)↵    at CodegenComponentFactoryResolver.push../node_modules/@angular/core/fesm5/core.js.CodegenComponentFactoryResolver.resolveComponentFactory (http://localhost:4200/vendor.js:71376:19)↵    at StepPageWrapperComponent.push../src/app/step-page-wrapper/step-page-wrapper.component.ts.StepPageWrapperComponent.ngOnInit (http://localhost:4200/main.js:244:66)↵    at checkAndUpdateDirectiveInline (http://localhost:4200/vendor.js:83560:19)↵    at checkAndUpdateNodeInline (http://localhost:4200/vendor.js:84824:20)↵    at checkAndUpdateNode (http://localhost:4200/vendor.js:84786:16)↵    at debugCheckAndUpdateNode (http://localhost:4200/vendor.js:85420:38)↵    at debugCheckDirectivesFn (http://localhost:4200/vendor.js:85380:13)↵    at Object.eval [as updateDirectives] (ng:///AppModule/WizardStepperComponent.ngfactory.js:16:5)↵    at Object.debugUpdateDirectives [as updateDirectives] (http://localhost:4200/vendor.js:85372:21)"
__proto__: Object

I do have this as an entryComponent

ie

@NgModule({
  declarations: [
    AppComponent,
    WizardStepperComponent,
    StepPage1Component,
    StepPage2Component,
    StepPage3Component,
    PageDirective,
    StepPageWrapperComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatStepperModule,
    MatInputModule,
    MatButtonModule,
    MatAutocompleteModule
  ],
  providers: [],
  bootstrap: [AppComponent],
  entryComponents: [
    StepPage1Component,
    StepPage2Component,
    StepPage3Component,
    StepPageWrapperComponent
  ]

Update 30 Aug 19

Stepping into this.componentFactoryResolver.resolveComponentFactory, I see the following...

enter image description here

So the component seems to be in the map, but the map.get is returning undefined?

Upvotes: 6

Views: 5587

Answers (1)

yurzui
yurzui

Reputation: 214085

So the component seems to be in the map, but the map.get is returning undefined?

Yes, it's misleading. You're passing instance of class not class itself.

Consider the following example:

class Test {}
console.log(Test);       // class Test {}
console.log(new Test()); // Test {}

As you can see the output is quite similar but they are two different things.

So the solution is to pass component Type instead of component instance:

this.steps = [
  StepPage1Component,
  StepPage2Component,
  StepPage3Component
]

Upvotes: 3

Related Questions