11

在 Angular 2 中创建动态组件时,我发现这个 过程需要将新创建的组件添加到 DOM。ViewContainerRef

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

但是,如果我要创建一个名为的服务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是动态创建组件。

第二个问题是容器组件componentRef从服务接收到特定输入后,它将如何注入到它的 DOM 中?

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

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

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

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

4

3 回答 3

9

如果像我这样的人现在仍在寻找一个简单而清晰的解决方案 - 就在这里。我从@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

PS不要忘记将您的动态组件添加entryComponents: []@NgModule

于 2019-11-08T11:58:58.747 回答
7

也许这个 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;
  }
}
于 2016-08-31T07:05:03.097 回答
0

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

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

Stackblitz 上的演示

回购代码

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

我试图使代码尽可能简单,以便可以轻松地重新分配任务。对话框包装器本身相当简单;大部分繁重的工作都在注入的组件父组件中。

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

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

于 2020-05-14T12:25:02.707 回答