3

I'm trying to load a dynamic component into my view when a user clicks the button which looks like the following:

<button (click)="showContent()">Show Panel</button>
<!--This starts as 'showPanel = false'-->
<div *ngIf="showPanel">
    <ng-template #ref></ng-template>
</div>

However, when clicking the button in my view, I try to run loadComponent() but I get an error stating Cannot read property 'createComponent' of undefined

I've looked up some solutions to this, there were some suggesting using QueryList<ViewContainerRef> but I have not been successful in getting it to work.

Source: ViewContainerRef is undefined when called in ngAfterViewInit

Another solution suggested using ElementRef and checking the chaged state, but even that was always undefined when trying to check it in ngAfterViewInit.

Source: @ViewChild in *ngIf

I'm looking for options that will work with Angular 8, but I'm not entirely sure where to look next.

Code below:

parent.component.ts

export class ParentComponent {

@ViewChild('ref', { static: false, read: ViewContainerRef }) ref: ViewContainerRef;

showPanel = false;

loadComponent(): void {
    const factory = this.componentFactoryResolver.resolveComponentFactory(ChildComponent);
    const component = this.ref.createComponent(factory);
    component.changeDetectorRef.detectChanges();
}

showContent(): void {
    this.showPanel = !this.showPanel;
    this.loadContent(); // Error is thrown here, this doesn't make sense as *ngIf should now be true.
}
expenguin
  • 1,052
  • 2
  • 21
  • 42
  • I don't understand how you want to load component into ref which doesn't exist yet since `showPanel = false;` – yurzui Oct 31 '19 at 15:48
  • Your DOM Element does not exist because you start with `false` on showPanel ergo your ref does not exist. – Michelangelo Oct 31 '19 at 15:49
  • @yurzui, I understand that, however when you click the button and it runs loadContent(), it fails with the same exact error. I didn't have that very clear in my question. updated. – expenguin Oct 31 '19 at 15:52
  • @Michelangelo same as with Yurzui above, my question was not very clear I've updated it to show whats actually happening – expenguin Oct 31 '19 at 15:53
  • At the time you change showPanel property Angular doesn't update the view. There are many options you can use here: 1) use setter for ViewChild 2) use ngComponentOutlet to render component 3) use cdRef.detectChanges() right after `this.showPanel = !this.showPanel;` – yurzui Oct 31 '19 at 16:04
  • @Yurzui, perfect, I will explore these options and report back. – expenguin Oct 31 '19 at 16:04
  • @Yurzui, so I explored those options and found the following: 1. The setter throws a Maximum call stack size exceeded Error, I assume this must be some form of recursion . I set it as such: @ViewChild('ref', { static: true, read: ViewContainerRef }) set ref(content: ViewContainerRef) { this.ref= content; } 2. The ngComponentOutlet seems like a good idea but it only ever outputs on type of Component, doesn't seem ideal for a dynamic case? 3. cdRef.detectChanges() can only come after I use .createComponent() which is where this is failing – expenguin Oct 31 '19 at 17:31

2 Answers2

2

It's because *ngIf removes the div element while the condition evaluates to false, it means that the child element doesn't exists inside your component template.

you can use [hidden] instead which only hide the div element so you can access it through template reference variable.

<button (click)="showContent()">Show Panel</button>
<!--This starts as 'showPanel = false'-->
<div [hidden]="!showPanel">
    <ng-template #ref></ng-template>
</div>
Bilel-Zheni
  • 1,202
  • 1
  • 6
  • 10
  • I'd rather not load the component at all if I don't have to. Having [hidden] still means I've got to render the component before its even used. Not ideal. – expenguin Oct 31 '19 at 15:53
  • but I haven't said that you need to load the component inside the ngAfterViewInit() method, your code mention that this line is commented. – Bilel-Zheni Oct 31 '19 at 15:59
  • My question was poorly written, my issue comes with loading the component after the ngAfterViewInit lifecycle hook. It throws the error when trying to run loadComponent() stating Cannot read property 'createComponent' of undefined after the *ngIf has been set to true. The idea is to not load anything into the DOM that doesn't have to be there. – expenguin Oct 31 '19 at 16:00
  • in this case you don't even need to wrap the ng-template tag with conditional div, because it doesn't display anything until you replace it with your dynamic component after user click the Show Panel button. – Bilel-Zheni Oct 31 '19 at 16:05
  • So I removed the ng-template out of the div and it did in fact work on button click, however I have need to utilize the div style classes. What would prevent the component loading if *ngIf = true? Is it due to the fact that its changing state? – expenguin Oct 31 '19 at 17:15
  • And just to note, [hidden] doesn't work in Angular 8 as far as I can tell – expenguin Oct 31 '19 at 17:53
2

Following up yurzui's ideas, here is how I'd apply them:

Here is a StackBlitz example.

changeDetectorRef.detectChanges()

showContent () {
    this.showPanel = !this.showPanel;

   if (this.showPanel) {
      this.cdr.detectChanges();
      this.loadComponent();
    }
  }

setter for ViewChild

  private _ref: ViewContainerRef;

  private get ref () {
    return this._ref;
  }

  @ViewChild('ref', { static: false, read: ViewContainerRef })
  private set ref (r) {
    console.log('setting ref', r)
    this._ref = r;

    if (this._ref) {
      this.loadComponent();
    }
  }

  showPanel = false;

  constructor (
    private cdr: ChangeDetectorRef,
    private cfr: ComponentFactoryResolver,
  ) { }

  loadComponent () {
    const factory = this.cfr.resolveComponentFactory(ChildComponent);
    const component = this.ref.createComponent(factory);
  }

  showContent () {
    this.showPanel = !this.showPanel;
  }

using <ng-container>

As you pointed out, using ngTemplateOutlet is a usually a good solution, but when you deal with more than one component, it can become cumbersome to perform all that logic in the template.

We can leverage the ViewContainerRef's API to handle everything from your component(.ts file).

<button (click)="showContent()">Show Panel</button>

<ng-container #vcr></ng-container>
@ViewChild('vcr', { static: true, read: ViewContainerRef })
 vcr: ViewContainerRef;

 showContent () {
  this.showPanel = !this.showPanel;  

  this.showPanel && this.attachComponent();

  !this.showPanel && this.removeComponent();  
}

private attachComponent () {
  const compFactory = this.cfr.resolveComponentFactory(ChildComponent);

  const compView = this.vcr.createComponent(compFactory);
}

private removeComponent () {
    this.vcr.clear();
}

This approach gives you more control than you can handle!
You can, for example, preserve a component's state after it showPanel becomes false by using vcr.detach and vcr.insert.

You can find how right here.

Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31