How can I use/create dynamic template to compile d

2018-12-31 00:28发布

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


AOT and JitCompiler (former RuntimeCompiler)

Would you like to use this features with AOT (ahead of time compilation)? Are you getting:

Error: Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function (position 65:17 in the original .ts file), resolving symbol COMPILER_PROVIDERS in .../node_modules/@angular/compiler/src/compiler.d.ts,

Please, leave your comment, vote here:

Could/would/will code using COMPILER_PROVIDERS be supported by AOT?

11条回答
闭嘴吧你
2楼-- · 2018-12-31 01:06

Following up on Radmin's excellent answer, there is a little tweak needed for everyone who is using angular-cli version 1.0.0-beta.22 and above.

COMPILER_PROVIDERScan no longer be imported (for details see angular-cli GitHub).

So the workaround there is to not use COMPILER_PROVIDERS and JitCompiler in the providers section at all, but use JitCompilerFactory from '@angular/compiler' instead like this inside the type builder class:

private compiler: Compiler = new JitCompilerFactory([{useDebug: false, useJit: true}]).createCompiler();

As you can see, it is not injectable and thus has no dependencies with the DI. This solution should also work for projects not using angular-cli.

查看更多
梦醉为红颜
3楼-- · 2018-12-31 01:13

I decided to compact everything I learned into one file. There's a lot to take in here especially compared to before RC5. Note that this source file includes the AppModule and AppComponent.

import {
  Component, Input, ReflectiveInjector, ViewContainerRef, Compiler, NgModule, ModuleWithComponentFactories,
  OnInit, ViewChild
} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

@Component({
  selector: 'app-dynamic',
  template: '<h4>Dynamic Components</h4><br>'
})
export class DynamicComponentRenderer implements OnInit {

  factory: ModuleWithComponentFactories<DynamicModule>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler) { }

  ngOnInit() {
    if (!this.factory) {
      const dynamicComponents = {
        sayName1: {comp: SayNameComponent, inputs: {name: 'Andrew Wiles'}},
        sayAge1: {comp: SayAgeComponent, inputs: {age: 30}},
        sayName2: {comp: SayNameComponent, inputs: {name: 'Richard Taylor'}},
        sayAge2: {comp: SayAgeComponent, inputs: {age: 25}}};
      this.compiler.compileModuleAndAllComponentsAsync(DynamicModule)
        .then((moduleWithComponentFactories: ModuleWithComponentFactories<DynamicModule>) => {
          this.factory = moduleWithComponentFactories;
          Object.keys(dynamicComponents).forEach(k => {
            this.add(dynamicComponents[k]);
          })
        });
    }
  }

  addNewName(value: string) {
    this.add({comp: SayNameComponent, inputs: {name: value}})
  }

  addNewAge(value: number) {
    this.add({comp: SayAgeComponent, inputs: {age: value}})
  }

  add(comp: any) {
    const compFactory = this.factory.componentFactories.find(x => x.componentType === comp.comp);
    // If we don't want to hold a reference to the component type, we can also say: const compFactory = this.factory.componentFactories.find(x => x.selector === 'my-component-selector');
    const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
    const cmpRef = this.vcRef.createComponent(compFactory, this.vcRef.length, injector, []);
    Object.keys(comp.inputs).forEach(i => cmpRef.instance[i] = comp.inputs[i]);
  }
}

@Component({
  selector: 'app-age',
  template: '<div>My age is {{age}}!</div>'
})
class SayAgeComponent {
  @Input() public age: number;
};

@Component({
  selector: 'app-name',
  template: '<div>My name is {{name}}!</div>'
})
class SayNameComponent {
  @Input() public name: string;
};

@NgModule({
  imports: [BrowserModule],
  declarations: [SayAgeComponent, SayNameComponent]
})
class DynamicModule {}

@Component({
  selector: 'app-root',
  template: `
        <h3>{{message}}</h3>
        <app-dynamic #ad></app-dynamic>
        <br>
        <input #name type="text" placeholder="name">
        <button (click)="ad.addNewName(name.value)">Add Name</button>
        <br>
        <input #age type="number" placeholder="age">
        <button (click)="ad.addNewAge(age.value)">Add Age</button>
    `,
})
export class AppComponent {
  message = 'this is app component';
  @ViewChild(DynamicComponentRenderer) dcr;

}

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, DynamicComponentRenderer],
  bootstrap: [AppComponent]
})
export class AppModule {}`
查看更多
忆尘夕之涩
4楼-- · 2018-12-31 01:14

EDIT - related to 2.3.0 (2016-12-07)

NOTE: to get solution for previous version, check the history of this post

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

In a Nutshell

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

The principal is:
1) create Template
2) find ComponentFactory in cache - go to 7)
3) - create Component
4) - create Module
5) - compile Module
6) - return (and cache for later use) ComponentFactory
7) use Target and ComponentFactory to create an Instance of dynamic Component

Here is a code snippet (more of it here) - Our custom Builder is returning just built/cached ComponentFactory and the view Target placeholder consume to create an instance of the DynamicComponent

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

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

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

This is it - in nutshell it. To get more details.. read below

.

TL&DR

Observe a plunker and come back to read details in case some snippet requires more explanation

.

Detailed explanation - Angular2 RC6++ & runtime components

Below description of this scenario, we will

  1. create a module PartsModule:NgModule (holder of small pieces)
  2. create another module DynamicModule:NgModule, which will contain our dynamic component (and reference PartsModule dynamically)
  3. create dynamic Template (simple approach)
  4. create new Component type (only if template has changed)
  5. create new RuntimeModule:NgModule. This module will contain the previously created Component type
  6. call JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule) to get ComponentFactory
  7. create an Instance of the DynamicComponent - job of the View Target placeholder and ComponentFactory
  8. assign @Inputs to new instance (switch from INPUT to TEXTAREA editing), consume @Outputs

NgModule

We need an NgModules.

While I would like to show a very simple example, in this case, I would need three modules (in fact 4 - but I do not count the AppModule). Please, take this rather than a simple snippet as a basis for a really solid dynamic component generator.

There will be one module for all small components, e.g. string-editor, text-editor (date-editor, number-editor...)

@NgModule({
  imports:      [ 
      CommonModule,
      FormsModule
  ],
  declarations: [
      DYNAMIC_DIRECTIVES
  ],
  exports: [
      DYNAMIC_DIRECTIVES,
      CommonModule,
      FormsModule
  ]
})
export class PartsModule { }

Where DYNAMIC_DIRECTIVES are extensible and are intended to hold all small parts used for our dynamic Component template/type. Check app/parts/parts.module.ts

The second will be module for our Dynamic stuff handling. It will contain hosting components and some providers.. which will be singletons. Therefor we will publish them standard way - with forRoot()

import { DynamicDetail }          from './detail.view';
import { DynamicTypeBuilder }     from './type.builder';
import { DynamicTemplateBuilder } from './template.builder';

@NgModule({
  imports:      [ PartsModule ],
  declarations: [ DynamicDetail ],
  exports:      [ DynamicDetail],
})

export class DynamicModule {

    static forRoot()
    {
        return {
            ngModule: DynamicModule,
            providers: [ // singletons accross the whole app
              DynamicTemplateBuilder,
              DynamicTypeBuilder
            ], 
        };
    }
}

Check the usage of the forRoot() in the AppModule

Finally, we will need an adhoc, runtime module.. but that will be created later, as a part of DynamicTypeBuilder job.

The forth module, application module, is the one who keeps declares compiler providers:

...
import { COMPILER_PROVIDERS } from '@angular/compiler';    
import { AppComponent }   from './app.component';
import { DynamicModule }    from './dynamic/dynamic.module';

@NgModule({
  imports:      [ 
    BrowserModule,
    DynamicModule.forRoot() // singletons
  ],
  declarations: [ AppComponent],
  providers: [
    COMPILER_PROVIDERS // this is an app singleton declaration
  ],

Read (do read) much more about NgModule there:

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

A ComponentFactory builder

Very important thing here is to keep in mind:

our component type, build with our DynamicTypeBuilder, could differ - but only by its template (created 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

So, we are touching the core of our solution. The Builder, will 1) create ComponentType 2) create its NgModule 3) compile ComponentFactory 4) cache it for later reuse.

An dependency we need to receive:

// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';

@Injectable()
export class DynamicTypeBuilder {

  // wee need Dynamic component builder
  constructor(
    protected compiler: JitCompiler
  ) {}

And here is a snippet how to get a ComponentFactory:

// plunker - app/dynamic/type.builder.ts
// this object is singleton - so we can use this as a cache
private _cacheOfFactories:
     {[templateKey: string]: ComponentFactory<IHaveDynamicData>} = {};

public createComponentFactory(template: string)
    : Promise<ComponentFactory<IHaveDynamicData>> {    
    let factory = this._cacheOfFactories[template];

    if (factory) {
        console.log("Module and Type are returned from cache")

        return new Promise((resolve) => {
            resolve(factory);
        });
    }

    // unknown template ... let's create a Type for it
    let type   = this.createNewComponent(template);
    let module = this.createComponentModule(type);

    return new Promise((resolve) => {
        this.compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                factory = _.find(moduleWithFactories.componentFactories
                                , { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });
}

Above we create and cache both Component and Module. Because if the template (in fact the real dynamic part of that all) is the same.. we can reuse

And here are two methods, which represent the really cool way how to create a decorated classes/types in runtime. Not only @Component but also the @NgModule

protected createNewComponent (tmpl:string) {
  @Component({
      selector: 'dynamic-component',
      template: tmpl,
  })
  class CustomDynamicComponent  implements IHaveDynamicData {
      @Input()  public entity: any;
  };
  // a component for this particular template
  return CustomDynamicComponent;
}
protected createComponentModule (componentType: any) {
  @NgModule({
    imports: [
      PartsModule, // there are 'text-editor', 'string-editor'...
    ],
    declarations: [
      componentType
    ],
  })
  class RuntimeComponentModule
  {
  }
  // a module for just this Type
  return RuntimeComponentModule;
}

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.

ComponentFactory 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 ComponentFactory 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,OnChanges,SimpleChange} from '@angular/core';

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

@Component({
  selector: 'dynamic-detail',
  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, OnChanges, OnDestroy, OnInit
{ 
    // wee need Dynamic component builder
    constructor(
        protected typeBuilder: DynamicTypeBuilder,
        protected templateBuilder: DynamicTemplateBuilder
    ) {}
    ...

We just receive, template and component builders. Next are 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.

Finally we will use our component builder, and its just compiled/cached ComponentFacotry. Our Target placeholder will be asked to instantiate the Component with that factory.

protected refreshContent(useTextarea: boolean = false){

  if (this.componentRef) {
      this.componentRef.destroy();
  }

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

  // here we get Factory (just compiled or from cache)
  this.typeBuilder
      .createComponentFactory(template)
      .then((factory: ComponentFactory<IHaveDynamicData>) =>
    {
        // Target will instantiate and inject component (we'll keep reference to it)
        this.componentRef = this
            .dynamicComponentTarget
            .createComponent(factory);

        // let's inject @Inputs to component instance
        let component = this.componentRef.instance;

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

small extension

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

done

That is pretty much it. Do not forget to Destroy anything what was built dynamically (ngOnDestroy). Also, be sure to cache dynamic types and modules if the only difference is their template.

Check it all in action here

to see previous versions (e.g. RC5 related) of this post, check the history

查看更多
十年一品温如言
5楼-- · 2018-12-31 01:14

I have a simple example to show how to do angular 2 rc6 dynamic component.

Say, you have a dynamic html template = template1 and want to dynamic load, firstly wrap into component

@Component({template: template1})
class DynamicComponent {}

here template1 as html, may be contains ng2 component

From rc6, have to have @NgModule wrap this component. @NgModule, just like module in anglarJS 1, it decouple different part of ng2 application, so:

@Component({
  template: template1,

})
class DynamicComponent {

}
@NgModule({
  imports: [BrowserModule,RouterModule],
  declarations: [DynamicComponent]
})
class DynamicModule { }

(Here import RouterModule as in my example there is some route components in my html as you can see later on)

Now you can compile DynamicModule as: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

And we need put above in app.moudule.ts to load it, please see my app.moudle.ts. For more and full details check: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts and app.moudle.ts

and see demo: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview

查看更多
像晚风撩人
6楼-- · 2018-12-31 01:14

Solved this in Angular 2 Final version simply by using the dynamicComponent directive from ng-dynamic.

Usage:

<div *dynamicComponent="template; context: {text: text};"></div>

Where template is your dynamic template and context can be set to any dynamic datamodel that you want your template to bind to.

查看更多
心情的温度
7楼-- · 2018-12-31 01:14

I myself am trying to see how can I update RC4 to RC5 and thus I stumbled upon this entry and new approach to dynamic component creation still holds a bit of mystery to me, so I wont suggest anything on component factory resolver.

But, what I can suggest is a bit clearer approach to component creation on this scenario - just use switch in template that would create string editor or text editor according to some condition, like this:

<form [ngSwitch]="useTextarea">
    <string-editor *ngSwitchCase="false" propertyName="'code'" 
                 [entity]="entity"></string-editor>
    <text-editor *ngSwitchCase="true" propertyName="'code'" 
                 [entity]="entity"></text-editor>
</form>

And by the way, "[" in [prop] expression have a meaning, this indicates one way data binding, hence you can and even should omit those in case if you know that you do not need to bind property to variable.

查看更多
登录 后发表回答