Here is how to get ViewContainerRef before @ViewChild query is evaluated

Written by maxim.koretskyi | Published 2017/07/06
Tech Story Tags: programming | angular | angular2 | web-development | javascript

TLDRvia the TL;DR App

In one of my recent article on dynamic component instantiation Here is what you need to know about dynamic components in Angular I’ve shown the way how to add a child component to the parent component view dynamically. All dynamic components are inserted into a specific place in the template using ViewContainerRef reference. This is reference usually obtained by the specifying some template reference variable in the parent component template and then using queries like ViewChild inside the component to get it.

Suppose we have our parent App component and we want to add a child A component into the specific place in the template. Here is how we do that.

A component

We’re creating A component

@Component({
  selector: 'a-comp',
  template: `
      <span>I am A component</span>
  `,
})
export class AComponent {
}

App root module

And then register it with in the declarations and entryComponents:

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, AComponent],
  entryComponents: [AComponent],
  bootstrap: [AppComponent]
})
export class AppModule {
}

App component

And then in the parent App component we put the code that creates A component instance and inserts it

@Component({
  moduleId: module.id,
  selector: 'my-app',
  template: `
      <h1>I am parent App component</h1>
      <div class="insert-a-component-inside">
          <ng-container #vc></ng-container>
      </div>
  `,
})
export class AppComponent {
  @ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef;

  constructor(private r: ComponentFactoryResolver) {}

  ngAfterViewInit() {
    const factory = this.r.resolveComponentFactory(AComponent);
    this.vc.createComponent(factory);
  }
}

Here is the working plunker. If that’s something you don’t understand, I suggest you read the article I mentioned in the beginning.

This is all well and good but there’s one limitation with that approach. We have to wait until the ViewChild query is evaluated and that happens during change detection. We can access the reference only after ngAfterViewInit lifecycle hook. But what if we don’t want to wait until Angular runs change detection and want to have a complete component view before change detection? As it turns out we can do that using a directive instead of template reference and ViewChild query.

Using directive instead of ViewChild query

Every directive can inject a reference to the ViewContainerRef into the constructor. This will be a reference to a view container associated with and anchored to the directive host element. So let’s implement such a directive:

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

@Directive({
  selector: '[app-component-container]',
})

export class AppComponentContainer {
  constructor(vc: ViewContainerRef) {
    vc.constructor.name === "ViewContainerRef_"; // true
  }
}

I’ve added the check in the constructor to ensure that view container is available when the directive is instantiated. Now we need to use it in the App component template instead #vc template reference:

<div class="insert-a-component-inside">
    <ng-container app-component-container></ng-container>
</div>

If you run it you will see that it works fine. Great, we now know how a directive can access the view container before change detection. Now it somehow needs to pass it to the component. How can we do that? Well, a directive can inject a parent component and call a method on the component directly. However, there’s a limitation in that the directive has to know the name of the parent component or use the approach described here.

A better alternative is to use a service shared between a component and its child directives and communicate through it! We can implement that service on the component directly to make it local. I’ll also use a custom string token for simplicity:

const AppComponentService= {
  createListeners: [],
  destroyListeners: [],
  onContainerCreated(fn) {
    this.createListeners.push(fn);
  },
  onContainerDestroyed(fn) {
    this.destroyListeners.push(fn);
  },
  registerContainer(container) {
    this.createListeners.forEach((fn) => {
      fn(container);
    })
  },
  destroyContainer(container) {
    this.destroyListeners.forEach((fn) => {
      fn(container);
    })
  }
};

@Component({
  providers: [
    {
      provide: 'app-component-service',
      useValue: AppComponentService
    }
  ],
  ...
})
export class AppComponent {

This service simply implements primitive pub/subscribe pattern and notifies subscribes when the container is registered.

Now we can inject that service into the AppComponentContainer directive and register the view container:

export class AppComponentContainer {
  constructor(vc: ViewContainerRef, @Inject('app-component-service') shared) {
    shared.registerContainer(vc);
  }
}

And the only thing that is left is to listen in the App component when the container is registered and use it to create a component dynamically:

export class AppComponent {
  vc: ViewContainerRef;

  constructor(private r: ComponentFactoryResolver, @Inject('app-component-service') shared) {
    shared.onContainerCreated((container) => {
      this.vc = container;
      const factory = this.r.resolveComponentFactory(AComponent);
      this.vc.createComponent(factory);
    });

    shared.onContainerDestroyed(() => {
      this.vc = undefined;
    })
  }
}

Here is the plunker. And that’s it. You can see that we no longer need a ViewChild query. And if you add a ngOnInit lifecycle hook you will see that the A component is rendered before it’s triggered.

Did you find the information in the article helpful?


Published by HackerNoon on 2017/07/06