Reputation: 1150
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
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
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
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
Reputation: 1610
About the below solution I was inspired from @valeriy-katkov answer, improved and simplified.
1. Configure dialog not to close on navigation
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
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
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
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
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
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:
<router-outlet></router-outlet>
to the html of the route that wants to open a dialogIt 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
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
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
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
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
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