Radim Köhler Radim Köhler - 3 months ago 495
TypeScript Question

How can I use/create dynamic template to compile Component with Angular2 (RC5)?

I want to dynamically create template. This should be used to build a

ComponentType
at Runtime and place (even replace) it somewhere inside of the hosting Component.

Until RC4 I was using
ComponentResolver
, but with RC5 I get message:


ComponentResolver
is deprecated for dynamic compilation. Use
ComponentFactoryResolver
together with
@NgModule/@Component.entryComponents
or ANALYZE_FOR_ENTRY_COMPONENTS provider instead. For runtime compile only, you can also use
Compiler.compileComponentSync/Async
.


I found this (offical angular2) document

Angular 2 Synchronous Dynamic Component Creation



And understand that I can use either


  • Kind of dynamic
    ngIf
    with
    ComponentFactoryResolver
    . If I will pass known components into hosting one inside of
    @Component({entryComponents: [comp1, comp2], ...})
    - I can use
    .resolveComponentFactory(componentToRender);

  • Real runtime compilation, with
    Compiler
    ...



But the question is how to use that
Compiler
? The Note above says that I should call:
Compiler.compileComponentSync/Async
- so how?

For example. I want to create (based on some configuration conditions) this kind of template for one kind of settings

<form>
<string-editor
[propertyName]="'code'"
[entity]="entity"
></string-editor>
<string-editor
[propertyName]="'description'"
[entity]="entity"
></string-editor>
...


and in another case this one (
string-editor
is replaced with
text-editor
)


<form>
<text-editor
[propertyName]="'code'"
[entity]="entity"
></text-editor>
...


And so on (different number/date/reference
editors
by property types, skipped some properties for some users...)
. I.e. this is an example, real configuration could generate much more different and complex templates.

The template is changing, so I cannot use
ComponentFactoryResolver
and pass existing ones... I need solution with the
Compiler

Answer

EDIT - related to RC6

Below solution won't work with RC6. The RuntimeCompiler does not support compileComponentAsync...

From RC6 changelog - https://github.com/angular/angular/blob/master/CHANGELOG.md#breaking-changes:

All the components and pipes now must be declared via an NgModule. NgModule is the basic compilation block passed into the Angular compiler via Compiler#compileModuleSync or #compileModuleAsync.

Because of this change, the Compiler#compileComponentAsync and #compileComponentSync were removed as well - any code doing compilation should compile module instead using the APIs mentioned above.

Lastly, since modules are the basic compilation unit, the ngUpgrade module was modified to always require an NgModule to be passed into the UpgradeAdapter's constructor - previously this was optional.

.

.

.

OBSOLETE SOLUTION - Working with RC5 only

Similar topic is discussed here Equivalent of $compile in Angular 2. We need to use RuntimeCompiler and NgModule. Read more about NgModule in Angular2 here:

There is a working plunker/example (dynamic template, dynamic component type,RuntimeCompiler, ... in action)

Below description of this scenario, we will

1) create brand new module MyModule:NgModule
2) create dynamic Template (simple approach)
3) use a builder to create new Component (only if template has changed)
4) call RuntimeCompiler.compileComponentAsync(componentType, DetailViewModule)
5) create handling to "re-create" Dynamic component (switch from INPUT to TEXTAREA editing)

NgModule

We need an NgModule, which is later used by RuntimeCompiler. The most simple module could be like this:

// plunker - app/module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
// read more here
// http://angularjs.blogspot.cz/2016/08/angular-2-rc5-ngmodules-lazy-loading.html
// https://docs.google.com/document/d/1VRNljdv-6QDY4_I0xx3DHd-IZ19QlthheMLdGGKAAzM/edit?pref=2&pli=1#
import { AppComponent }  from './app.component';
// even directives are moved from component(s)
import { DynamicDetail } from './dynamic/detail.view';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, DynamicDetail ],
  bootstrap:    [ AppComponent ]
})
export class MyModule { }

To get more understanding about this feature, please read here:

template builder

In our example we will process detail of this kind of entity

entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
};

To create a template, in this plunker we use this simple/naive builder.

The real solution, a real template builder, is the place where your application can do a lot

// plunker - app/dynamic/template.builder.ts
import {Injectable} from "@angular/core";

@Injectable()
export class DynamicTemplateBuilder {

    public prepareTemplate(entity: any, useTextarea: boolean){

      let properties = Object.keys(entity);
      let template = "<form >";
      let editorName = useTextarea 
        ? "text-editor"
        : "string-editor";

      properties.forEach((propertyName) =>{
        template += `
          <${editorName}
              [propertyName]="'${propertyName}'"
              [entity]="entity"
          ></${editorName}>`;
      });

      return template + "</form>";
    }
}

A trick here is - it builds a template which uses some set of known properties, e.g. entity. Such property(-ies) must be part of dynamic component, which we will create next.

To make it a bit more easier, we can use an interface to define properties, which our Template builder can use. This will be implemented by our dynamic Component type.

export interface IHaveDynamicData { 
    public entity: any;
    ...
}

component builder

Very important thing here is to keep in mind:

our type, build with our DynamicTypeBuilder, could differ - but only in its template (build above). Components properties (inputs, outputs or some protected) are still same. If we need different properties, we should define different combination of Template and Type Builder

// plunker - app/dynamic/type.builder.ts
import {Component, Input, Output, Injectable} from "@angular/core";
import {Observable}                           from "rxjs/Rx";    
import {FORM_DIRECTIVES}                      from "@angular/forms";
import {DYNAMIC_DIRECTIVES}                   from '../parts/all';   

@Injectable()
export class DynamicTypeBuilder {    
  static CacheOfTypes: { [templateKey: string]: any } = {};

  public CreateComponent(template: string): any {    
    let type = DynamicTypeBuilder.CacheOfTypes[template];    
    if (type) {
       return type; // we already have created such type
    }        
    // unknown template ... let's create a Type for it
    type = this.CreateNewComponent(template, FORM_DIRECTIVES.concat(DYNAMIC_DIRECTIVES));
    // cache that type - because the only difference would be "template"
    DynamicTypeBuilder.CacheOfTypes[template] = type;    
    return type;
  }

  protected CreateNewComponent (tmpl:string, injectDirectives: any[]) {
      @Component({
          selector: 'dynamic-component',
          template: tmpl,
          directives: injectDirectives,
      })
      class CustomDynamicComponent  implements IHaveDynamicData {
          @Input()  public entity: any;
      };

      return CustomDynamicComponent ;
  }
}

Important:

our component dynamic types differ, but just by template. So we use that fact to cache them. This is really very important. Angular2 will also cache these.. by the type. And if we would recreate for the same template strings new types... we will start to generate memory leaks.

RuntimeCompiler used by hosting component

Final piece is a component, which hosts the target for our dynamic component, e.g. <div #dynamicContentPlaceHolder></div>. We get a reference to it and use RuntimeCompiler to provide us with a Factory to create a component. That is in a nutshell, and here are all the pieces of that component (if needed, open plunker here)

Let's firstly summarize import statements:

import {Component, ComponentRef,ViewChild,ViewContainerRef}   from '@angular/core';
import {AfterViewInit,OnInit,OnDestroy,OnChange,SimpleChange} from '@angular/core';
import {RuntimeCompiler}                                      from "@angular/compiler";

import { IHaveDynamicData, DynamicTypeBuilder } from './type.builder';
import { DynamicTemplateBuilder }               from './template.builder';
import { MyModule }                             from '../module';

Here is another snippet of it, with a standard decorator and constructor parts:

@Component({
  selector: 'dynamic-detail',
  providers: [DynamicTypeBuilder, DynamicTemplateBuilder],
  template: `
<div>
  check/uncheck to use INPUT vs TEXTAREA:
  <input type="checkbox" #val (click)="refreshContent(val.checked)" /><hr />
  <div #dynamicContentPlaceHolder></div>  <hr />
  entity: <pre>{{entity | json}}</pre>
</div>
`,
})
export class DynamicDetail implements AfterViewInit
{ 
    // wee need Dynamic component builder
    constructor(
        protected typeBuilder: DynamicTypeBuilder,
        protected templateBuilder: DynamicTemplateBuilder,
        protected compiler: RuntimeCompiler,
    ) {}
    ...

We just receive, template and component builders (our code) and the Angular2 RuntimeCompiler. Also, above - we've already imported the MyModule - essential part of this solution.

Properties which are needed for our example (more in comments)

// reference for a <div> with #dynamicContentPlaceHolder
@ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef}) 
protected dynamicComponentTarget: ViewContainerRef;
// this will be reference to dynamic content - to be able to destroy it
protected componentRef: ComponentRef<IHaveDynamicData>;

// until ngAfterViewInit, we cannot start (firstly) to process dynamic stuff
protected wasViewInitialized = false;

// example entity ... to be recieved from other app parts
// this is kind of candiate for @Input
protected entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
  };

In this simple scenario, our hosting component does not have any @Input. So it does not have to react to changes. But despite of that fact (and to be ready for coming changes) - we need to introduce some flag if the component was already (firstly) initiated. And only then we can start the magic.

Also, we need to keep a reference to compiled template.. to be able properly destroy() it, whenever we will change it.

// this is the best moment where to start to process dynamic stuff
public ngAfterViewInit(): void
{
    this.wasViewInitialized = true;
    this.refreshContent();
}
// wasViewInitialized is an IMPORTANT switch 
// when this component would have its own changing @Input()
// - then we have to wait till view is intialized - first OnChange is too soon
public ngOnChanges(changes: {[key: string]: SimpleChange}): void
{
    if (this.wasViewInitialized) {
        return;
    }
    this.refreshContent();
}

public ngOnDestroy(){
  if (this.componentRef) {
      this.componentRef.destroy();
      this.componentRef = null;
  }
}

And finally RuntimeCompiler. It gets template, component type - and calls compileComponentAsync(dynamicType, MyModule). And that is the place where we must provide new feature NgModule (MyModule in our case).

protected refreshContent(useTextarea: boolean = false){

  if (this.componentRef) {
      this.componentRef.destroy();
  }
  console.log(this.useTextarea)

  // here we get a TEMPLATE with dynamic content === TODO
  var template = this.templateBuilder
    .prepareTemplate(this.entity, useTextarea);

  // pass template and get the complete ConcreteType<T>
  var dynamicType = this.typeBuilder
    .CreateComponent(template);

  this.compiler
    .compileComponentAsync<IHaveDynamicData>(dynamicType, MyModule)
    .then((factory: ng.ComponentFactory<IHaveDynamicData>) =>
    {
        // our component will be inserted after #dynamicContentPlaceHolder
        this.componentRef = this.dynamicComponentTarget.createComponent(factory, 0);

        // and here we have access to our dynamic component
        let component: IHaveDynamicData = this.componentRef.instance;

        component.entity = this.entity;
    });
}

And that is pretty it. Do not remember to Destroy anything what was build dynamically (ngOnDestroy). Also, do cache dynamic types if they differ - just by template.

Check it all in action here

Comments