Brian Bauman
Brian Bauman

Reputation: 1150

Close Angular Material Dialog On Browser Back

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

Upvotes: 22

Views: 21928

Answers (15)

B4ckup
B4ckup

Reputation: 33

I came up with a somewhat different answer that worked more consistently with my use case:

Subscribe to afterOpened of the dialog service and create a history entry when dialog is opened:

this._dialog.afterOpened
      .takeUntil(this)
      .subscribe(e => {
        window.history.pushState(null, null, window.location.href);
      });

Make sure to use closeOnNavigation: true when opening the dialog.

Upvotes: 0

Moon
Moon

Reputation: 279

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

Upvotes: 1

Mario B
Mario B

Reputation: 2347

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]}

Upvotes: 0

Vassilis Blazos
Vassilis Blazos

Reputation: 1610

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

Upvotes: 12

PARAMANANDA PRADHAN
PARAMANANDA PRADHAN

Reputation: 1169

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

....
}

Upvotes: 3

user12163165
user12163165

Reputation: 627

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!

Upvotes: 0

wutzebaer
wutzebaer

Reputation: 14865

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

Upvotes: 0

RTYX
RTYX

Reputation: 1334

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.

Upvotes: 1

Greg Jackman
Greg Jackman

Reputation: 696

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!

Upvotes: 1

Ramdane Oualitsen
Ramdane Oualitsen

Reputation: 410

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

Upvotes: 0

yglodt
yglodt

Reputation: 14551

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.

Upvotes: 1

Murhaf Sousli
Murhaf Sousli

Reputation: 13296

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

}

Upvotes: 22

Taranjit Kang
Taranjit Kang

Reputation: 2580

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

Upvotes: 0

Valeriy Katkov
Valeriy Katkov

Reputation: 40682

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

Upvotes: 7

Aravind
Aravind

Reputation: 41571

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

Upvotes: 2

Related Questions