4

I'm building a small reference application to see how to dynamically inject components in one component to view the content on the page. I'm getting an error that points to the ViewContainerRef object.

This is the component that should display the injected component's content in the view, but it's throwing an error:

ViewContainerRef Error

Here is the StatsComponent that is throwing the error:

export class StatsComponent implements AfterViewInit, OnDestroy {
  @Input() dynComp: DynamicComponent;
  @ViewChild(ComponentHostDirective) appComponentHost: ComponentHostDirective;
  componentRef: ComponentRef<any>;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

  ngAfterViewInit() {
    this.loadComponent();
  }

  ngOnDestroy() {
    this.componentRef.destroy();
  }

  loadComponent() {
    console.log('inside stats component: ', this.dynComp);
    const comp = this.dynComp;
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp.component);
    console.log('host: ', this.appComponentHost);  // <-- this is undefined

    const viewContainerRef = this.appComponentHost.viewContainerRef;
    viewContainerRef.clear();

    this.componentRef = viewContainerRef.createComponent(componentFactory);
    (<DynamicComponent>this.componentRef.instance).data = comp.data;
  }

}

I have a working demo here, and a github project.

Why is the container not being referenced?

[UPDATE]: this now works! Go to my demo and github project to see it in action.

King Wilder
  • 629
  • 1
  • 7
  • 19

2 Answers2

3

Angular can't recognize @ViewChild(ComponentHostDirective) in your template because you didn't include ComponentHostDirective directive to list of directives that are used to compile StatsComponent:

To understand which directives angular uses to compile angular template take a look at this answer Angular 2 Use component from another module (see diagram)

I know you have already declared ComponentHostDirective within AppModule. But your StatsComponent is declared within HomeModule and this module knows nothing about ComponentHostDirective. We have to declare or import this directive in HomeModule.

If we will declare ComponentHostDirective in HomeModule we get the error

Type ComponentHostDirective is part of the declarations of 2 modules: AppModule and HomeModule!

SharedModule comes to the rescue:

src/app/shared/shared.module.ts

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [
    ComponentHostDirective
  ],
  exports: [
    CommonModule,
    ComponentHostDirective
  ]
})
export class SharedModule {}

app.module.ts

@NgModule({
  declarations: [
    ComponentHostDirective <== remove it
  ],
  imports: [
    ...
    SharedModule, // add this
    ...
  ]
})
export class AppModule { }

home.module.ts

@NgModule({
  imports: [
    ...
    SharedModule, // add this
    ...
  ]
})
export class HomeModule { }

After that your appComponentHost property will refer to ComponentHostDirective instance.

In addition, you will get the error

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Customers'.

after component creation.

Don't change bindings within ngAfterViewInit hook. Use ngOnInit instead:

export class StatsComponent implements OnInit, OnDestroy {
  ...
  ngOnInit() {
    this.loadComponent();
  }

The article Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error explains the behavior in great details (Thank's @Maximus).

Finally you will see the result:

enter image description here

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • brilliant! This works, now, sort of. The only problem now is that it should display the content of any button that is selected, not just the first one. So I'm still missing something. And thanks for the references to the various articles. Very informative. It'll take a few reads to fully understand them. – King Wilder Aug 06 '17 at 00:27
  • @KingWilder Just change `ngOnInit` to `ngOnChanges` – yurzui Aug 06 '17 at 04:59
  • Yep, that did it. – King Wilder Aug 07 '17 at 17:50
0

small buglet:

this.componentRef = viewContainerRef.createComponent(componentFactory);

should be:

let componentRef = viewContainerRef.createComponent(componentFactory);

don't add it on the component. and use let instead of const.

bryan60
  • 28,215
  • 4
  • 48
  • 65