How can I use/create dynamic template to compile dynamic Component with Angular 2.0?

后端 未结 15 1882
忘掉有多难
忘掉有多难 2020-11-21 05:14

I want to dynamically create a template. This should be used to build a ComponentType at runtime and place (even replace) it somewhere inside of the ho

相关标签:
15条回答
  • 2020-11-21 06:03

    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 {}`
    
    0 讨论(0)
  • 2020-11-21 06:04

    2019 June answer

    Great news! It seems that the @angular/cdk package now has first-class support for portals!

    As of the time of writing, I didn't find the above official docs particularly helpful (particularly with regard to sending data into and receiving events from the dynamic components). In summary, you will need to:

    Step 1) Update your AppModule

    Import PortalModule from the @angular/cdk/portal package and register your dynamic component(s) inside entryComponents

    @NgModule({
      declarations: [ ..., AppComponent, MyDynamicComponent, ... ]
      imports:      [ ..., PortalModule, ... ],
      entryComponents: [ ..., MyDynamicComponent, ... ]
    })
    export class AppModule { }
    

    Step 2. Option A: If you do NOT need to pass data into and receive events from your dynamic components:

    @Component({
      selector: 'my-app',
      template: `
        <button (click)="onClickAddChild()">Click to add child component</button>
        <ng-template [cdkPortalOutlet]="myPortal"></ng-template>
      `
    })
    export class AppComponent  {
      myPortal: ComponentPortal<any>;
      onClickAddChild() {
        this.myPortal = new ComponentPortal(MyDynamicComponent);
      }
    }
    
    @Component({
      selector: 'app-child',
      template: `<p>I am a child.</p>`
    })
    export class MyDynamicComponent{
    }
    

    See it in action

    Step 2. Option B: If you DO need to pass data into and receive events from your dynamic components:

    // A bit of boilerplate here. Recommend putting this function in a utils 
    // file in order to keep your component code a little cleaner.
    function createDomPortalHost(elRef: ElementRef, injector: Injector) {
      return new DomPortalHost(
        elRef.nativeElement,
        injector.get(ComponentFactoryResolver),
        injector.get(ApplicationRef),
        injector
      );
    }
    
    @Component({
      selector: 'my-app',
      template: `
        <button (click)="onClickAddChild()">Click to add random child component</button>
        <div #portalHost></div>
      `
    })
    export class AppComponent {
    
      portalHost: DomPortalHost;
      @ViewChild('portalHost') elRef: ElementRef;
    
      constructor(readonly injector: Injector) {
      }
    
      ngOnInit() {
        this.portalHost = createDomPortalHost(this.elRef, this.injector);
      }
    
      onClickAddChild() {
        const myPortal = new ComponentPortal(MyDynamicComponent);
        const componentRef = this.portalHost.attach(myPortal);
        setTimeout(() => componentRef.instance.myInput 
          = '> This is data passed from AppComponent <', 1000);
        // ... if we had an output called 'myOutput' in a child component, 
        // this is how we would receive events...
        // this.componentRef.instance.myOutput.subscribe(() => ...);
      }
    }
    
    @Component({
      selector: 'app-child',
      template: `<p>I am a child. <strong>{{myInput}}</strong></p>`
    })
    export class MyDynamicComponent {
      @Input() myInput = '';
    }
    

    See it in action

    0 讨论(0)
  • 2020-11-21 06:04

    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.

    0 讨论(0)
提交回复
热议问题