expenguin
expenguin

Reputation: 1124

Load dynamic component within *ngIf - createComponent is undefined

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.
}

Upvotes: 3

Views: 5003

Answers (2)

Andrei Gătej
Andrei Gătej

Reputation: 11934

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.

Upvotes: 2

Bilel-Zheni
Bilel-Zheni

Reputation: 1312

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>

Upvotes: 3

Related Questions