Angular CanDeactivate Guard Not Working Properly With MatDialog

2.1k views Asked by At

I've created a route guard with CanDeactivate that determines if a user can navigate away from a page if they have any unsaved changes, and triggers a MatDialog modal if any unsaved changes exist. I've looked at plenty of guides and similar threads and my code is working (for the most part). See below:

Here's the route guard, it calls the component's confirmRouteChange function, if it has one, and returns the result, an Observable<boolean>:

@Injectable({
    providedIn: 'root'
})
export class UnsavedChangesGuard implements CanDeactivate<DirtyComponent> {
    canDeactivate(component: DirtyComponent): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
        return component?.confirmRouteChange ? component.confirmRouteChange() : true;
    }
}

Here's the implementation of confirmRouteChange, it just opens the MatDialog if the form is dirty and returns the Observable that notifies when the dialog is closed:

confirmRouteChange() {
        if (this.isDirty) {
            let dialogRef = this.matDialog.open(AlertModalComponent, {
                data: {
                    msg: 'Your changes have not been saved. If you leave this page, your changes will be lost.',
                    title: 'Changes have not been saved',
                    class: 'save-changes'
                }
            });

            return dialogRef.afterClosed();
        } else {
            return true;
        }
    }

And here's the implementation of my modal's save/close options:

save() {
        this.dialogRef.close(false);
    }

    close() {
        this.dialogRef.close(true);
    }

So here's my problem: when I navigate away with unsaved changes, the modal pops up (great), the app navigates properly according to the choice selected in the modal (great); however, behind the modal I can see that the rest of my app navigates away from the page EXCEPT for the component that the route guard is placed on. So if I attempt to navigate to the homepage, I can see all of the homepage AND the component that with my form of unsaved changes. It's as if it starts navigating, stops, and THEN it waits on the user's response. If I use the native confirm() dialog, it works perfectly meaning the app effectively freezes while it waits for the user response. I believe it's because if I log what confirm() returns, it does not return anything until the user selects something. On the other hand, dialogRef.afterClosed() returns the Observable immediately, which I guess triggers navigation, and then the app stops and waits for the user's response.

I think there's something wrong with my usage of the modal, but I'm not sure what. Any help is greatly appreciated!

Edit: I think the return dialogRef.afterClosed() statement is executing before the dialog has finished opening, causing the navigation to start and then stop.

2

There are 2 answers

1
AudioBubble On BEST ANSWER

It is exactly the way you describe it.

Your route guard expects immediately true or false. It isn't capable of dealing with observables.

So my suggestion is to do the following.

First

Have your check, but directy return false, when the component is dirty so that the route guard prevents a rerouting. Build up a service, that stores the new route and contains a field named, for instance, reroutingAllowed. This is initially set to false.

canDeactivate(component: DirtyComponent): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return component?.isDirty();
}

confirmRouteChange() {
    // here we already take the new field into account
    if (this.isDirty && !reroutingAllowed) {
        let dialogRef = this.matDialog.open(AlertModalComponent, {
            data: {
                msg: 'Your changes have not been saved. If you leave this page, your changes will be lost.',
                title: 'Changes have not been saved',
                class: 'save-changes'
            }
        });

        // directly return false
        return false;
    } else {
        return true;
    }
}

Second

Momorize parallely the new route target the user wanted to go to in your service in order to have access to it when the user met its decission in the modal dialog.

Third

Wait for the user to answer the dialog. If he wants to leave the current page set the variable reroutingAllowed to true and trigger the Angular router to call the new route target again.

This time the guard will let the user pass, event though the form is still dirty.

You just have to think about how to set up the mentioned service an how to get and memorize the new route target.

0
Noy Oliel On

I had the exact same issue. I was able to solve it by simply converting the observable to a promise.

instead of:

dialogRef.afterClosed()

This should solve the issue:

dialogRef.afterClosed().toPromise()