4

In my Ionic 5 application I have the following navigation path.

PageHome -> PageA ->  PageB

I have implemented CanDeactivate guard for PageA.

export class LeavePageGuard implements CanDeactivate<isDeactivatable>{
  canDeactivate(
    component: isDeactivatable
  ): Observable<boolean> | Promise<boolean> | boolean {
    return component.canPageLeave();
  }
}

When user edit something and press back button before saving I am raising a popup to confirm if user wants to leave.

  async canPageLeave() {

    if (this.byPassNav) {
      this.byPassNav = false;
      return true;
    }
    if (JSON.stringify(this.dataOld) != JSON.stringify(this.data)) {

      const alert = await this.alertCtrl.create({
        header: 'Unsaved Chnages',
        message: 'Do you want to leave?',
        buttons: [
          {
            text: 'No',
            role: 'cancel',
            handler: () => { }
          },
          {
            text: 'Yes'),
            role: 'goBack',
            handler: () => { }
          }
        ]
      });
      await alert.present();
      let data = await alert.onDidDismiss();
      if (data.role == 'goBack') {
        return true;
      } else {
        return false;
      }
    } else {
      return true;
    }
  }

To move forward to PageB I am using a boolean byPassNav. I am setting this value to TRUE before moving forward and the method canPageLeave is returning TRUE.

The forward navigation is not working in one scenario except the following.

on PageA change some data and click on back button -> Confirmation pop up will open -> Select No -> Confirmation pop up will close and the same page remains open. Select button to move forward to PageB.

This will move the navigation to pageB but also will make the page as Root Page and remove all route history. I can't go back from PageB after this flow.

Edit: Adding the code for isDeactivatable

export interface isDeactivatable {
    canPageLeave: () => Observable<boolean> | Promise<boolean> | boolean;
}
Tapas Mukherjee
  • 2,088
  • 1
  • 27
  • 66

2 Answers2

5

Seems like you just want to execute the canDeactivate guard when navigating back, but not when navigating forward.

If that's the case, please take a look at this working Stackblitz demo:

demo

You could avoid using the byPassNav (so that you don't need to update its value manually) and slightly update your guard in the following way:

import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from "@angular/router";
import { Observable } from "rxjs";

export interface isDeactivatable {
  canPageLeave: (
    nextUrl?: string // <--- here!
  ) => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanLeavePageGuard implements CanDeactivate<isDeactivatable> {
  canDeactivate(
    component: isDeactivatable,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return component.canPageLeave(nextState.url); // <--- and here!
  }
}

Please notice that the only change is that the canLeave() method will now get the url of the next page that the user is trying to navigate to.

With that small change, you can use the url of the next page to decide if the user should see the alert prompt or not:

async canPageLeave(nextUrl?: string) {
    if (this.status === "saved") {
      return true;
    }

    if (nextUrl && !nextUrl.includes("home")) {
      return true;
    }

    const alert = await this.alertCtrl.create({
      header: "Unsaved Chnages",
      message: "Do you want to leave?",
      buttons: [
        {
          text: "No",
          role: "cancel",
          handler: () => {}
        },
        {
          text: "Yes",
          role: "goBack",
          handler: () => {}
        }
      ]
    });

    await alert.present();

    const data = await alert.onDidDismiss();

    if (data.role == "goBack") {
      return true;
    } else {
      return false;
    }
  }

There's also another "alternative" approach that involves getting the navigation direction from the NavController.

This approach is more like a workaround because the navigation direction is actually a private property from the NavigationController, but we can still access it if we want:

async canPageLeave() {
    if (this.status === "saved") {
      return true;
    }   

    // ----------------------
    // Alternative approach
    // ----------------------
    // The direction is a private property from the NavController
    // but we can still use it to see if the user is going back
    // to HomePage or going forward to SecondPage.
    // ----------------------

    const { direction } = (this.navCtrl as unknown) as {
      direction: "forward" | "back" | "root";
    };

    if (direction !== "back") {
      return true;
    }

    const alert = await this.alertCtrl.create({
      header: "Unsaved Chnages",
      message: "Do you want to leave?",
      buttons: [
        {
          text: "No",
          role: "cancel",
          handler: () => {}
        },
        {
          text: "Yes",
          role: "goBack",
          handler: () => {}
        }
      ]
    });

    await alert.present();

    const data = await alert.onDidDismiss();

    if (data.role == "goBack") {
      return true;
    } else {
      return false;
    }
  }

This approach may sound simpler since you don't need to check the next url manually, but keep in mind that the Ionic team may remove it in the future without any notice (since it's a private property) so it may be better to just use the nextUrl like explained above.

sebaferreras
  • 44,206
  • 11
  • 116
  • 134
  • Hi @sebaferreras, firstly thank you for putting in so much effort. I tried with your solution and a single line change made it work. Which is using NavController and navigateForward method. In my navigation, I am using the angular router and trying to avoid NavController. So I tried putting 'this.router.navigateByUrl("/second");' in your code in place of 'this.navCtrl.navigateForward("/second");' and the results are same as mine. The 3rd page will have no back button. I am not sure why this is the case but if I can't make it work with router, I have to use NavController. – Tapas Mukherjee Feb 28 '21 at 21:34
  • It's not really recommended to use the router directly in Ionic apps, specially because the `NavController` is an abstraction built on top of the router to handle page transitions and some other things (setting a page as root for example doesn't mean anything in Angular, but it does in Ionic). – sebaferreras Mar 01 '21 at 14:47
  • In fact if you take a look at **[here](https://github.com/ionic-team/ionic-framework/blob/master/angular/src/providers/nav-controller.ts#L69)** and **[here](https://github.com/ionic-team/ionic-framework/blob/master/angular/src/providers/nav-controller.ts#L194)** you'll see that the `NavController` actually uses `this.router.navigateByUrl(...)` behind the scenes. But if you really need to use the router directly, you can set the direction manually like this: `this.navController.setDirection('forward'); this.router.navigateByUrl('...');` (the direction can be `'forward'`, `'back'` or `'root'`) – sebaferreras Mar 01 '21 at 14:48
  • @TapasMukherjee Sorry, forgot to tag you in the comment above :P – sebaferreras Mar 01 '21 at 14:53
  • thx for the info. I am using routers because of documentation like https://ionicframework.com/docs/reference/migration#navigation. Ionic docs are asking to use routers and saying nav controller will be deprecated in future. So im confused now. – Tapas Mukherjee Mar 01 '21 at 23:29
  • @TapasMukherjee yeah, is kind of confusing. The `NavController` in Ionic 3 was a custom routing system that Ionic was going to deprecated in Ionic 4, but then they decided to update the `NavController` to use the Angular router behind the scenes. At least that's what I think it happened. – sebaferreras Mar 02 '21 at 17:09
  • 1
    saved my day! tks – AdsHan May 13 '21 at 19:46
  • 3
    Indeed, using NavController solves the issue. But it is strange, since they say we should use Router. Thanks! – Lucas Leandro Jul 08 '21 at 18:54
0

"Interface that a class can implement to be a guard deciding if a route can be deactivated. If all guards return true, navigation continues. If any guard returns false, navigation is cancelled. If any guard returns a UrlTree, current navigation is cancelled and a new navigation begins to the UrlTree returned from the guard."[src].

In this kind of Guards, there is no random behaviour, it is all about what your guards returned ! If your one guard return an UrlTree, this one will overwrite the old one ! I think it is your case !

The isDeactivatable is it a component ? Could please add the full code ! The component in wich you want set the guard, ...

Nadhir Houari
  • 390
  • 4
  • 10
  • Hi @Nadhir, sorry for replying late. isDeactivatable is an interface declared as I have checked in the tutorials. Also, I am not returning any UrlTree and only returning a false value on PageA when users select 'No' to stay on the page. As per the above explanation when a false value is returned on PageA the navigation is canceled. This is working fine because the same page says open but moving forward to another PageB is making it the root page. Is there a way I can return UrlTree on PageA so that the navigation stack remains the same? – Tapas Mukherjee Feb 14 '21 at 20:09