使用 Angular 2 创建独立的模态对话服务

Create standalone modal dialog service with Angular 2

为了研究Angular2中创建动态对话框组件的方法,我google了很久。几乎所有的建议如下:

  1. 为动态组件创建占位符。
  2. 使用 ComponentFactoryResolver 动态创建组件以在打开的对话框中显示
  3. 在应用程序模块中使用 entryComponents 让编译器了解自定义组件的工厂

这一切都很好,但在我的项目中,我必须像 shlomiassaf/angular2-modal or angular2-material 一样实现一个独立的模式服务,但没有这些库提供给最终用户的大量自定义和设置。创建此类功能的步骤是什么?

您可以在您的应用程序中创建单独的模块来处理组件、服务、指令等的某些功能... 最后,您可以将模块注入主(根)模块并开始使用第 3 部分库。

关于处理该案例的精彩教程在此 LINK

我想出了如何使用 angular 2 服务创建一个简单的对话框。主要概念是以编程方式创建覆盖,然后将其 hostView 附加到应用程序,然后将其附加到 document.body。然后创建对话框本身并使用覆盖参考将对话框附加到新创建的覆盖。

叠加

import {
  Component,
  ChangeDetectionStrategy,
  EventEmitter,
  Output,
  ViewChild,
  ViewContainerRef
} from "@angular/core";

@Component ({
  moduleId: module.id,
  selector: "dialog-overlay",
  template: `
    <div class="dialog-overlay" (click)="onOverlayClick($event)">
      <div #dialogPlaceholder ></div>
    </div>
  `,
  styleUrls: ["./overlay.component.css"],
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class OverlayComponent {
  @ViewChild("dialogPlaceholder", {read: ViewContainerRef})
  public dialogPlaceholder: ViewContainerRef;

  @Output()
  public overlayClick: EventEmitter<any> = new EventEmitter();

  public isOverlayOpen: boolean = false;

  public onOverlayClick($event: Event): void {
    const target: HTMLElement = <HTMLElement>$event.target;

    if (target.className.indexOf("dialog-overlay") !== -1) {
        this.overlayClick.emit();
    }
  }
}

这里我们有一个带有叠加样式的简单组件(未包含在示例中)和一个模板变量 dialogPlaceholder,我们将使用它来放置我们的对话框。

对话框组件

import {
  Component,
  EventEmitter,
  HostBinding,
  Input,
  Output,
  ViewChild,
  ViewContainerRef,
  ChangeDetectionStrategy
} from "@angular/core";

@Component ({
  moduleId: module.id,
  selector: "dialog",
  template: `
    <div class="your-dialog-class">
      <div class="your-dialog-title-class">{{title}}</div>
      ... whatever
      <div #dynamicContent ></div>
      ... whatever
    </div>
  `
  styleUrls: ["./dialog.component.css"],
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class DialogComponent {
  @ViewChild("dynamicContent", {read: ViewContainerRef})
  public dynamicContent: ViewContainerRef;

  @Input()
  public isOpen: boolean = false;

  @Input()
  public title: string;

  @Output()
  public isOpenChange: EventEmitter<any> = new EventEmitter();

  public onCloseClick(event: Event): void {
    event.preventDefault();
    this.isOpenChange.emit(false);
  }
}

此组件将通过服务以编程方式创建,其 #dynamicContent 将用作对话框内容的占位符

服务

我不会包括整个服务列表,只有 create 方法只是为了展示动态创建对话框的主要概念

public create(content: Type<any>,
              params?: DialogParams): DialogService {
    if (this.dialogComponentRef) {
        this.closeDialog();
    }

    const dialogParams: DialogParams = Object.assign({}, this.dialogDefaultParams, params || {});
    const overlayFactory: ComponentFactory<any> = this.componentFactoryResolver.resolveComponentFactory(OverlayComponent);
    const dialogFactory: ComponentFactory<any> = this.componentFactoryResolver.resolveComponentFactory(DialogComponent);

    // create an overlay
    this.overlayComponentRef = overlayFactory.create(this.injector);
    this.appRef.attachView(this.overlayComponentRef.hostView);
    this.document.body.appendChild(
      this.overlayComponentRef.location.nativeElement
    );
    this.overlayComponentRef.instance.isOverlayOpen = dialogParams.isModal;

    // create dialog box inside an overlay
    this.dialogComponentRef = this.overlayComponentRef
        .instance
        .dialogPlaceholder
        .createComponent(dialogFactory);
    this.applyParams(dialogParams);

    this.dialogComponentRef.changeDetectorRef.detectChanges();

    // content
    this.contentRef = content ? this.attachContent(content, contentContext) : undefined;

    const subscription: Subscription = this.dialogComponentRef.instance.isOpenChange.subscribe(() => {
        this.closeDialog();
    });

    const overlaySubscription: Subscription = this.overlayComponentRef.instance.overlayClick.subscribe(() => {
        if (dialogParams.closeOnOverlayClick) {
            this.closeDialog();
        }
    });

    this.subscriptionsForClose.push(subscription);
    this.subscriptionsForClose.push(overlaySubscription);

    return this;
}

// this method takes a component class with its context and attaches it to dialog box
private attachContent(content: any,
                      context: {[key: string]: any} = undefined): ComponentRef<any> {
  const containerRef: ViewContainerRef = this.dialogComponentRef.instance.dynamicContent;
  const factory: ComponentFactory<any> = this.componentFactoryResolver.resolveComponentFactory(content);
  const componentRef: ComponentRef<any> = containerRef.createComponent(factory);

  this.applyParams(context, componentRef);
  componentRef.changeDetectorRef.detectChanges();

  const { instance } = componentRef;

  if (instance.closeEvent) {
    const subscription: Subscription = componentRef.instance.closeEvent.subscribe(() => {
        this.closeDialog();
    });

    this.subscriptionsForClose.push(subscription);
  }

    return componentRef;
}

// this method applies dialog parameters to dialog component.
private applyParams(inputs: {[key: string]: any}, component: ComponentRef<any> = this.dialogComponentRef): void {
    if (inputs) {
        const inputsKeys: Array<string> = Object.getOwnPropertyNames(inputs);

        inputsKeys.forEach((name: string) => {
            component.instance[name] = inputs[name];
        });
    }
}

public closeDialog(): void {
    this.subscriptionsForClose.forEach(sub => {
        sub.unsubscribe();
    });
    this.dialogComponentRef.destroy();
    this.overlayComponentRef.destroy();
    this.dialogComponentRef = undefined;
    this.overlayComponentRef = undefined;
}

尽管这种方法比 shlomiassaf/angular2-modalangular2-material 中的方法简单得多,但它需要做很多工作。在服务中动态创建组件的整个方法违反了关注点分离 原则,但是谈到动态创建的对话框,动态创建它们比将它们保留在模板中的某个地方更方便。