如何使用服务的输入/输出动态创建组件实例并将其分别注入 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
的服务,其中包含返回不同形状组件的函数,其中一些 @Input
像 bgColor
,我不知道该服务将如何在不指定 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
中的表单被提交时,带有name
和favouriteFood
的对象会被传递回父级。这也会触发父组件关闭dialog-wrapper
。
我已尝试使代码尽可能简单明了,以便可以轻松地重新分配任务。 dialog wrapper 本身相当简单;大部分繁重的工作都在injected component 和parent component 中。
这并不完全是 OP 中概述的架构,但我相信它满足:
我需要可以随时随地注入的独立组件对象。
【讨论】:
以上是关于如何使用服务的输入/输出动态创建组件实例并将其分别注入 DOM?的主要内容,如果未能解决你的问题,请参考以下文章
是否可以从describe_instances()输出创建EC2实例?
如何在运行时手动创建服务并将其连接到 typescript Angular 2 中的组件
使用 ng-content 动态创建 angular2 组件