Mat Dialog: beforeClosed() should have an option to cancel
Bug, feature request, or proposal:
Feature Request
What is the expected behavior?
It should be possible to cancel the close event.
What is the current behavior?
It is not possible to cancel the close event.
What is the use-case or motivation for changing an existing behavior?
Use-case: Dialog with Form Model which will be sent to the server. So for example, the result of the this server call is an error, which should be displayed inside the dialog and prevent it from closing. Right now, this is only possible if I put the logic (store or service calls) inside the dialog component. This isn’t following the dumb and smart components pattern, where the logic should be put inside the container component.
Which versions of Angular, Material, OS, TypeScript, browsers are affected?
Angular 7, Angular Material 7
Is there anything else we should know?
👍 Also if you have a form inside a dialog and you'd like to cancel the closing of the dialog based on whether the form is dirty or not. A bit the use case of the CanDeactivate on the router.
I'd like to provide a PR for this if that's desired. I'd implement this on the cdk-experimental Dialog.
My proposal would be to add a canClose function to the DialogRef. Inside a Dialog one gets the DialogRef injected and could hook on the function onto it, like
import { DialogRef } from '@angular/cdk-experimental/dialog';
...
@Component({...}
export class SomeDialog {
constructor(private dialogRef: DialogRef<any>) {
this.dialogRef.canClose = (/* pass some data? */) => {
// decide whether the dialog can be closed based
// on the current state of SomeDialog component here.
// if it can be closed, return true, otherwise false
}
}
}
On the other side, in the DialogRef implementation here: https://github.com/angular/material2/blob/master/src/cdk-experimental/dialog/dialog-ref.ts#L72-L74
...the current logic could be altered to verify whether such canClose function exists, and if so invoke it.
What do you think? Let me know if this could be a valid option and I'll provide an according PR.
+1
+1. Any news about this?
+1 I'd like to wait with closing the dialog until it's output has been successfully stored in the backend.
My workaround was to subscribe to a rxjs Subject in OnInit hook of the dialog to execute close(). subject.next() then will be fired once the whatever operation is complete
+1
Any news on this? I would love to implement a generic way to prevent closing a dialog containing a dirty form. This would be similar to the unload event when you close the browser.
@marty30 not that clean way, but I try to workaround with something like this:
constructor(
@Inject(MAT_DIALOG_DATA) public data: { type: string, id?: number },
private dialogRef: MatDialogRef<GameBaseComponent>) {
}
ngOnInit(): void {
this.dialogRef.disableClose = true;
this.dialogRef.backdropClick().subscribe(async () => await this.safeClose());
this.createForm();
}
public async safeClose(result?: string): Promise<void> {
if (!this.form.touched && !this.form.dirty) {
this.dialogRef.close();
return;
}
const confirmResult = await this.pageService.confirm({ ... }).afterClosed().toPromise();
if (!Boolean(confirmResult)) {
return;
}
this.dialogRef.close(result);
}
and I change mat-dialog-close with (click)='safeClose()' everywhere in the template too Anybody has better solution?
Solution for close dialog conditionally. disable close = true;
closeDialog() { if (this.profile.form.dirty && confirm('You have unsaved changes! Are you sure you want to continue?')) { this.dialogRef.close(); } }
this.dialogRef.keydownEvents().subscribe(key => { if (key.code === 'Escape') { this.closeDialog(); } });
just landed here. interesting read, still open. i bump and will implemented suggestion in the meantime.
after reading the question again - i found out my solutions is not right for this case (SOLUTION 1) - please see SOLUTION 2
SOLUTION 2 - SUBMIT BUTTON SHOULDN'T CLOSE DIALOG BEFORE VALIDATING
if you'd like to know if submit was pressed but not close the dialog until some outer validations occur - then you should add a new event emitter to you costume dialog as such:
export class MyDialogComponent {
@Output() onSubmit: EventEmitter<any> = new EventEmitter<any>();
constructor(private _dialogRef: MatDialogRef,
@Inject(MAT_DIALOG_DATA) private _dialogData) {
}
submitButtonClick() { // this function should be called in the template on `click`
this.onSubmit.emit(<IF VALUE NEEDED IN THE SUBSCRIBER - PUT HERE>);
}
}
then you can subscribe to that event, perform your validation & close the dialog if necessary like so:
export class MyPage {
constructor(public dialog: MatDialog) {}
...
openMyDialog() {
const dRef = this.dialog.open(MyDialogComponent);
dRef.componentInstance.onSubmit.subscribe(() => {
// DO YOUR VALIDATION HERE
...
// IF VALID THEN CLOSE
dRef.close();
});
}
}
or from the template with
<my-dialog (onSubmit)="validateInput()"></my-dialog>
SOLUTION 1 - WHEN CALLING CLOSE, PERFORM SOME INNER VALIDATION IN THE DIALOG
we had this issue as well - the workaround we implemented is fairly simple - just hijack the original close function like so:
export class MyDialogComponent {
private _originalClose: Function;
constructor(private _dialogRef: MatDialogRef,
@Inject(MAT_DIALOG_DATA) private _dialogData) {
this._originalClose = this._dialogRef.close;
this._dialogRef.close = this.closeDialog.bind(this);
}
closeDialog() {
// DO YOUR VALIDATION LOGIC HERE
...
// CALL ORIGINAL CLOSE FUNCTION IF VALIDATION PASSED
this._originalClose.bind(this._dialogRef)();
}
}
this way all of the consumers can call the original close function without being aware of the internal MyDialog API or closeFunction :)
I tried your solution 1 and it worked except for one little problem. How can _dialogData be referenced in the calling class? In my project, I got an error "_dialogData is undefined".
export class MyDialogComponent {
private _originalClose: Function;
constructor(private _dialogRef: MatDialogRef,
@Inject(MAT_DIALOG_DATA) public _dialogData) { // Made private to public to capture user input
this._originalClose = this._dialogRef.close;
this._dialogRef.close = this.closeDialog.bind(this);
}
closeDialog() {
if (String(this.data.confirm_word) == "SAVE")
{
this._originalClose.bind(this.dialogRef)();
}
else
{
alert("Please enter the word 'SAVE' in the text box.")
}
}
onNoClick(): void {
this.dialogRef.close();
}
}
openDialog(): void {
const dialogRef = this.dialog.open(MyDialogComponent, {
width: '500px',
data: {confirm_word: "", message: "Do you like to save the changes in the grid? Please type the word SAVE below."}
});
dialogRef.afterClosed().subscribe(result => {
alert ("open dialog = " + String(result)) //--> result is undefined.
if (String(result) == "SAVE")
{
this.saveGrid();
}
});
}
<h1 mat-dialog-title>Confirm</h1>
<div mat-dialog-content style="font-family: Calibri,Candara,Segoe,Segoe UI,Optima,Arial,sans-serif;">
<p>{{data.message}}</p>
<mat-form-field>
<mat-label></mat-label>
<input matInput [(ngModel)]="data.confirm_word" cdkFocusInitial>
</mat-form-field>
</div>
<div mat-dialog-actions style="font-family: Calibri,Candara,Segoe,Segoe UI,Optima,Arial,sans-serif;">
<button mat-button (click)="onNoClick()">Cancel</button>
<button mat-button [mat-dialog-close]="data.confirm_word">Ok</button>
</div>
I tried your solution 1 and it worked except for one little problem. How can _dialogData be referenced in the calling class? In my project, I got an error "_dialogData is undefined".
export class MyDialogComponent { private _originalClose: Function; constructor(private _dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public _dialogData) { // Made private to public to capture user input this._originalClose = this._dialogRef.close; this._dialogRef.close = this.closeDialog.bind(this); } closeDialog() { if (String(this.data.confirm_word) == "SAVE") { this._originalClose.bind(this.dialogRef)(); } else { alert("Please enter the word 'SAVE' in the text box.") } } onNoClick(): void { this.dialogRef.close(); } } openDialog(): void { const dialogRef = this.dialog.open(MyDialogComponent, { width: '500px', data: {confirm_word: "", message: "Do you like to save the changes in the grid? Please type the word SAVE below."} }); dialogRef.afterClosed().subscribe(result => { alert ("open dialog = " + String(result)) //--> result is undefined. if (String(result) == "SAVE") { this.saveGrid(); } }); } <h1 mat-dialog-title>Confirm</h1> <div mat-dialog-content style="font-family: Calibri,Candara,Segoe,Segoe UI,Optima,Arial,sans-serif;"> <p>{{data.message}}</p> <mat-form-field> <mat-label></mat-label> <input matInput [(ngModel)]="data.confirm_word" cdkFocusInitial> </mat-form-field> </div> <div mat-dialog-actions style="font-family: Calibri,Candara,Segoe,Segoe UI,Optima,Arial,sans-serif;"> <button mat-button (click)="onNoClick()">Cancel</button> <button mat-button [mat-dialog-close]="data.confirm_word">Ok</button> </div>
Hi i think you should try the 2nd solution & it will work for your usecase
(by creating a new EventEmitter in your dialog component and subscribing to it)
BTW (!!!)
in you dialog code - the cancel execution order is:
onNoClick()from the templatethis.dialogRef.close()which was replaced withthis.closeDialog()this.closeDialog()this._originalClose()so that's bad. if you expect some result in.afterClosed()then you should pass the value here:this._originalClose.bind(this.dialogRef)(MY_RESULT);or if the originalclosefunction was never hijacked then:this.dialogRef.close(MY_RESULT);
Your solution 2 works perfect. Thanks
+1 any updates on this one?
At least BeforeClosed should have a parameter "Cancel" to prevent the dialog closing otherwise how useful is this method for the user? One as well can use AfterClosed for their needs.
The perfect solution would be adding BeforeClosing method which will have Cancel as one of its parameters to prevent the dialog closing.
+1 any updates on this one?
At least BeforeClosed should have a parameter "Cancel" to prevent the dialog closing otherwise how useful is this method for the user? One as well can use AfterClosed for their needs.
The perfect solution would be adding BeforeClosing method which will have Cancel as one of its parameters to prevent the dialog closing.
Yeah, think so, too! Right now beforeClosed is just a an alt for afterClosed imho.
+1 alguna actualización sobre este?
Al menos BeforeClosed debe tener un parámetro "Cancelar" para evitar que el cuadro de diálogo se cierre; de lo contrario, ¿qué tan útil es este método para el usuario? Uno también puede usar AfterClosed para sus necesidades.
La solución perfecta sería agregar el método BeforeClosing que tendrá Cancelar como uno de sus parámetros para evitar que se cierre el cuadro de diálogo.
Hello! I join this thread. And I agree with you.
Is there any news?
Adding my voice here. We have forms in dialogs, and want to prompt the user to confirm discarding the form with another dialog. This is possible to do if we set disableClose = true and then implement our own close button. But ideally this works when pressing Escape or clicking outside the dialog as well.
Here's an example of the functionality I'd like:
this.dialogRef.beforeClose(() => {
return new Promise((resolve, reject) => {
if (this.form.pristine) {
resolve()
} else {
this.confirmDiscardDialog().afterClosed().subscribe((confirmed) => confirmed ? resolve() : reject())
}
})
});
(Or the observable equivalent)
This feature would be great. I'm trying to make my dialog as "dumb" as possible. I have a form in my dialog that saves sends stuff to a server. If the server responds with an error I want to abort the closing of the dialog and let the user correct the problem. As of now. I have to have the server logic inside of the dialog to be able to only close it on success.
This feature would be great. I'm trying to make my dialog as "dumb" as possible. I have a form in my dialog that saves sends stuff to a server. If the server responds with an error I want to abort the closing of the dialog and let the user correct the problem. As of now. I have to have the server logic inside of the dialog to be able to only close it on success.
Exactly my use-case. For me it feels a bit bad, if the Dialog-Content itself needs to decide wether to close or not. I'd love to control it in the calling service depending on the result. But yeah, going with your solution right now, too.
I'm using a parent class for my dialog components and have worked this functionality in via this methodology. Maybe this will help someone else.
I also am steering clear of mat-dialog-close and am instead using a custom close function, but this code will also allow users to still press escape and click on the overlay to close the dialog.
private ref: MatDialogRef;
constructor() {
this.ref = inject(MatDialogRef);
if (!this.ref.disableClose) {
const backdrop: HTMLElement = document.querySelector(".cdk-overlay-backdrop");
// don't forget to unsubscribe from these.
fromEvent(backdrop, "click").subscribe(() => this.close()),
fromEvent(window, "keydown")
.pipe(filter((e: KeyboardEvent) => e.key === "Escape"))
.subscribe(() => this.close())
}
}
protected close() {
// do confirmation logic here
if (canClose) this.ref.close();
}
I think I have a bit simpler workaround for this behavior (edited version):
const dialog = this.matDialog.open(FooComponent, {
...dialogConfig,
disableClose: true // This one is intentionally forced to 'true' to enable manually handling the closing
});
dialog.backdropClick()
.pipe(take(1))
.subscribe(() => {
if (!dialogConfig.disableClose /* && whatever other logic you want*/) {
dialog.close();
}
});
dialog.keydownEvents()
.subscribe(event => {
if (event.key === 'Escape' && !dialogConfig.disableClose /* && whatever other logic you want*/) {
dialog.close();
}
});
I think I have a bit simpler workaround for this behavior:
const dialog = this.matDialog.open(FooComponent, { ...dialogConfig, disableClose: true // This one is intentionally forced to 'true' to enable manually handling the closing }); dialog.backdropClick() .pipe(take(1)) .subscribe(() => { if (!dialogConfig.disableClose /* && whatever other logic you want*/) { dialog.close(); } }); dialog.keydownEvents() .pipe(take(1)) .subscribe(event => { if (event.key === 'Escape' && !dialogConfig.disableClose /* && whatever other logic you want*/) { dialog.close(); } });
I think you might want to remove the .pipe(take(1)) from the keydownEvents but otherwise that looks nice! I may be wrong though.
Building off the recent workarounds, I decided to have a simple helper function that can be called from the constructor of any Dialog Component to add the desired behavior of allowing close on ESC key but disabling on backdrop click. This also allows an instance of a dialog to override this default behavior by setting disableClose: false
export function disableClose(dialogRef: MatDialogRef<any>) {
if (dialogRef.disableClose !== false) {
dialogRef.disableClose = true;
fromEvent(window, 'keydown')
.pipe(
takeWhile(() => dialogRef.getState() !== MatDialogState.CLOSED),
filter((e: KeyboardEvent) => e.key === 'Escape')
)
.subscribe(() => {
dialogRef.close();
});
}
}