如何使用服务的输入/输出动态创建组件实例并将其分别注入 DOM?

Posted

技术标签:

【中文标题】如何使用服务的输入/输出动态创建组件实例并将其分别注入 DOM?【英文标题】:How to dynamically create component instance with input/output from a service and inject it to DOM separately? 【发布时间】:2017-01-07 13:12:32 【问题描述】:

在 Angular 2 中创建动态组件时,我 found out 表示此过程需要 ViewContainerRef 才能将新创建的组件添加到 DOM。

在将@Input@Output 传递给那些动态创建的组件时,我在上面的第二个链接和here 中找到了答案。

但是,如果我要创建一个名为 shape.service 的服务,其中包含返回不同形状组件的函数,其中一些 @InputbgColor,我不知道该服务将如何在不指定 DOM 位置的情况下创建组件,以及容器组件如何从服务接收这个返回的组件(它的类型可能是ComponentRef)并将其注入到容器组件指定的 DOM 中。

例如,一个服务包含一个方法:

getCircle(bgColor:string): ComponentRef<Circle> 
    let circleFactory = componentFactoryResolver.resolveComponentFactory(CircleComponent);
    let circleCompRef = this.viewContainerRef.createComponent(circleFactory);
    circleCompRef.instance.bgColor = bgColor;

    return circleCompRef;

这里出现了第一个问题,我如何让this.viewContainerRef 同时指向无处?我导入ViewContainerRef的原因是动态创建组件。

第二个问题,container-component 从服务接收到 input-specificcomponentRef 后,它会如何注入到它的 DOM 中?

更新: 我认为我上面的问题不够具体。我的情况是:

    父组件调用服务并获取componentRef/s, 创建一个包含 componentRef/s 和一些其他数据的对象,并将这些创建的 object/s 存储到数组中 以@Input 的形式将其传递给其子代, 并让每个孩子将 componentRef 注入其 DOM 并以其他方式使用对象中的其余数据。

这意味着服务调用组件不知道这些 componentRef 将被注入到哪里。简而言之,我需要可以随时随地注入的独立组件对象。

我已经多次阅读 rumTimeCompiler 解决方案,但我并不真正了解它是如何工作的。与使用 viewContainerRef 创建组件相比,似乎工作量太大。如果我找不到其他解决方案,我会深入研究...

【问题讨论】:

注意:也可以通过RuntimeCompiler***.com/q/38888008/1679310查看解决方案 【参考方案1】:

如果像我这样的人现在仍在寻找简单明了的解决方案 - 就在这里。我是从@angular/cdk https://github.com/angular/components/tree/master/src/cdk 得到的 并做了一个简单的服务。

import 
    Injectable,
    ApplicationRef,
    ComponentFactoryResolver,
    ComponentRef,
    Injector,
    EmbeddedViewRef
 from '@angular/core';

export type ComponentType<T> = new (...args: any[]) => T;

@Injectable(
    providedIn: 'root'
)
export class MyService 

    constructor(
        private _appRef: ApplicationRef,
        private _resolver: ComponentFactoryResolver,
        private _injector: Injector
    )  

    private _components: ComponentRef<any>[] = [];

    add<T>(
        component: ComponentType<T> | ComponentRef<T>,
        element?: Element | string
    ): ComponentRef<T> 
        const componentRef = component instanceof ComponentRef
            ? component
            : this._resolver.resolveComponentFactory(component).create(this._injector);
        this._appRef.attachView(componentRef.hostView);
        if (typeof element === 'string') 
            element = document.querySelector(element);
        
        if (!element) 
            element = document.body;
        
        element.appendChild(
            (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as htmlElement
        );
        this._components.push(componentRef);
        return componentRef;
    

    remove(dialog: number | ComponentRef<any>): boolean 
        let componentRef;
        if (typeof dialog === 'number' && this._components.length > dialog)  
            componentRef = this._components.splice(dialog, 1)[0];
        
        else 
            for (const cr of this._components) 
                if (cr === dialog) 
                    componentRef = cr;
                
            
        
        if (componentRef) 
            this._remove(componentRef);
            return true;
        
        return false;
    

    private _remove(componentRef: ComponentRef<any>) 
        this._appRef.detachView(componentRef.hostView);
        componentRef.destroy();
    

    clear() 
        while (this._components.length > 0) 
            this._remove(this._components.pop());
        
    

    getIndex(componentRef: ComponentRef<any>): number 
        return this._components.indexOf(componentRef);
    


您可以将 ComponentClass 或 ComponentRef 传递给 add 和 Element 或任何 querySelector 字符串,该字符串指向您希望将组件作为第二个参数附加到的任何 DOM 元素(或不传递任何内容,则假定您要附加到正文)。

const cr = this._service.add(MyComponent); // will create MyComponent and attach it to document.body or
const cr = this._service.add(MyComponent, this.someElement); // to attach to Element stored in this.someElement or
const cr = this._service.add(MyComponent, 'body div.my-class > div.my-other-div'); // to search for that element and attach to it
const crIndex = this._service.getIndex(cr);
cr.instance.myInputProperty = 42;
cr.instance.myOutputEmitter.subscribe(
    () => 
        // do something then for example remove this component
        this._service.remove(cr);
    
);
this._service.remove(crIndex); // remove by index or
this._service.remove(cr); // remove by reference or
this._service.clear(); // remove all dynamically created components

附注不要忘记将您的动态组件添加到entryComponents: [] of @NgModule

【讨论】:

完美运行,但我必须做一些调整,顺便说一句,我使用的是 Angular 9:* 直接在构造函数中注入 private appRef: ApplicationRef 导致循环依赖错误:Error: Cannot instantiate cyclic dependency! ApplicationRef,所以我选择了使用注入器延迟加载对象 ``` this.appRef = this.injector.get(ApplicationRef); this.appRef.attachView(componentRef.hostView); ``` * 此外,当调用 add 元素时,您可以将其附加到由 id 标识的 dom 对象,使用:this._service.add(MyComponent, '#my-element-id'); 是的,效果很好。真的非常感谢!不确定 entryComponents[] 因为我可以在不设置的情况下使用它。【参考方案2】:

也许这个 plunker 会帮助你:https://plnkr.co/edit/iTG7Ysjuv7oiDozuXwj6?p=preview

据我所知,您的服务中需要ViewContainerRef。 但是调用你的服务的组件可以将其作为参数添加,如下所示:

(只是一个服务.. 请参阅 plunker 以获得完整的工作示例)

import  Injectable, ViewContainerRef, ReflectiveInjector, ComponentFactoryResolver, ComponentRef  from '@angular/core';

import  HelloComponent, HelloModel  from './hello.component';

@Injectable()
export class DynamicCompService 

  constructor (private componentFactoryResolver: ComponentFactoryResolver)  

  public createHelloComp (vCref: ViewContainerRef, modelInput: HelloModel): ComponentRef 

    let factory = this.componentFactoryResolver.resolveComponentFactory(HelloComponent);

    // vCref is needed cause of that injector..
    let injector = ReflectiveInjector.fromResolvedProviders([], vCref.parentInjector);

    // create component without adding it directly to the DOM
    let comp = factory.create(injector);

    // add inputs first !! otherwise component/template crashes ..
    comp.instance.model = modelInput;

    // all inputs set? add it to the DOM ..
    vCref.insert(comp.hostView);

    return comp;
  

【讨论】:

糟糕,我在完成我的句子之前按了 Enter :( 很抱歉删除了您答案中的“绿色检查”。我认为这是我需要的,但我发现我的问题不够具体。 .. 如果可能的话,您介意检查更新并提供其他解决方案吗?很抱歉给您带来麻烦 我不知道是否可以像这样创建组件,因为组件需要知道它可以使用哪些组件、服务和指令。我目前也在寻找解决方案。也许我们会发现一些东西..保持这个线程! :) 感谢您的努力!我仍然不知道为什么不指定 viewContainer 就不能创建组件实例。如果可能的话,我相信将现有组件实例从一个容器移动、删除或添加到另一个容器会更容易。以某种方式可以使用componentFactory,但如果该组件需要输入/输出,则很难实现。我试图寻找为什么在创建组件实例时指定 viewcontainer 是“必须”的,但我仍然不知道为什么。我希望至少有人会给出答案......【参考方案3】:

这是动态添加组件并管理该组件与其父级之间的通信的另一种方法。

具体的例子是一个带有表单的对话框,这是一个常见的用例。

Demo on Stackblitz.

Repo with code

dialog-wrapper 是一个对话框组件 dialog-form 是注入到 dialog-wrapper 的动态组件的示例,具有必要的支持服务。 name (Alice) 是通过dialog-wrapper 将任意数据从父组件传递到dialog-form 的示例 当dialog-form 中的表单被提交时,带有namefavouriteFood 的对象会被传递回父级。这也会触发父组件关闭dialog-wrapper

我已尝试使代码尽可能简单明了,以便可以轻松地重新分配任务。 dialog wrapper 本身相当简单;大部分繁重的工作都在injected component 和parent component 中。

这并不完全是 OP 中概述的架构,但我相信它满足:

我需要可以随时随地注入的独立组件对象。

【讨论】:

以上是关于如何使用服务的输入/输出动态创建组件实例并将其分别注入 DOM?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以从describe_instances()输出创建EC2实例?

如何在运行时手动创建服务并将其连接到 typescript Angular 2 中的组件

Django创建动态网页的基础知识

使用 ng-content 动态创建 angular2 组件

使用 ng-content 动态创建 angular2 组件

在 JSF 2.0 中动态创建输入字段并将其链接到支持 bean