Issue
In fact, I'm having more issues with the ngComponentOutlet-embedded component inside the MatDialog. But let's start here.
What I'm building
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).
Observations
- when an inner HTML contains a
<button (click)="close()">Close</button>
and I click on the button, the inner Component'sclose()
method is not triggered - it triggers the
close()
method if I bind it to the(mousedown)
event instead of(click)
; probably works with other events but the(click)
one - when I click on the button, instead the inner component is reloaded (see console logs in examples)
- when I click anywhere on the dialog, the inner component is reloaded (see console logs in examples); does not happen in Angular 9
Angular 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).
Repro examples
(stackblitz is ill, give it a few retries if it sneezes out 500s. Probably covid...)
- In the Angular 9 example, the MatDialog works as expected
- In the Angular 11 example the MatDialog does not work as expected
- I have tried Angular 13 (@latest), the problem persists
Questions
- Why is this happening?
- How do I get around this?
Raw files 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();
}
}
}
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
});
}
}
Solution
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,
});
}
}
Why the same code worked in Angular 9, but not in Angular 11 and above?
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);
}
Answered By - Siddhant
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.