fbpx Skip to content

Aquent | DEV6

Modal Templates in Angular 2 – Part 3: Dynamic Templates

Written by: Tyler Padley
Note: After this writing, ng-bootstrap has modified their modal implementation to accept Component Types. The techniques discussed here can still be used to create TemplateRef objects.

Complete project files for this series are available on github.

Welcome to the third and final part in my series on dynamic templates in Angular 2. In part 1, I demonstrated how changes to ng-bootstrap made it necessary to get a handle on how Angular 2 creates TemplateRef objects. I also posted a simple example using an inline template, which is a great option for one-time use.

In part 2, I explored how to make templating a little more reusable and generic by implementing a template store. While it did accomplish the goal, it did leave a little to be desired in terms of efficiency. Enter Angular 2’s ComponentFactoryResolver, and truly dynamic, on demand templating.

We’ll start with a base Class that will provide some common functionality for capturing TemplateRefs:

export class TemplateWrapperBase implements AfterViewInit, TemplateWrapper {
   @ViewChild(TemplateRef) private content: TemplateRef<any>;
   ...
   private template: Subject<TemplateRef<any>> = new Subject();
  
   getTemplateRef ():Observable<TemplateRef<any>> {
      return this.template.asObservable();
   }
  
   ngAfterViewInit (): any {

if ( this.content ) {
          this.template.next(this.content);
          this.template.complete();

}
   }
}

This class will examine its view for a TemplateRef (<template> tags in the component’s template) and capture the first one. It is recommended to employ one template per file.

Since templates aren’t populated until the view initializes, we are hooking into the AfterViewInit lifecycle process and checking for it there.

Since this is an asynchronous process, we are exposing an Observable service that can be subscribed to. Once a TemplateRef has been found we send it through the Observable and mark it as complete. This class also implements a basic interface to make consuming these templates a little easier. More on that later.

Now that we have a good base, let’s start with a simple template:

@Component({

   ...
   template: `<template>
      ...

      <div class="modal-footer">
         <button type="button" class="btn btn-secondary" (click)="cancelModal()">Cancel</button>
         <button type="button" class="btn btn-secondary" (click)="saveModal()">Done</button>
      </div>
   </template>`
})

export class ClassTemplateComponent extends TemplateWrapperBase {
   cancelModal (): void {}
  
   saveModal (): void {}
}

The component implements Angular 2’s @Component decorator, provides the template (which can also be in an external file) and extends the base class. Now let’s add the class to the Module declarations:

@NgModule({
   ...  
   declarations: [..., ClassTemplateComponent],
   entryComponents: [ClassTemplateComponent],
   ...

 })

Note that we also must declare this component as an entryComponent if we want to retain the factory for it. Our next step is to create a service class that will render the component and extract the template:

@Injectable()
export class TemplateService {
  
   constructor(private resolver: ComponentFactoryResolver ) {}
  
   public getTemplateContent ( viewContainer: ViewContainerRef, component: Type<any>): Promise<TemplateRef<any>> {
   return new Promise((resolve, reject) => {
     let factory: ComponentFactory<any> = this.resolver.resolveComponentFactory(component);
     let ref: ComponentRef<any> = viewContainer.createComponent(factory);
     let instance: TemplateWrapper = <TemplateWrapper>ref.instance;
         ...
         instance.getTemplateRef().subscribe((template: TemplateRef<any>) => {
            if ( template !== null ) {
               resolve(template);
            } else {
               reject("template does not exist in this component: " + component);
            }
            ref.destroy();
         }, (error) => {
            reject(error);
            ref.destroy();
         });
      });
   }

Whoa, there is a lot going on here. The class implements Injectable to work as a service, and takes the ComponentFactoryResolver injected by Angular in the constructor. It exposes a single method for retrieving templates that returns a Promise of type TemplateRef<any>.

public getTemplateContent ( viewContainer: ViewContainerRef, component: Type<any>)

TemplateRefs require a view container to render, but since this is a service class, we don’t have a component or template to get one from. So let’s pass it in from the calling views. We are also taking a basic Type<any> object here, which in this case is the class reference of the component we want to invoke.

Note: This is not a best practice and is only included here for brevity. Better solutions would be to implement a factory or enum in order to decouple the template class names from the views where this method would be called.

let factory: ComponentFactory<any> = this.resolver.resolveComponentFactory(component);
let ref: ComponentRef<any> = viewContainer.createComponent(factory);
let instance: TemplateWrapper = <TemplateWrapper>ref.instance;

The method then resolves the component type down to its factory and uses the view container and factory to generate a ComponentRef. The ComponentRef will have the newly created component in its instance value, as well as other component metadata. We then cast the newly created instance to our Interface to ensure type safety.

instance.getTemplateRef().subscribe((template: TemplateRef<any>) => {
      if ( template !== null ) {
         resolve(template);
      } else {
         reject("template does not exist in this component: " + component);
      }
      ref.destroy();
    }, (error) => {
      reject(error);
      ref.destroy();
    });

Lastly, the method subscribes to the Observable to wait for the TemplateRef from the ViewInit lifecycle hook and returns it. It then destroys the ComponentRef to free up memory. We can add this service as a provider, and then we are ready to create dynamic templates, like so:

export class DynamicTemplateComponent {
   constructor(private view: ViewContainerRef, private modalService: NgbModal, private templateService: TemplateService) {
   }
  
   openModal (): void {
      this.templateService.getTemplateContent(this.view, ClassTemplateComponent).then((template: TemplateRef<any>) => {
         let modal: NgbModalRef = this.modalService.open(template);
      })

This approach is by far the most flexible and should have you well on your way to creating dynamic, configurable template objects that you can reuse throughout your application without having to store them all in a single location or forcing angular to render them when they may not get used. While I developed these techniques to handle a new requirement for ng-bootstrap, this service could be used to create TemplateRefs for any of your dynamic templating needs.

Feel free to go back and check out part 1 and part 2.

Sign up for our Angular Course

Learn the most recent version and start building your own Angular apps

View course details