16

I am using Angular Material (5.0.0) to create a Single Page Web Application for mobile devices. I have a scenario when I need to display a dialog. I would like to allow the user to hit back to close the dialog, as it's a very common behavior on mobile (especially on Android).

When this happens currently, the page goes to the previous page. I instead need the button to simply dismiss the dialog.

Any ideas on how this might be accomplished??

Brian Bauman
  • 1,000
  • 1
  • 9
  • 23
  • You could isolate a browser back action - [like this](https://stackoverflow.com/questions/25806608/how-to-detect-browser-back-button-event-cross-browser) - and prevent it, then perform your desired action. – bazzells Dec 08 '17 at 23:23

14 Answers14

16

This option is already available

let dialogRef = dialog.open(DialogExample, {
  height: '400px',
  width: '600px',
  closeOnNavigation: true
});

Other ways using routes changes events:

1. From app component

  constructor(router: Router, matDialog: MatDialog) {

    // Close any opened dialog when route changes
    router.events.pipe(
      filter((event: RouterEvent) => event instanceof NavigationStart),
      tap(() => this.matDialog.closeAll())
    ).subscribe();
  }

2. From dialog component

@Component({
  selector: 'example-dialog',
  templateUrl: 'example-dialog.html',
})
export class ExampleDialog {

  constructor(
    public dialogRef: MatDialogRef<ExampleDialog>,
    router: Router
  ) {

    // Close dialog ref on route changes
    router.events.pipe(
      filter((event: RouterEvent) => event instanceof NavigationStart),
      tap(() => this.dialogRef.close()),
      take(1),
    ).subscribe();
  }

}
Murhaf Sousli
  • 12,622
  • 20
  • 119
  • 185
  • 10
    I don't believe this answers the question. I understand that the back button should close the dialog *instead* of navigating back. These examples I think will only close the dialog not prevent navigation? – ZNS May 06 '19 at 20:04
  • 3
    @ZNS I answered the question, but maybe you should ask "how to dismiss browser's back navigation" instead, There might an angular way to do it, and when you get that function, just inject the dialog service and use `matDialog.closeAll()` – Murhaf Sousli Jul 18 '19 at 19:54
9

About the below solution I was inspired from @valeriy-katkov answer, improved and simplified.

1. Configure dialog not to close on navigation

closeOnNavigation

this.dialog.open(Component, {
      ...
      closeOnNavigation: false
    });

2. Add CanActivateChild guard to the root route.

The routes configuration:

const rootRoutes: Routes = [{
    path: '',
    canActivateChild: [ CanActivateChildGuard ],
    children: [
        ...
    ]
}];

3. The guard will cancel the navigation on Browser Back if an dialog is open and it will close the dialogs instead.

simple guard:

export class CanActivateChildGuard implements CanActivateChild {
    constructor(private dialog: MatDialog) {}

    canActivateChild(): boolean {
        if (this.dialog.openDialogs.length > 0) {
            this.dialog.closeAll();
            return false;
        } else {
            return true;
        }
    }
}
Vassilis Blazos
  • 1,560
  • 1
  • 14
  • 20
  • The Guard closes all dialogs. Is there an adaption on how to close only the currently opened in foreground / the dialog opened lastly? – MojioMS Sep 02 '20 at 07:31
  • This is most compressive solution. Another solution would be to use a queryParam on the route, but that has a drawback to store the history, so if a user opens a dialog multiple times then the browser back button will trigger the dialog multiple time – albanx Mar 02 '22 at 00:27
7

As a workaround you can add CanActivateChild guard to the root route. The guard will cancel navigation if an dialog is open and close the dialog.

The routes configuration:

const rootRoutes: Routes = [{
    path: '',
    canActivateChild: [ CanActivateChildGuard ],
    children: [
        ...
    ]
}];

And the guard:

export class CanActivateChildGuard implements CanActivateChild {
    constructor(
        private readonly router: Router,
        private readonly location: Location
    ) {}

    canActivateChild(route: ActivatedRouteSnapshot): boolean {
        if (this.dialog.openDialogs.length > 0) {
            // fix navigation history, see github issue for more details
            // https://github.com/angular/angular/issues/13586
            const currentUrlTree = this.router.createUrlTree([], route);
            const currentUrl = currentUrlTree.toString();
            this.location.go(currentUrl);

            this.dialog.closeAll();
            return false;
        } else {
            return true;
        }
    }
}
Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123
  • 1
    TS2339: Property 'go' does not exist on type 'Location'. – MojioMS Oct 28 '20 at 12:58
  • this solution has an issue that looks like an angular bug: you press back first time it closes the dialog, you press back second time does nothing, you press back third times takes you back 2 steps. – albanx Mar 02 '22 at 01:13
2

You can inject the MatDialog service and call closeAll method to close all the opened dialogs as below,

constructor(public dialog: MatDialog) {
         this.dialog.closeAll();
}

LIVE DEMO

Aravind
  • 40,391
  • 16
  • 91
  • 110
  • Maybe the main problem is with that back button? – Martin Nuc Dec 08 '17 at 23:58
  • not like that. which component are you navigating by clicking `back` button. – Aravind Dec 09 '17 at 00:07
  • 4
    I might not have been clear. I am on my component, lets say MainComponent. The user takes some action that launches a dialog. The user hits (browser) back to cancel the dialog. Currently, the router is handling the change of URL and showing the previous component (and also taking the dialog down). I want them to remain on the MainComponent (not changing the component) and simply take the dialog down. I imagine to accomplish this I need to have the dialog some how change the URL so the browser back changes the URL back to the original component URL, but not sure how. – Brian Bauman Dec 10 '17 at 02:53
2

Following is my solution to close dialog on back keypress, it is working perfectly in android mobile and browser

DialogComponent{
...

  ngOnInit() {
    this.handleBackKey();
  }

  handleBackKey() {
    window.history.pushState(null, "Back", window.location.href);

    this.dialog.afterClosed().subscribe((res) => {
      window.onpopstate = null;
      window.history.go(-1);
    });

    window.onpopstate = () => {
      this.dialog.close();
      window.history.pushState(null, "Back", window.location.href);
    };
  }

....
}
PARAMANANDA PRADHAN
  • 1,104
  • 1
  • 14
  • 23
1

I figured out that if you call history.pushState() upfront opening the Modal, you can "close" the modal by navigating back.

...
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = false;
dialogConfig.autoFocus = false;
dialogConfig.closeOnNavigation = true;

history.pushState({ foo: "bar" }, "Image", "/currentpage#");    

return this.dialog.open(PictureGalleryComponent, dialogConfig).afterClosed();
...

Also, [mat-dialog-close]="true" still works since the hash does not harm the current url.

A bit hacky still.

yglodt
  • 13,807
  • 14
  • 91
  • 127
  • I'm doing history.pushState({ }, "oAdmin", '/#'+this.router.url); when usign hash, it also works when have multiple dialogs open – alexOtano May 19 '19 at 19:01
1

I was also trying to achieve this and found another great approach ...

Make the dialog its own route!

There's a great article here to explain it: https://medium.com/ngconf/routing-to-angular-material-dialogs-c3fb7231c177

The basic steps:

  • Add a child route, to the route that wants to open a dialog
  • Add <router-outlet></router-outlet> to the html of the route that wants to open a dialog
  • When user clicks button (or equivalent) and dialog should open, navigate to that child route
  • When child route component is constructed, open the dialog
  • In the new component, handle dialog closing, and navigate to parent route

It worked really well for my scenario. It has the benefit of the dialog being a route, so you can even link to it, and when developing and working on the dialog, you don't have to keep reopening it!

Greg Jackman
  • 686
  • 7
  • 9
1

What I'm doing is taking advantage of the MatDialogConfig.closeOnNavigation option and the browser's history.

The idea is basically to duplicate the current state in the browser history when the dialog component gets initialized, and to set to true the closeOnNavigation property, so that when the user clicks back on his browser, the dialog closes but he remains in the same url (because we duplicated it on the history). It might not be the nicest solution but it seems to be working fine.

The whatever.component.ts opening the dialogue:

    export class WhateverComponent {

      constructor(dialog: MatDialog) {
      }

      openDialog() {
        this.dialog.open(MyDialog, {
          closeOnNavigation: true
        }).backdropClick().subscribe(e => history.back());
      }

    }

The actual dialog.component.ts:

    export class MyDialog implements OnInit {
        constructor(public dialogRef: MatDialogRef<MyDialog>,
                    @Inject(PLATFORM_ID) private platformId: Object) {
        }

        public ngOnInit() {
            if (isPlatformBrowser(this.platformId)) {
                history.pushState({}, document.getElementsByTagName('title')[0].innerHTML, window.location.href);
            }
        }
    }

I wrap it inside a isPlatformBrowser because I'm worried it would throw some errors with SSR.

RTYX
  • 1,244
  • 1
  • 14
  • 32
  • Please add the following to where the Dialog is opened to restore the history if the dialog is closed by clicking the backdrop. this.dialog.open(DialogComponent, { closeOnNavigation: true, }).backdropClick().subscribe(e => { history.back();}); Otherwise this works pretty well and is quick to implement – Timothy Louw Oct 11 '19 at 06:59
1

my solution

this.router.navigate([window.location.pathname], {
    fragment: post._id,
})

const dialogRef = this.dialog.open(PostComponent, {

})

This is your url when opening the dialog

https://xxxx.com/posts#hash

On back

https://xxxx.com/posts

Moon
  • 259
  • 1
  • 2
  • 12
0

you could listen to the popstate and then do something such as close a dialog.

import { HostListener } from '@angular/core';
  @HostListener('window:popstate', ['$event'])
  onPopState(event) {
      this.dialog.closeAll();

}

or using Location..

import {Location} from "@angular/common";

constructor(private location: Location) { }

ngOnInit() {
   this.location.subscribe(x => if back then close dialog);
}
Taranjit Kang
  • 2,510
  • 3
  • 20
  • 40
0

When ever you open the dialog, add a query param to your url

ex: /test?dlgopen=true

when you close the dialog, remove the dlgopen from you url and the rest will be handled by your browser automatically. Home it helps

0

My solution is to open all dialogs with closeOnNavigation:false and then use this code, even works with overlapping dialogs

// push history state when a dialog is opened
dialogRef.afterOpened.subscribe((ref: MatDialogRef<any, any>) => {

  // when opening a dialog, push a new history entry with the dialog id
  location.go('', '', ref.id);

  ref.afterClosed().subscribe(() => {
    // when closing but the dialog is still the current state (because it has not been closed via the back button), pop a history entry
    if (location.getState() === ref.id) {
      history.go(-1);
    }
  });

});

location.subscribe((event: PopStateEvent) => {
  const frontDialog = dialogRef.openDialogs[dialogRef.openDialogs.length - 1];
  // when the user hits back, the state wont equal the front popup anymore, so close it
  // when a popup was closed manually, the state should match the underlying popup, and we wont close the current front
  if (frontDialog && location.getState() !== frontDialog.id) {
    frontDialog.close();
  }
});
wutzebaer
  • 14,365
  • 19
  • 99
  • 170
0

Maybe this approach will help you..

THIS CODE GOES INSIDE THE ACTUAL DIALOG COMPONENT in that case PesonComponent

import { Location } from '@angular/common';


constructor(
     private location: Location,
     @Optional() @Inject(MAT_DIALOG_DATA) private person: Person,
     @Optional() private personDialogRef: MatDialogRef<PersonComponent>
) { }

now just subscribe to afterOpened and afterclosed and change the URL accordingly

ngOnInit(): void {

    let currentUrl = this.location.path(); // saving current URL to update afterClosed


    this.orderInfoDialogRef.afterOpened().pipe(
      takeUntil(this.onDestroy),
      tap(() => this.location.go(currentUrl + `/${this.person._id}`)) // this will change the URL to /person/123
    ).subscribe();


    this.orderInfoDialogRef.afterClosed().pipe(
      takeUntil(this.onDestroy),
      tap(() => this.location.go(currentUrl)) // this will revert the URL back to /person
    ).subscribe();

}

}

Now, touch the back button on a mobile device like Android and observe how the URL will change from /person/123 to person and the dialog will close.

if you use a button that closes the dialog, the afterClosed will also change the URL back to /person.

good luck!

user12163165
  • 555
  • 8
  • 12
0

I had a similar usecase and solved this using a CanDeactivate - Guard. The upside of this over an CanAvtivate-Guard is that you only need to set the Guard for those components that actually open a Dialog. In my application those were only 2 components.

First i set closeOnNavigation to false, so that the dialog is not closed immediately when the navigation is started:

 const matDialogRef = this.matDialog.open(AddAbilityToHeroDialogComponent,
      { ...
        closeOnNavigation: false,
      });

Second i implemented the Guard - basically i injected MatDialog and check if there are dialogs open. If so i abort the navigation and just close all dialogs.

@Injectable({
  providedIn: 'root'
})
export class CloseModalOnBrowserBackIfNecessaryDeactivateGuard implements CanDeactivate<Component> {
  constructor(private dialog: MatDialog) {
  }

  canDeactivate(component: Component, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if (this.dialog.openDialogs.length > 0) {
      this.dialog.closeAll();
      return false;
    } else {
      return true;
    }
  }
}

Finally I added the Guard to the routes that are opening Dialogs like so:

 {path: "edit/:id", component: HeroesEditComponent, canDeactivate: [CloseModalOnBrowserBackIfNecessaryDeactivateGuard]}
Mario B
  • 2,102
  • 2
  • 29
  • 41