In fact, I'm having more issues with the ngComponentOutlet-embedded component inside the MatDialog. But let's start here.
I want to display an arbitrary Component inside a MatDialog. I've found a way, but while it works on Angular 9 (the version I've found an example written in), it does not work in Angular 11 (the version my project is based on) nor on Angular 13 (@latest).
<button (click)="close()">Close</button>
and I click on the button, the inner Component's close()
method is not triggeredclose()
method if I bind it to the (mousedown)
event instead of (click)
; probably works with other events but the (click)
oneAngular 9 does not have this problem. I am using exactly the same app code in both examples below (both projects created with ng new
, using different ng
versions).
(stackblitz is ill, give it a few retries if it sneezes out 500s. Probably covid...)
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();
}
}
}
my-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
});
}
}
Get rid of createInjector(inner.data)
method call from the BaseDialogComponent
template.
Instead create the injector and store it within the BaseDialogComponent
property. Then assign that property to *ngComponentOutlet
injector.
@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,
});
}
}
First of all the issue(varying behavior) is not due to the code within Angular framework, but due to some code within Angular Material.
In Angular Material v11, the CDK overlay adds a click
event listener on document body
during capture phase. Hence whenever you clicked, the Change detection was triggered even before the click listener associated with the button got chance to execute, which is turn resulted into re-rendering of the view as createInjector()
method always returned a new Injector instance when called.
Due to the same reason you observed the below behavior of component being 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
The Angular Material v9 doesn't include this click
event listener code, hence the listener associated with the button executed and closed the dialog without causing any issue. The clicks within the overlay and not on "Close" button again didn't triggered any Change Detection, and hence no re-rendering happened.
You can replicate the same behavior in your Angular 9 code by adding a listener as below:
// AppComponent
constructor(private dialogService: MyDialogService) {
document.body.addEventListener('click', () => console.log('clicked'), true);
}