Angular 11+ MatDialog:内部组件的 (ngComponentOutlet) html 不会触发 <button (click)="close()"> 方法。适用于 Angular 9

Angular 11+ MatDialog: inner component's (ngComponentOutlet) html does not trigger <button (click)="close()"> method. Works on Angular 9

事实上,我对 MatDialog 内的 ngComponentOutlet 嵌入式组件有更多问题。但让我们从这里开始吧。

我正在构建什么

我想在 MatDialog 中显示任意组件。我找到了一种方法,但是虽然它适用于 Angular 9(我找到了一个例子的版本),但它在 Angular 11(我的项目基于的版本)中不起作用) 也不在 Angular 13 (@latest).

观察结果

Angular9没有这个问题。我在下面的两个示例中使用了完全相同的应用程序代码(两个项目都是使用 ng new 创建的,使用不同的 ng 版本)。

复制示例

(stackblitz 生病了,如果它打了 500 秒就重试几次。可能是 covid...)

Broken example (Angular 11)

Working example (Angular 9)

问题

  1. 为什么会这样?
  2. 我该如何解决这个问题?

原始文件 FFR

app.module.ts

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

import {AppComponent} from './app.component';
import {MatDialogModule} from '@angular/material/dialog';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {BaseDialogComponent, SampleInnerComponent} from './my-dialog.service';

@NgModule({
  declarations: [
    AppComponent,
    BaseDialogComponent, SampleInnerComponent
  ],
  imports: [
    BrowserModule,
    MatDialogModule, BrowserAnimationsModule
  ],
  exports: [BaseDialogComponent, SampleInnerComponent],
  providers: [BaseDialogComponent, SampleInnerComponent],
  bootstrap: [AppComponent],
  entryComponents: [BaseDialogComponent, SampleInnerComponent]
})
export class AppModule { }

app.component.ts

import {Component} from '@angular/core';
import {MyDialogService} from './my-dialog.service';
import {MatDialogRef} from '@angular/material/dialog';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="toggle()">TOGGLE</button>
  `,
})
export class AppComponent {
  title = 'repro-broken';
  private dialogRef: MatDialogRef<any>;

  constructor(private dialogService: MyDialogService) {
  }

  toggle(): void {
    if (this.dialogRef) {
      this.dialogRef.close(undefined);
      this.dialogRef = undefined;
    } else {
      this.dialogRef = this.dialogService.open();
    }
  }
}

我的-dialog.service.ts

import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
import {Component, Inject, Injectable, Injector} from '@angular/core';
import {ReplaySubject} from 'rxjs';
import {tap} from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class MyDialogService {

  constructor(private dialog: MatDialog) {
  }

  open(): MatDialogRef<any> {
    const innerComp = new InjectedDialogRef();
    const dialogRef = this.dialog.open(BaseDialogComponent, {
      // width: '',
      // height: '',
      // closeOnNavigation: false,
      // disableClose: true,
      // backdropClass: [],
      // hasBackdrop: false,
      data: {component: SampleInnerComponent, data: innerComp}
    });

    innerComp.dialog$.next(dialogRef);
    return dialogRef;
  }

}


@Injectable()
export class InjectedDialogRef {
  dialog$ = new ReplaySubject<MatDialogRef<any>>(1);
}

@Component({
  selector: 'app-dialog-sample',
  template: `
    <div (mousedown)="stuff()">Dialog Inner Component</div>
    <button (click)="close()">Close</button>
    <!--    <button (click)="stuff()">Stuff</button>-->
  `,
})
export class SampleInnerComponent {
  public dialog: MatDialogRef<any>;

  constructor(private inj: InjectedDialogRef) {
    inj.dialog$
      .pipe(tap(evt => console.log('Got a dialog', evt)))
      .subscribe(dialog => this.dialog = dialog);
  }

  close(): void {
    console.log('Closing the dialog', this.dialog);
    this.dialog.close(undefined);
  }

  stuff(): void {
    console.log('Doing stuff');
  }
}

@Component({
  selector: 'app-dialog-base',
  template: `
    <h2 mat-dialog-title>MyTitle</h2>
    <div mat-dialog-content>
      <ng-container *ngComponentOutlet="inner.component; injector:createInjector(inner.data)"></ng-container>
    </div>
  `,
})
export class BaseDialogComponent {

  constructor(
    @Inject(MAT_DIALOG_DATA) public inner: any,
    private inj: Injector) {
    console.log('Opening base dialog');
  }

  createInjector(inj: InjectedDialogRef): Injector {
    return Injector.create({
      providers: [{provide: InjectedDialogRef, useValue: inj}],
      parent: this.inj
    });
  }
}

BaseDialogComponent 模板中删除 createInjector(inner.data) 方法调用。

而是创建注入器并将其存储在 BaseDialogComponent 属性 中。然后将 属性 分配给 *ngComponentOutlet 喷油器。

@Component({
  selector: 'app-dialog-base',
  template: `
    <h2 mat-dialog-title>MyTitle</h2>
    <div mat-dialog-content>

    <!-- Removed createInjector(inner.data) method call and replaced with contentInjector property  -->
      <ng-container *ngComponentOutlet="inner.component; injector:contentInjector"></ng-container>

    </div>
  `,
})
export class BaseDialogComponent implements OnInit {
  contentInjector!: Injector; // Defined property to hold the content injector

  constructor(
    @Inject(MAT_DIALOG_DATA) public inner: any,
    private inj: Injector
  ) {
    console.log('Opening base dialog');
  }

  // Created the injector within ngOnInit
  ngOnInit() {
    this.contentInjector = this.createInjector(this.inner.data);
  }

  createInjector(inj: InjectedDialogRef): Injector {
    return Injector.create({
      providers: [{ provide: InjectedDialogRef, useValue: inj }],
      parent: this.inj,
    });
  }
}

Stackblitz


为什么相同的代码在 Angular 9 中有效,但在 Angular 11 及更高版本中却无效?

首先,问题(不同的行为)不是由于 Angular 框架内的代码造成的,而是由于 Angular Material.

内的一些代码造成的

在 Angular Material v11 中,CDK overlay 在 capture 阶段在文档 body 上添加了一个 click 事件侦听器。因此,每当您单击时,甚至在与按钮关联的单击侦听器有机会执行之前就会触发更改检测,这导致视图的 re-rendering 因为 createInjector() 方法总是返回一个新的 Injector 实例被调用时。

由于同样的原因,您观察到组件的以下行为是 reloaded/rendered:

when I click anywhere on the dialog, the inner component is reloaded (see console logs in examples); does not happen in Angular 9

click event listener in Angular Material v11

Angular Material v9 不包含此 click 事件侦听器代码,因此与按钮关联的侦听器已执行并关闭了对话框而没有引起任何问题。在叠加层内而不是在“关闭”按钮上的点击再次没有触发任何更改检测,因此没有 re-rendering 发生。

您可以通过添加如下侦听器在 Angular 9 代码中复制相同的行为:

// AppComponent
constructor(private dialogService: MyDialogService) {
  document.body.addEventListener('click', () => console.log('clicked'), true);
}