components icon indicating copy to clipboard operation
components copied to clipboard

Mat Dialog: beforeClosed() should have an option to cancel

Open knoefel opened this issue 7 years ago • 25 comments

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?

knoefel avatar Nov 27 '18 15:11 knoefel

👍 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.

juristr avatar Feb 04 '19 10:02 juristr

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.

juristr avatar Feb 04 '19 13:02 juristr

+1

dneukirchen avatar Feb 27 '19 12:02 dneukirchen

+1. Any news about this?

mikeandtherest avatar Dec 09 '19 03:12 mikeandtherest

+1 I'd like to wait with closing the dialog until it's output has been successfully stored in the backend.

matze1234 avatar Dec 17 '19 15:12 matze1234

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

pionnegru avatar Jan 10 '20 08:01 pionnegru

+1

szabolcssimonyi avatar Jun 24 '20 19:06 szabolcssimonyi

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 avatar Jun 25 '20 06:06 marty30

@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?

szabolcssimonyi avatar Jun 25 '20 06:06 szabolcssimonyi

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(); } });

Krivobarac avatar Jan 26 '21 14:01 Krivobarac

just landed here. interesting read, still open. i bump and will implemented suggestion in the meantime.

janpauldahlke avatar Feb 26 '21 15:02 janpauldahlke

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 :)

bnohad avatar May 03 '21 12:05 bnohad

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>

osca2000 avatar May 11 '21 05:05 osca2000

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:

  1. onNoClick() from the template
  2. this.dialogRef.close() which was replaced with this.closeDialog()
  3. this.closeDialog()
  4. 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 original close function was never hijacked then: this.dialogRef.close(MY_RESULT);

bnohad avatar May 11 '21 07:05 bnohad

Your solution 2 works perfect. Thanks

osca2000 avatar May 17 '21 03:05 osca2000

+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.

arman-g avatar Oct 31 '21 18:10 arman-g

+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.

angelaki avatar Nov 02 '21 17:11 angelaki

+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?

leandroprinsich avatar Apr 11 '22 19:04 leandroprinsich

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)

Plonq avatar May 10 '22 06:05 Plonq

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.

teolag avatar May 12 '23 07:05 teolag

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.

angelaki avatar May 12 '23 08:05 angelaki

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();  
}

Will-at-FreedomDev avatar Jul 17 '23 18:07 Will-at-FreedomDev

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();
        }
    });

grozdanovgg avatar Oct 31 '23 09:10 grozdanovgg

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.

Will-at-FreedomDev avatar Oct 31 '23 13:10 Will-at-FreedomDev

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();
      });
  }
}

drc-nloftsgard avatar Feb 27 '24 21:02 drc-nloftsgard