ฉันจะใช้ / สร้างแม่แบบไดนามิกเพื่อรวบรวมส่วนประกอบแบบไดนามิกด้วย Angular 2.0 ได้อย่างไร


197

ฉันต้องการสร้างเทมเพลตแบบไดนามิก สิ่งนี้ควรใช้เพื่อสร้างComponentTypeที่รันไทม์และสถานที่(แม้แทนที่)ใดที่หนึ่งภายในคอมโพเนนต์การโฮสต์

จนกระทั่ง RC4 ฉันใช้ComponentResolverแต่ด้วย RC5 ฉันได้รับข้อความต่อไปนี้:

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.

ฉันพบเอกสารนี้ ( การสร้างชิ้นส่วนแบบไดนามิกเชิงมุม 2) )

และเข้าใจว่าฉันสามารถใช้อย่างใดอย่างหนึ่ง

  • ชนิดของแบบไดนามิกด้วยngIf ComponentFactoryResolverถ้าฉันผ่านส่วนประกอบที่รู้จักภายใน@Component({entryComponents: [comp1, comp2], ...}) - ฉันสามารถใช้.resolveComponentFactory(componentToRender);
  • การรวบรวมรันไทม์จริงด้วยCompiler...

แต่คำถามคือวิธีการใช้งานCompilerอย่างไร หมายเหตุข้างต้นบอกว่าฉันควรจะโทร:Compiler.compileComponentSync/Async - ดังนั้นได้อย่างไร

ตัวอย่างเช่น. ฉันต้องการสร้าง(ตามเงื่อนไขการกำหนดค่า)เทมเพลตชนิดนี้สำหรับการตั้งค่าประเภทหนึ่ง

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

และในกรณีอื่นอันนี้( string-editorถูกแทนที่ด้วยtext-editor)

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

และอื่น ๆ(ที่แตกต่างกันจำนวน / วันที่ / อ้างอิงeditorsตามประเภทสถานที่ให้บริการข้ามคุณสมบัติบางอย่างสำหรับผู้ใช้บางคน ... ) นั่นคือตัวอย่างการกำหนดค่าจริงสามารถสร้างเทมเพลตที่ซับซ้อนและแตกต่างได้มากขึ้น

แม่แบบที่มีการเปลี่ยนแปลงดังนั้นผมจึงไม่สามารถใช้ComponentFactoryResolverและผ่านที่มีอยู่ ... Compilerผมต้องแก้ปัญหาด้วย


เนื่องจากวิธีการแก้ปัญหาที่ฉันพบนั้นดีมากฉันต้องการให้ทุกคนค้นหาคำถามนี้เพื่อดูคำตอบของฉันซึ่งไกลมากที่ด้านล่างสุดในขณะนี้ :)
Richard Houltz

บทความนี่คือสิ่งที่คุณต้องรู้เกี่ยวกับส่วนประกอบแบบไดนามิกใน Angularมีคำอธิบายที่ดีของส่วนประกอบแบบไดนามิก
Max Koretskyi

นี่คือปัญหาที่เกิดขึ้นกับคำตอบเดียวทุกข้อและสิ่งที่$compileสามารถทำได้จริง ๆ แล้ววิธีการเหล่านี้ไม่สามารถทำได้ - ฉันกำลังสร้างแอปพลิเคชันที่ฉันต้องการคอมไพล์ HTML เนื่องจากมาผ่านหน้าเพจของบุคคลที่สามและการโทร ajax ฉันไม่สามารถลบ HTML ออกจากหน้าและวางไว้ในเทมเพลตของฉันเอง เฮ้อ
Augie Gardner

@AugieGardner มีสาเหตุที่เป็นไปไม่ได้จากการออกแบบ เชิงมุมไม่ใช่ความผิดสำหรับการตัดสินใจด้านสถาปัตยกรรมที่ไม่ดีหรือระบบดั้งเดิมที่บางคนมี หากคุณต้องการแยกโค้ด HTML ที่มีอยู่คุณสามารถใช้เฟรมเวิร์กอื่นได้เนื่องจาก Angular สามารถทำงานได้ดีกับ WebComponents การกำหนดขอบเขตที่ชัดเจนเพื่อชี้แนะพยุหะของโปรแกรมเมอร์มือใหม่นั้นสำคัญกว่าการยอมให้แฮ็คสกปรกสำหรับระบบรุ่นเก่า
Phil

คำตอบ:


163

แก้ไข - เกี่ยวข้องกับ2.3.0 (2016-12-07)

หมายเหตุ: เพื่อรับวิธีแก้ปัญหาสำหรับรุ่นก่อนหน้าตรวจสอบประวัติของโพสต์นี้

หัวข้อที่คล้ายกันจะกล่าวถึงที่นี่เทียบเท่าของ $ รวบรวมในเชิงมุม 2 เราจำเป็นต้องใช้และJitCompiler NgModuleอ่านเพิ่มเติมเกี่ยวกับNgModuleใน Angular2 ที่นี่:

โดยสังเขป

มีพลั่วเกอร์ / ตัวอย่างการทำงาน (แม่แบบไดนามิก, ประเภทองค์ประกอบแบบไดนามิก, โมดูลแบบไดนามิก,JitCompiler ,, ... กำลังดำเนินการ)

หลักการคือ:
1)สร้างเทมเพลต
2)ค้นหาComponentFactoryในแคช - ไปที่7)
3) - สร้างComponent
4) - สร้างModule
5) - รวบรวม 5) - รวบรวมModule
6) - ส่งคืน (และแคชเพื่อใช้ในภายหลัง) ComponentFactory
7)ใช้เป้าหมายและComponentFactoryสร้างอินสแตนซ์ ของแบบไดนามิกComponent

นี่คือข้อมูลโค้ด(เพิ่มเติมจากที่นี่ ) - เครื่องมือสร้างแบบกำหนดเองของเราจะส่งคืนเพียงสร้าง / แคชComponentFactoryและมุมมองตัวยึดตำแหน่งเป้าหมายใช้เพื่อสร้างอินสแตนซ์ของ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;
        //...
    });

นี่มัน - สรุปมัน เพื่อรับรายละเอียดเพิ่มเติม .. อ่านด้านล่าง

.

TL & DR

สังเกตผู้สังเกตการณ์และกลับมาอ่านรายละเอียดในกรณีที่ข้อมูลบางส่วนต้องการคำอธิบายเพิ่มเติม

.

คำอธิบายโดยละเอียด - Angular2 RC6 ++ และส่วนประกอบรันไทม์

ด้านล่างคำอธิบายของสถานการณ์นี้เราจะ

  1. สร้างโมดูลPartsModule:NgModule (ผู้ถือชิ้นเล็ก ๆ )
  2. สร้างโมดูลอื่นDynamicModule:NgModuleซึ่งจะมีองค์ประกอบแบบไดนามิกของเรา(และการอ้างอิงPartsModuleแบบไดนามิก)
  3. สร้างเทมเพลตแบบไดนามิก(วิธีการง่าย ๆ )
  4. สร้างComponentประเภทใหม่ (เฉพาะในกรณีที่มีการเปลี่ยนแปลงแม่แบบ)
  5. RuntimeModule:NgModuleสร้างใหม่ โมดูลนี้จะมีComponentประเภทที่สร้างไว้ก่อนหน้านี้
  6. โทรJitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)ไปรับComponentFactory
  7. สร้างอินสแตนซ์ของDynamicComponent- งานของตัวแทนเป้าหมายดูและComponentFactory
  8. กำหนดให้@Inputsกับอินสแตนซ์ใหม่ (เปลี่ยนจากINPUTเป็นTEXTAREAแก้ไข)กิน@Outputs

NgModule

เราต้องการNgModuleเอส

ในขณะที่ฉันต้องการจะแสดงตัวอย่างที่ง่ายมากในกรณีนี้ผมจะต้องสามโมดูล(ในความเป็นจริง 4 - แต่ฉันไม่นับ AppModule) โปรดใช้สิ่งนี้แทนที่จะเป็นตัวอย่างง่ายๆเป็นพื้นฐานสำหรับเครื่องกำเนิดส่วนประกอบแบบไดนามิกที่แข็งแกร่งจริงๆ

จะมีหนึ่งโมดูลสำหรับส่วนประกอบเล็ก ๆ เช่นstring-editor, text-editor ( date-editor, number-editor... )

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

ที่ไหนDYNAMIC_DIRECTIVESมีขยายและมีจุดมุ่งหมายที่จะถือชิ้นส่วนขนาดเล็กทั้งหมดใช้สำหรับแบบไดนามิกของเราตัวแทนแม่แบบ / ประเภท ตรวจสอบแอพ / ชิ้นส่วน / parts.module.ts

ที่สองจะเป็นโมดูลสำหรับการจัดการสิ่งแบบไดนามิกของเรา มันจะมีส่วนประกอบของโฮสต์และผู้ให้บริการบางราย .. ซึ่งจะเป็นซิงเกิล ดังนั้นเราจะเผยแพร่วิธีมาตรฐาน - ด้วย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
            ], 
        };
    }
}

ตรวจสอบการใช้งานของforRoot()ในAppModule

สุดท้ายเราจะต้องใช้ adhoc โมดูลรันไทม์ .. แต่มันจะถูกสร้างขึ้นในภายหลังเป็นส่วนหนึ่งของDynamicTypeBuilderงาน

โมดูลที่สี่คือโมดูลที่เป็นผู้ประกาศผู้ให้บริการคอมไพเลอร์:

...
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
  ],

อ่าน(อ่าน)เพิ่มเติมเกี่ยวกับNgModule ที่นั่น:

เครื่องมือสร้างเทมเพลต

ในตัวอย่างของเราเราจะประมวลผลรายละเอียดของเอนทิตีประเภทนี้

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

ในการสร้างtemplateในการนี้plunkerเราใช้แบบนี้ / สร้างที่ไร้เดียงสา

โซลูชันที่แท้จริงซึ่งเป็นตัวสร้างเทมเพลตจริงเป็นสถานที่ที่แอปพลิเคชันของคุณสามารถทำได้มาก

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

เคล็ดลับที่นี่คือ - entityมันสร้างแม่แบบซึ่งใช้ชุดของคุณสมบัติที่รู้จักกันบางอย่างเช่น คุณสมบัติดังกล่าว (-ies) จะต้องเป็นส่วนหนึ่งขององค์ประกอบแบบไดนามิกซึ่งเราจะสร้างต่อไป

เพื่อให้ง่ายขึ้นอีกเล็กน้อยเราสามารถใช้อินเทอร์เฟซเพื่อกำหนดคุณสมบัติซึ่งตัวสร้างเทมเพลตของเราสามารถใช้ได้ สิ่งนี้จะถูกนำไปใช้โดยประเภทส่วนประกอบแบบไดนามิกของเรา

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

ComponentFactoryสร้าง

สิ่งที่สำคัญมากที่นี่คือการจำ:

ประเภทส่วนประกอบของเราสร้างกับเราDynamicTypeBuilderอาจจะแตกต่างกัน - แต่โดยแม่แบบของมัน(ที่สร้างขึ้นด้านบน) คุณสมบัติของส่วนประกอบ(อินพุตเอาต์พุตหรือการป้องกันบางอย่าง ) ยังคงเหมือนเดิม หากเราต้องการคุณสมบัติที่แตกต่างเราควรกำหนดชุดค่าผสมที่แตกต่างกันของแม่แบบและตัวสร้างประเภท

ดังนั้นเรากำลังสัมผัสกับแกนหลักของการแก้ปัญหาของเรา ตัวสร้างจะ 1) สร้างComponentType2) สร้างNgModule3) คอมไพล์ComponentFactory4) แคชเพื่อใช้ในภายหลัง

การพึ่งพาที่เราต้องได้รับ:

// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';
    
@Injectable()
export class DynamicTypeBuilder {

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

และนี่คือตัวอย่างข้อมูลวิธีรับ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);
            });
    });
}

ดังกล่าวข้างต้นที่เราสร้างและแคชทั้งสองและComponent Moduleเพราะถ้าแม่แบบ(อันที่จริงส่วนที่แท้จริงของทั้งหมดนั้น)เหมือนกัน .. เราสามารถนำกลับมาใช้ใหม่ได้

และนี่คือสองวิธีซึ่งเป็นวิธีที่ยอดเยี่ยมจริงๆในการสร้างคลาส / ประเภทการตกแต่งในรันไทม์ ไม่เพียง@Componentแต่ยัง@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;
}

สำคัญ:

ชนิดไดนามิกองค์ประกอบของเราแตกต่างกัน แต่เพียงแค่เทมเพลต ดังนั้นเราจึงใช้ข้อเท็จจริงนั้นเพื่อแคชพวกเขา สิ่งนี้สำคัญมากจริงๆ Angular2 ยังจะแคชเหล่านี้ .. โดยประเภท และถ้าเราจะสร้างเทมเพลตสตริงชนิดใหม่ขึ้นมาใหม่ ... เราจะเริ่มสร้างหน่วยความจำรั่ว

ComponentFactory ใช้โดยองค์ประกอบโฮสต์

<div #dynamicContentPlaceHolder></div>ชิ้นสุดท้ายเป็นส่วนประกอบซึ่งเป็นเจ้าภาพเป้าหมายสำหรับองค์ประกอบแบบไดนามิกของเราเช่น เราได้รับการอ้างอิงถึงมันและใช้ComponentFactoryในการสร้างองค์ประกอบ สั้นและนี่คือชิ้นส่วนทั้งหมดของชิ้นส่วนนั้น(ถ้าจำเป็นให้เปิดพลั่วเกอร์ที่นี่ )

ก่อนอื่นเรามาสรุปคำสั่งการนำเข้า:

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
    ) {}
    ...

เราเพิ่งได้รับผู้สร้างเทมเพลตและส่วนประกอบ ถัดไปคือคุณสมบัติที่จำเป็นสำหรับตัวอย่างของเรา(เพิ่มเติมในความคิดเห็น)

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

@Inputในสถานการณ์สมมตินี้ง่ายส่วนประกอบโฮสติ้งของเราไม่ได้มี ดังนั้นจึงไม่ต้องตอบสนองต่อการเปลี่ยนแปลง แต่อย่างไรก็ตามข้อเท็จจริงนั้น(และเพื่อเตรียมพร้อมสำหรับการเปลี่ยนแปลงที่กำลังจะมา) - เราจำเป็นต้องแนะนำการตั้งค่าสถานะบางอย่างถ้าองค์ประกอบนั้นมีอยู่แล้ว(ตอนแรก)เริ่มต้นแล้ว และหลังจากนั้นเราก็สามารถเริ่มเวทมนตร์ได้

ในที่สุดเราจะใช้สร้างองค์ประกอบของเราและมันเป็นเพียงแค่การรวบรวม / แคช ComponentFacotryเรายึดเป้าหมายจะถูกขอให้ยกตัวอย่างกับโรงงานที่Component

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;
        //...
    });
}

นามสกุลเล็ก

นอกจากนี้เราจำเป็นต้องอ้างอิงถึงเทมเพลตที่รวบรวม .. เพื่อให้สามารถใช้งานได้อย่างถูกต้องdestroy()ทุกครั้งที่เราจะทำการเปลี่ยนแปลง

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

เสร็จแล้ว

นั่นมันสวยมาก อย่าลืมที่จะทำลายอะไรสิ่งที่ถูกสร้างขึ้นแบบไดนามิก(ngOnDestroy) นอกจากนี้ตรวจสอบให้แน่ใจว่าแคชแบบไดนามิกtypesและmodulesความแตกต่างเพียงอย่างเดียวคือแม่แบบของพวกเขา

ตรวจสอบการทำงานทั้งหมดที่นี่

หากต้องการดูรุ่นก่อนหน้า(เช่น RC5 ที่เกี่ยวข้อง)ของโพสต์นี้ให้ตรวจสอบประวัติ


50
ดูเหมือนว่าวิธีการแก้ปัญหาที่ซับซ้อนแบบนี้เลิกใช้ง่ายและชัดเจนมีวิธีอื่นในการทำเช่นนี้หรือไม่?
tibbus

3
ฉันคิดว่าวิธีเดียวกันกับ @tibbus: วิธีนี้มีความซับซ้อนมากกว่าที่เคยเป็นกับรหัสที่เลิกใช้แล้ว ขอบคุณสำหรับคำตอบของคุณ
Lucio Mollinedo

5
@ribsies ขอบคุณสำหรับบันทึกของคุณ ผมขออธิบายบางอย่าง คำตอบอื่น ๆ หลายคนพยายามที่จะทำให้มันง่าย แต่ฉันพยายามที่จะอธิบายและแสดงในสถานการณ์ที่จะปิดการใช้งานจริง เราจะต้องทำการแคชสิ่งต่าง ๆ เราจะต้องเรียกใช้การทำลายในการสร้างใหม่เป็นต้นดังนั้นในขณะที่ความมหัศจรรย์ของการสร้างแบบไดนามิกนั้นtype.builder.tsเป็นไปตามที่คุณต้องการ บริบท ... หวังว่ามันจะมีประโยชน์;)
Radim Köhler

7
@Radim Köhler - ฉันได้ลองตัวอย่างนี้แล้ว มันทำงานโดยไม่มีทอท. แต่เมื่อฉันพยายามรันด้วย AOT จะแสดงข้อผิดพลาด "ไม่พบเมตาดาต้า NgModule สำหรับ RuntimeComponentModule" คุณช่วยฉันแก้ไขข้อผิดพลาดนี้ได้ไหม
Trusha

4
คำตอบนั้นสมบูรณ์แบบ! แต่สำหรับการใช้งานในชีวิตจริงไม่สามารถทำได้ ทีมงานเชิงมุมควรให้คำตอบสำหรับสิ่งนี้ในกรอบงานเนื่องจากเป็นข้อกำหนดทั่วไปในการใช้งานทางธุรกิจ หากไม่จำเป็นต้องถามว่า Angular 2 เป็นแพลตฟอร์มที่เหมาะสมสำหรับแอปพลิเคชันทางธุรกิจหรือไม่
Karl

58

แก้ไข (26/08/2560) : วิธีแก้ปัญหาด้านล่างใช้ได้ดีกับ Angular2 และ 4 ฉันได้อัปเดตให้มีตัวแปรเทมเพลตแล้วคลิกตัวจัดการคลิกและทดสอบด้วย Angular 4.3
สำหรับ Angular4, ngComponentOutlet ดังที่อธิบายไว้ในคำตอบของ Ophirเป็นทางออกที่ดีกว่ามาก แต่ตอนนี้มันไม่สนับสนุนปัจจัยการผลิตและผลเลย ถ้า [PR นี้] ( https://github.com/angular/angular/pull/15362]เป็นที่ยอมรับมันจะเป็นไปได้ผ่านอินสแตนซ์ส่วนประกอบที่ส่งคืนโดยการสร้างเหตุการณ์
ng-dynamic-componentอาจดีที่สุดและง่ายที่สุด วิธีแก้ปัญหาทั้งหมด แต่ฉันยังไม่ได้ทดสอบ

@Long Field คำตอบคือจุดที่! นี่คืออีกตัวอย่าง (ซิงโครนัส):

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

@Component({
  selector: 'my-app',
  template: `<h1>Dynamic template:</h1>
             <div #container></div>`
})
export class App implements OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(private compiler: Compiler) {}

  ngOnInit() {
    this.addComponent(
      `<h4 (click)="increaseCounter()">
        Click to increase: {{counter}}
      `enter code here` </h4>`,
      {
        counter: 1,
        increaseCounter: function () {
          this.counter++;
        }
      }
    );
  }

  private addComponent(template: string, properties?: any = {}) {
    @Component({template})
    class TemplateComponent {}

    @NgModule({declarations: [TemplateComponent]})
    class TemplateModule {}

    const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === TemplateComponent
    );
    const component = this.container.createComponent(factory);
    Object.assign(component.instance, properties);
    // If properties are changed at a later stage, the change detection
    // may need to be triggered manually:
    // component.changeDetectorRef.detectChanges();
  }
}

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

อยู่ที่http://plnkr.co/edit/fdP9Oc


3
ผมบอกว่ามันเป็นตัวอย่างวิธีการเขียนรหัสน้อยที่เป็นไปได้ที่จะทำเช่นเดียวกับในคำตอบของฉัน stackoverflow.com/a/38888009/1679310 ในกรณีที่มันควรจะเป็นกรณีที่มีประโยชน์(ส่วนใหญ่สร้างแม่แบบ RE)เมื่อเงื่อนไขการเปลี่ยนแปลง ... ง่ายngAfterViewInitโทรด้วย a const templateจะไม่ทำงาน แต่ถ้างานของคุณคือลดวิธีที่อธิบายไว้ข้างต้นโดยละเอียด(สร้างเทมเพลตสร้างคอมโพเนนต์สร้างโมดูลคอมไพล์สร้างโรงงาน .. สร้างอินสแตนซ์) ... คุณน่าจะทำได้
Radim Köhler

ขอบคุณสำหรับการแก้ปัญหา: ฉันมีปัญหาในการโหลด templateUrl และรูปแบบแม้ว่าฉันได้รับข้อผิดพลาดต่อไปนี้: ไม่มีการใช้งาน ResourceLoader ไม่สามารถอ่าน url localhost: 3000 / แอพ / หน้า / pages_common.cssความคิดใดที่ฉันหายไป?
Gerardlamo

เป็นไปได้ไหมที่จะรวบรวมเทมเพลต html ด้วยข้อมูลเฉพาะสำหรับเซลล์ในกริดเช่นการควบคุม plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=previewในตัวเรียงเสียงนี้ฉันจะรวบรวมและแสดงภาพในคอลัมน์สุดท้ายได้อย่างไร ความช่วยเหลือใด ๆ
Karthick

1
@monnef คุณพูดถูก ฉันไม่ได้ตรวจสอบบันทึกของคอนโซล ฉันได้ปรับรหัสเพื่อเพิ่มส่วนประกอบใน ngOnInit แทนที่จะเป็นเบ็ด ngAfterViewInit เนื่องจากตัวก่อนจะถูกเรียกใช้ก่อนและหลังหลังการตรวจจับการเปลี่ยนแปลง (ดูgithub.com/angular/angular/issues/10131และหัวข้อที่คล้ายกัน)
Rene Hamburger

1
เรียบร้อยและเรียบง่าย ทำงานตามที่คาดไว้เมื่อแสดงผ่านเบราว์เซอร์ในอุปกรณ์ แต่สิ่งนี้ใช้ได้กับ AOT หรือไม่ เมื่อแอพทำงานใน PROD หลังจากการคอมไพล์ฉันได้รับ "ข้อผิดพลาด: คอมไพเลอร์รันไทม์ไม่โหลด" ในขณะที่พยายามรวบรวมส่วนประกอบ (btw ฉันใช้ Ionic 3.5)
mymo

52

ฉันต้องมาถึงงานปาร์ตี้ช้าไม่มีวิธีแก้ปัญหาที่นี่ดูเหมือนจะมีประโยชน์สำหรับฉัน - ยุ่งมากและรู้สึกเหมือนเป็นการแก้ปัญหามากเกินไป

สิ่งที่ฉันสิ้นสุดที่ทำคือการใช้Angular 4.0.0-beta.6's ngComponentOutlet

นี่เป็นวิธีแก้ปัญหาที่สั้นและง่ายที่สุดที่ฉันเขียนในไฟล์ขององค์ประกอบแบบไดนามิก

  • นี่คือตัวอย่างง่ายๆที่เพิ่งได้รับข้อความและวางไว้ในแม่แบบ แต่แน่นอนคุณสามารถเปลี่ยนแปลงได้ตามความต้องการของคุณ:
import {
  Component, OnInit, Input, NgModule, NgModuleFactory, Compiler
} from '@angular/core';

@Component({
  selector: 'my-component',
  template: `<ng-container *ngComponentOutlet="dynamicComponent;
                            ngModuleFactory: dynamicModule;"></ng-container>`,
  styleUrls: ['my.component.css']
})
export class MyComponent implements OnInit {
  dynamicComponent;
  dynamicModule: NgModuleFactory<any>;

  @Input()
  text: string;

  constructor(private compiler: Compiler) {
  }

  ngOnInit() {
    this.dynamicComponent = this.createNewComponent(this.text);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));
  }

  protected createComponentModule (componentType: any) {
    @NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
  }

  protected createNewComponent (text:string) {
    let template = `dynamically created template with text: ${text}`;

    @Component({
      selector: 'dynamic-component',
      template: template
    })
    class DynamicComponent implements OnInit{
       text: any;

       ngOnInit() {
       this.text = text;
       }
    }
    return DynamicComponent;
  }
}
  • คำอธิบายสั้น ๆ :
    1. my-component - ส่วนประกอบที่แสดงองค์ประกอบแบบไดนามิก
    2. DynamicComponent - องค์ประกอบที่จะสร้างขึ้นแบบไดนามิกและมันจะแสดงผลภายในองค์ประกอบของฉัน

อย่าลืมอัพเกรดไลบรารีเชิงมุมทั้งหมดเป็น ^ Angular 4.0.0

หวังว่านี่จะช่วยโชคดี!

UPDATE

ใช้ได้กับเชิงมุม 5 ด้วย


3
สิ่งนี้ใช้ได้ผลดีมากสำหรับฉันกับ Angular4 การปรับเท่านั้นที่ฉันต้องทำคือสามารถระบุโมดูลนำเข้าสำหรับ RuntimeComponentModule ที่สร้างขึ้นแบบไดนามิก
ราหุลปาเทล

8
นี่คือตัวอย่างด่วนที่เริ่มต้นจาก Angular Quickstart: embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel

5
โซลูชันนี้ใช้งานได้กับ "ng build --prod" หรือไม่ ดูเหมือนว่าคลาสคอมไพเลอร์และ AoT ไม่สอดคล้องกันกับ ATM
Pierre Chavaroche

2
@OphirStern ฉันยังค้นพบว่าวิธีการทำงานได้ดีใน Angular 5 แต่ไม่ใช่ด้วย --prod build flag
TaeKwonJoe

2
ฉันทดสอบด้วยเชิงมุม 5 (5.2.8) โดยใช้ JitCompilerFactory และการใช้แฟล็ก --prod ไม่ทำงาน! ไม่มีใครมีทางออกหรือไม่? (BTW JitCompilerFactory โดยไม่มีการตั้งค่าสถานะ --prod อย่างไม่มีที่ติ)
Frank

20

2561 มิถุนายนตอบ

ข่าวดี! ดูเหมือนว่าแพ็กเกจ@ angular / cdkตอนนี้มีการสนับสนุนชั้นหนึ่งสำหรับพอร์ทัล !

เมื่อถึงเวลาของการเขียนฉันไม่พบเอกสารทางการที่เป็นประโยชน์ข้างต้นโดยเฉพาะ (โดยเฉพาะอย่างยิ่งเกี่ยวกับการส่งข้อมูลเข้าและรับเหตุการณ์จากองค์ประกอบแบบไดนามิก) โดยสรุปคุณจะต้อง:

ขั้นตอนที่ 1) อัปเดตของคุณ AppModule

นำเข้าPortalModuleจาก@angular/cdk/portalแพ็คเกจและลงทะเบียนส่วนประกอบแบบไดนามิกของคุณภายในentryComponents

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

ขั้นตอนที่ 2 ตัวเลือกก: ถ้าคุณไม่ต้องการส่งผ่านข้อมูลและรับเหตุการณ์จากคอมโพเนนต์แบบไดนามิกของคุณ :

@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{
}

เห็นมันในทางปฏิบัติ

ขั้นตอนที่ 2 ตัวเลือก B: หากคุณต้องการส่งผ่านข้อมูลเข้าและรับเหตุการณ์จากองค์ประกอบแบบไดนามิกของคุณ :

// 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 = '';
}

เห็นมันในทางปฏิบัติ


1
เพื่อนคุณเพิ่งถูกจับ คนนี้จะได้รับความสนใจ ฉันไม่อยากจะเชื่อเลยว่าเรื่องยากที่จะแช่งคือการเพิ่มส่วนประกอบไดนามิกอย่างง่ายใน Angular จนกว่าฉันจะต้องทำสิ่งใดสิ่งหนึ่ง มันเหมือนทำการรีเซ็ตและกลับไปสู่เวลาก่อนการ JQuery
Gi1ber7

2
@ Gi1ber7 ฉันรู้ใช่มั้ย ทำไมมันใช้เวลานานขนาดนี้?
สตีเฟ่นพอล

1
วิธีการที่ดี แต่คุณรู้วิธีส่งพารามิเตอร์ไปยัง ChildComponent หรือไม่
Snook

1
@Snook สิ่งนี้อาจตอบคำถามของคุณstackoverflow.com/questions/47469844/…
Stephen Paul

4
@StephenPaul วิธีการนี้Portalแตกต่างจากngTemplateOutletและngComponentOutletอย่างไร 🤔
Glenn Mohammad

18

ฉันตัดสินใจที่จะกระชับทุกสิ่งที่ฉันเรียนรู้เป็นไฟล์เดียว มีหลายสิ่งหลายอย่างที่ต้องทำเมื่อเทียบกับ RC5 โปรดทราบว่าไฟล์ต้นฉบับนี้มี AppModule และ 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 {}`

10

ฉันมีตัวอย่างง่าย ๆ เพื่อแสดงวิธีการทำส่วนประกอบแบบไดนามิก angular 2 rc6

สมมติว่าคุณมีเทมเพลต html แบบไดนามิก = template1 และต้องการโหลดแบบไดนามิกก่อนอื่นให้ห่อส่วนประกอบ

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

template1 นี่เป็น html, อาจมีส่วนประกอบ ng2

จาก rc6 ต้องมี @NgModule ห่อส่วนนี้ @NgModule เช่นเดียวกับโมดูลใน anglarJS 1 มันแยกส่วนต่าง ๆ ของแอปพลิเคชั่น ng2 ดังนั้น:

@Component({
  template: template1,

})
class DynamicComponent {

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

(ที่นี่นำเข้า RouterModule ในตัวอย่างของฉันมีส่วนประกอบเส้นทางบางส่วนใน html ของฉันตามที่คุณเห็นในภายหลัง)

ตอนนี้คุณสามารถรวบรวม DynamicModule เป็น: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

และเราจำเป็นต้องใส่ข้างต้นใน app.moudule.ts เพื่อโหลดโปรดดู app.moudle.ts ของฉัน สำหรับรายละเอียดเพิ่มเติมและตรวจสอบรายละเอียด: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.tsและ app.moudle.ts

และดูตัวอย่าง: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview


3
ดังนั้นคุณได้ประกาศ module1, module2, module3 และถ้าคุณต้องการเนื้อหาเทมเพลต "ไดนามิก" อีกตัวคุณจะต้องสร้างฟอร์ม defintion (ไฟล์) moudle4 (module4.ts) ใช่ไหม? ถ้าใช่นั่นดูเหมือนจะไม่เป็นแบบไดนามิก มันคงที่ใช่มั้ย หรือฉันจะพลาดบางสิ่งบางอย่าง?
Radim Köhler

ในข้างต้น "template1" เป็นสตริงของ html คุณสามารถใส่อะไรลงไปและเราเรียกว่าเทมเพลตแบบไดนามิกนี้ตามที่คำถามนี้ถาม
ฟิลด์ยาว

6

ในเชิงมุม 7.x ฉันใช้องค์ประกอบเชิงมุมสำหรับเรื่องนี้

  1. ติดตั้ง @ angular-elements npm i @ angular / elements -s

  2. สร้างบริการเสริม

import { Injectable, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { IStringAnyMap } from 'src/app/core/models';
import { AppUserIconComponent } from 'src/app/shared';

const COMPONENTS = {
  'user-icon': AppUserIconComponent
};

@Injectable({
  providedIn: 'root'
})
export class DynamicComponentsService {
  constructor(private injector: Injector) {

  }

  public register(): void {
    Object.entries(COMPONENTS).forEach(([key, component]: [string, any]) => {
      const CustomElement = createCustomElement(component, { injector: this.injector });
      customElements.define(key, CustomElement);
    });
  }

  public create(tagName: string, data: IStringAnyMap = {}): HTMLElement {
    const customEl = document.createElement(tagName);

    Object.entries(data).forEach(([key, value]: [string, any]) => {
      customEl[key] = value;
    });

    return customEl;
  }
}

โปรดทราบว่าแท็กองค์ประกอบที่กำหนดเองของคุณจะต้องแตกต่างจากตัวเลือกองค์ประกอบเชิงมุม ใน AppUserIconComponent:

...
selector: app-user-icon
...

และในกรณีนี้ชื่อแท็กที่กำหนดเองฉันใช้ "user-icon"

  1. จากนั้นคุณต้องโทรลงทะเบียนใน AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. และตอนนี้คุณสามารถใช้รหัสนี้ได้ทุกที่:
dynamicComponents.create('user-icon', {user:{...}});

หรือเช่นนี้

const html = `<div class="wrapper"><user-icon class="user-icon" user='${JSON.stringify(rec.user)}'></user-icon></div>`;

this.content = this.domSanitizer.bypassSecurityTrustHtml(html);

(ในเทมเพลต):

<div class="comment-item d-flex" [innerHTML]="content"></div>

โปรดทราบว่าในกรณีที่สองคุณต้องผ่านวัตถุด้วย JSON.stringify และหลังจากนั้นแยกวิเคราะห์อีกครั้ง ฉันหาทางออกที่ดีกว่าไม่ได้


interresting วิธี แต่คุณจะต้องกำหนดเป้าหมาย es2015 (เพื่อให้การสนับสนุนสำหรับ IE11) ใน tsconfig.json ของคุณ othewise มันจะล้มเหลวในdocument.createElement(tagName);
ฮิ้ว

สวัสดี, ตามที่คุณกล่าวถึงวิธีการจัดการอินพุต, ดังนั้นเอาต์พุตของคอมโพเนนต์ลูกสามารถจัดการได้เช่นนี้?
Mustahsan

5

แก้ไขสิ่งนี้ใน Angular 2 Final version โดยใช้คำสั่งdynamicComponentจากng-dynamic NG-แบบไดนามิก

การใช้งาน:

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

โดยที่ template คือเทมเพลตไดนามิกและบริบทของคุณสามารถตั้งค่าเป็น datamodel แบบไดนามิกใด ๆ ที่คุณต้องการให้เทมเพลตของคุณเชื่อมโยง


ในขณะที่เขียน Angular 5 กับ AOT ไม่สนับสนุนสิ่งนี้เนื่องจากคอมไพเลอร์ JIT ไม่รวมอยู่ในชุดข้อมูล โดยไม่ต้องทอทมันทำงานเช่นเสน่ห์ :)
ริชาร์ด Houltz

สิ่งนี้ยังใช้กับเชิงมุม 7+ หรือไม่
Carlos E

4

ฉันต้องการเพิ่มรายละเอียดเล็ก ๆ น้อย ๆ ที่ด้านบนของโพสต์ที่ยอดเยี่ยมนี้โดย Radim

ฉันใช้วิธีแก้ปัญหานี้และทำงานเพื่อแก้ไขข้อ จำกัด บางอย่างและอย่างรวดเร็ว ฉันจะร่างสิ่งเหล่านั้นแล้วให้คำตอบกับสิ่งนั้นเช่นกัน

  • ก่อนอื่นฉันไม่สามารถแสดงรายละเอียดแบบไดนามิกภายในรายละเอียดแบบไดนามิก
  • ปัญหาต่อไปคือฉันต้องการแสดงรายละเอียดแบบไดนามิกในส่วนใดส่วนหนึ่งที่ให้บริการในโซลูชัน ที่เป็นไปไม่ได้ด้วยวิธีแก้ปัญหาเบื้องต้น
  • สุดท้ายไม่สามารถใช้ URL เทมเพลตในส่วนไดนามิกเช่น string-editor

ฉันทำคำถามอื่นตามโพสต์นี้เกี่ยวกับวิธีการบรรลุข้อ จำกัด เหล่านี้ซึ่งสามารถพบได้ที่นี่:

การรวบรวมเทมเพลตแบบไดนามิกซ้ำใน angular2

ฉันจะร่างคำตอบสำหรับข้อ จำกัด เหล่านี้หากคุณพบปัญหาเดียวกันกับฉันเพราะนั่นจะทำให้การแก้ปัญหามีความยืดหยุ่นมากขึ้น มันคงจะยอดเยี่ยมมากถ้าหากมีการอัพเดทครั้งแรกด้วย

ในการเปิดใช้งานการซ้อนรายละเอียดไดนามิกภายในซึ่งกันและกันคุณจะต้องเพิ่ม DynamicModule.forRoot () ในคำสั่งการนำเข้าในtype.builder.ts

protected createComponentModule (componentType: any) {
    @NgModule({
    imports: [
        PartsModule, 
        DynamicModule.forRoot() //this line here
    ],
    declarations: [
        componentType
    ],
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
}

นอกจากนั้นมันเป็นไปไม่ได้ที่จะใช้<dynamic-detail>ภายในส่วนใดส่วนหนึ่งที่เป็นตัวแก้ไขสตริงหรือแก้ไขข้อความ

หากต้องการเปิดใช้งานคุณจะต้องเปลี่ยนparts.module.tsและdynamic.module.ts

ข้างในparts.module.tsคุณจะต้องเพิ่มDynamicDetailในDYNAMIC_DIRECTIVES

export const DYNAMIC_DIRECTIVES = [
   forwardRef(() => StringEditor),
   forwardRef(() => TextEditor),
   DynamicDetail
];

นอกจากนี้ในที่dynamic.module.tsคุณจะต้องลบ dynamicDetail ตามที่ตอนนี้พวกเขาเป็นส่วนหนึ่งของชิ้นส่วน

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

Plunker ดัดแปลงที่ทำงานสามารถพบได้ที่นี่: http://plnkr.co/edit/UYnQHF?p=preview (ฉันไม่ได้แก้ปัญหานี้ฉันเป็นแค่ผู้ส่งสาร :-D)

ในที่สุดมันก็เป็นไปไม่ได้ที่จะใช้ templateurls ในชิ้นส่วนที่สร้างขึ้นบนส่วนประกอบแบบไดนามิก วิธีแก้ปัญหา (หรือวิธีแก้ปัญหาฉันไม่แน่ใจว่ามันเป็นบั๊กเชิงมุมหรือการใช้เฟรมเวิร์กผิด) คือการสร้างคอมไพเลอร์ในตัวสร้างแทนการฉีด

    private _compiler;

    constructor(protected compiler: RuntimeCompiler) {
        const compilerFactory : CompilerFactory =
        platformBrowserDynamic().injector.get(CompilerFactory);
        this._compiler = compilerFactory.createCompiler([]);
    }

จากนั้นใช้การ_compilerคอมไพล์จากนั้น templateUrl จะเปิดใช้งานเช่นกัน

return new Promise((resolve) => {
        this._compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                let _ = window["_"];
                factory = _.find(moduleWithFactories.componentFactories, { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

หวังว่านี่จะช่วยคนอื่นได้!

ขอแสดงความนับถือ Morten


4

การติดตามคำตอบที่ยอดเยี่ยมของ Radmin นั้นมีความจำเป็นเล็กน้อยสำหรับทุกคนที่ใช้แองกูลาร์ - ซีไอเวอร์ชัน 1.0.0-beta.22 ขึ้นไป

COMPILER_PROVIDERSไม่สามารถนำเข้าได้อีกต่อไป (สำหรับรายละเอียดดูangi-cli GitHub )

ดังนั้นการแก้ปัญหาไม่ควรใช้COMPILER_PROVIDERSและJitCompilerในprovidersส่วนเลย แต่ใช้JitCompilerFactoryจาก '@ angular / compiler' แทนเช่นนี้ในคลาส builder type:

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

อย่างที่คุณเห็นมันไม่สามารถฉีดได้และไม่มีการพึ่งพา DI โซลูชันนี้ควรใช้ได้กับโครงการที่ไม่ได้ใช้งานเชิงมุม


1
ขอบคุณสำหรับคำแนะนำนี้ แต่ฉันพบว่า "ไม่พบเมทาดาทา NgModule สำหรับ 'DynamicHtmlModule'" การใช้งานของฉันขึ้นอยู่กับstackoverflow.com/questions/40060498/…
Cybey

2
ทุกคนทำงานกับ JitCompiletFactory กับตัวอย่าง AOT แล้วหรือยัง ฉันมีข้อผิดพลาดเดียวกับ @Cybey
user2771738

ดูเหมือนจะเป็นไปไม่ได้แน่นอน โปรดดูgithub.com/angular/angular/issues/11780 , medium.com/@isaacplmann/…และstackoverflow.com/questions/42537138/…
เซบาสเตียน

2

ฉันพยายามที่จะดูว่าฉันจะอัปเดต RC4 เป็น RC5 ได้อย่างไรและฉันสะดุดกับรายการนี้และวิธีการใหม่ในการสร้างส่วนประกอบแบบไดนามิกยังคงมีความลึกลับอยู่เล็กน้อยสำหรับฉันดังนั้นฉันจะไม่แนะนำสิ่งใดเกี่ยวกับตัวแก้ไขส่วนประกอบของโรงงาน

แต่สิ่งที่ฉันสามารถแนะนำได้คือวิธีการสร้างส่วนประกอบที่ชัดเจนกว่าในสถานการณ์นี้ - เพียงแค่ใช้สวิตช์ในแม่แบบที่จะสร้างตัวแก้ไขสตริงหรือตัวแก้ไขข้อความตามเงื่อนไขบางอย่างดังนี้:

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

และโดยวิธีการ "[" ในการแสดงออก [prop] มีความหมายนี้บ่งบอกถึงการผูกข้อมูลทางเดียวดังนั้นคุณสามารถและควรละเว้นเหล่านั้นในกรณีที่คุณรู้ว่าคุณไม่จำเป็นต้องผูกคุณสมบัติกับตัวแปร


1
นั่นจะเป็นวิธีที่จะไป .. หากswitch/ caseมีการตัดสินใจน้อย แต่ลองจินตนาการว่าเทมเพลตที่สร้างขึ้นอาจมีขนาดใหญ่จริงๆ ... และแตกต่างกันสำหรับแต่ละเอนทิตีแตกต่างกันไปตามความปลอดภัยแตกต่างกันตามสถานะเอนทิตี้ของแต่ละประเภทคุณสมบัติ (หมายเลขวันที่อ้างอิง ... บรรณาธิการ) ... ในกรณีเช่นนี้ การแก้ปัญหานี้ในเทมเพลต html ด้วยngSwitchจะสร้างhtmlไฟล์ขนาดใหญ่และมีขนาดใหญ่มาก
Radim Köhler

โอ้ฉันเห็นด้วยกับคุณ ฉันมีสถานการณ์แบบนี้ที่นี่ตอนนี้เนื่องจากฉันพยายามโหลดส่วนประกอบสำคัญของแอปพลิเคชันโดยไม่ทราบก่อนรวบรวมคลาสเฉพาะที่จะแสดง แม้ว่ากรณีนี้ไม่จำเป็นต้องสร้างองค์ประกอบแบบไดนามิก
zii

1

นี่คือตัวอย่างของการควบคุมฟอร์มแบบไดนามิกที่สร้างจากเซิร์ฟเวอร์

https://stackblitz.com/edit/angular-t3mmg6

ตัวอย่างนี้คือตัวควบคุมฟอร์มแบบไดนามิกอยู่ในการเพิ่มองค์ประกอบ (นี่คือที่ที่คุณสามารถรับ Formcontrols จากเซิร์ฟเวอร์) ถ้าคุณเห็นวิธี addcomponent คุณสามารถดูการควบคุมแบบฟอร์ม ในตัวอย่างนี้ฉันไม่ได้ใช้วัสดุเชิงมุม แต่ใช้งานได้ (ฉันใช้ @ work) นี่คือเป้าหมายเป็นมุมฉาก 6 แต่ใช้ได้กับเวอร์ชันก่อนหน้าทั้งหมด

จำเป็นต้องเพิ่ม JITComplierFactory สำหรับ AngularVersion 5 ขึ้นไป

ขอบคุณ

วีเจย์


0

สำหรับกรณีนี้ดูเหมือนว่าการใช้คำสั่งเพื่อสร้างองค์ประกอบแบบไดนามิกจะเป็นตัวเลือกที่ดีกว่า ตัวอย่าง:

ใน HTML ที่คุณต้องการสร้างองค์ประกอบ

<ng-container dynamicComponentDirective [someConfig]="someConfig"></ng-container>

ฉันจะเข้าใกล้และออกแบบคำสั่งด้วยวิธีต่อไปนี้

const components: {[type: string]: Type<YourConfig>} = {
    text : TextEditorComponent,
    numeric: NumericComponent,
    string: StringEditorComponent,
    date: DateComponent,
    ........
    .........
};

@Directive({
    selector: '[dynamicComponentDirective]'
})
export class DynamicComponentDirective implements YourConfig, OnChanges, OnInit {
    @Input() yourConfig: Define your config here //;
    component: ComponentRef<YourConfig>;

    constructor(
        private resolver: ComponentFactoryResolver,
        private container: ViewContainerRef
    ) {}

    ngOnChanges() {
        if (this.component) {
            this.component.instance.config = this.config;
            // config is your config, what evermeta data you want to pass to the component created.
        }
    }

    ngOnInit() {
        if (!components[this.config.type]) {
            const supportedTypes = Object.keys(components).join(', ');
            console.error(`Trying to use an unsupported type ${this.config.type} Supported types: ${supportedTypes}`);
        }

        const component = this.resolver.resolveComponentFactory<yourConfig>(components[this.config.type]);
        this.component = this.container.createComponent(component);
        this.component.instance.config = this.config;
    }
}

ดังนั้นในส่วนประกอบข้อความ, สตริง, วันที่, อะไรก็ตาม - การกำหนดค่าใด ๆ ที่คุณได้ส่งผ่านใน HTML ในng-containerองค์ประกอบจะพร้อมใช้งาน

การกำหนดค่าyourConfigสามารถเหมือนกันและกำหนดข้อมูลเมตาของคุณ

ทั้งนี้ขึ้นอยู่กับการกำหนดค่าหรือประเภทอินพุตของคุณคำสั่งควรทำตามและจากประเภทที่รองรับมันจะทำให้องค์ประกอบที่เหมาะสม ถ้าไม่มันจะบันทึกข้อผิดพลาด


-1

การสร้างคำตอบโดย Ophir Stern นี่คือตัวแปรที่ทำงานร่วมกับ AoT ใน Angular 4 ปัญหาเดียวที่ฉันมีคือฉันไม่สามารถฉีดบริการใด ๆ ลงใน DynamicComponent แต่ฉันสามารถอยู่กับมันได้

หมายเหตุ: ฉันยังไม่ได้ทดสอบกับ Angular 5

import { Component, OnInit, Input, NgModule, NgModuleFactory, Compiler, EventEmitter, Output } from '@angular/core';
import { JitCompilerFactory } from '@angular/compiler';

export function createJitCompiler() {
  return new JitCompilerFactory([{
    useDebug: false,
    useJit: true
  }]).createCompiler();
}

type Bindings = {
  [key: string]: any;
};

@Component({
  selector: 'app-compile',
  template: `
    <div *ngIf="dynamicComponent && dynamicModule">
      <ng-container *ngComponentOutlet="dynamicComponent; ngModuleFactory: dynamicModule;">
      </ng-container>
    </div>
  `,
  styleUrls: ['./compile.component.scss'],
  providers: [{provide: Compiler, useFactory: createJitCompiler}]
})
export class CompileComponent implements OnInit {

  public dynamicComponent: any;
  public dynamicModule: NgModuleFactory<any>;

  @Input()
  public bindings: Bindings = {};
  @Input()
  public template: string = '';

  constructor(private compiler: Compiler) { }

  public ngOnInit() {

    try {
      this.loadDynamicContent();
    } catch (err) {
      console.log('Error during template parsing: ', err);
    }

  }

  private loadDynamicContent(): void {

    this.dynamicComponent = this.createNewComponent(this.template, this.bindings);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));

  }

  private createComponentModule(componentType: any): any {

    const runtimeComponentModule = NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })(class RuntimeComponentModule { });

    return runtimeComponentModule;

  }

  private createNewComponent(template: string, bindings: Bindings): any {

    const dynamicComponent = Component({
      selector: 'app-dynamic-component',
      template: template
    })(class DynamicComponent implements OnInit {

      public bindings: Bindings;

      constructor() { }

      public ngOnInit() {
        this.bindings = bindings;
      }

    });

    return dynamicComponent;

  }

}

หวังว่านี่จะช่วยได้

ไชโย!

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.