August 20, 2020
  • Engineering
  • Tutorials
  • Angular
  • Ionic
  • Navigation

How to Navigate in Ionic Modals with ion-nav

Simon Grimm

This is a guest post from Simon Grimm, Ionic Developer Expert and educator at the Ionic Academy. Simon also recently released the book Practical Ionic, a guide to building real world Ionic applications.

After my session during Ioniconf, there was a question about routing inside Ionic modals that came up during the Q&A. Because the modal is not part of your standard Angular routing, you need a different way to handle navigation inside an overlay that exists outside the rest of your application.

You can achieve the same navigation in all directions (forward, back, root) using the ion-nav component – a highly underrated component that will rescue your day.

The docs already highlight when this component really shines:

“This fits use cases where you could have a modal, which needs its own sub-navigation, without making it tied to the app’s URL.”

Let’s build an app with navigation inside of a modal and make everything work, including the Android back button as well!

Getting Started with our App

As always, let’s start with a blank Ionic app. Go ahead and generate a new module, a component inside that module and finally another page which we will use for our modal logic:

ionic start customNavigation blank --type=angular --capacitor
cd ./customNavigation

ionic g module components/sharedComponents --flat
ionic g component components/modalBase

ionic g page pages/modalContent

You don’t need any external libraries for the functionality we are going to build, everything is included in the Ionic core!

Custom Navigation Component

First of all we focus on creating a base for the modal. The purpose of this base component is to render the ion-nav component and nothing else. Therefore, start by opening the components/modal-base/modal-base.component.html and replace everything with:

<ion-nav [root]="rootPage"></ion-nav>

This isn’t a lot, but it is enough to make this component a standalone component for our navigation. Whenever we call this component, we directly want to set the root of the navigation, and therefore we also declare the variable inside the components/modal-base/modal-base.component.ts like this:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-modal-base',
  templateUrl: './modal-base.component.html',
  styleUrls: ['./modal-base.component.scss'],
})
export class ModalBaseComponent implements OnInit {
  rootPage: any;

  constructor() {}

  ngOnInit() {}
}

To use it in the pages of our Ionic application, we now also need to declare and export it in the previously generated components/shared-components.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ModalBaseComponent } from './modal-base/modal-base.component';
import { IonicModule } from '@ionic/angular';

@NgModule({
  declarations: [ModalBaseComponent],
  imports: [CommonModule, IonicModule],
  exports: [ModalBaseComponent],
})
export class SharedComponentsModule {}

With this, we can can now use the component anywhere inside our app when we need a modal with its own navigation stack!

Presenting the Modal

Now we want to actually call the modal, and therefore we can start by importing the just changed module to our home/home.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { HomePage } from './home.page';

import { HomePageRoutingModule } from './home-routing.module';
import { SharedComponentsModule } from '../components/shared-components.module';
import { ModalContentPageModule } from './../pages/modal-content/modal-content.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    HomePageRoutingModule,
    SharedComponentsModule,
    ModalContentPageModule,
  ],
  declarations: [HomePage],
})
export class HomePageModule {}

We also import the ModalContentPageModule, because that’s going to be the real content of the modal and we will use the page directly (not just as a string!) when we open the modal. That’s why we need to import the module of the page as well.

Let’s quickly add a button to our view before we start with the real functionality, so open the home/home.page.html and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Modal Nav
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-button expand="full" (click)="presentModal()">Open Modal</ion-button>
</ion-content>

Now we can present the modal, and we will basically present our ModalBaseComponent which we define as the component for the modal, but we also pass the rootPage to the modal (inside the componentProps), which is now the ModalContentPage.

That means, the base component will be prevented, but that’s actually only the container for our navigation. At the same time, the content page is used as the root page of that navigation, so that’s going to be the page we see inside our app when the modal opens!

Go ahead now and change the home/home.page.ts to this:

import { Component } from '@angular/core';
import { ModalController, IonRouterOutlet } from '@ionic/angular';
import { ModalBaseComponent } from '../components/modal-base/modal-base.component';
import { ModalContentPage } from '../pages/modal-content/modal-content.page';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  constructor(
    private modalController: ModalController,
    private routerOutlet: IonRouterOutlet
  ) {}

  async presentModal() {
    const modal = await this.modalController.create({
      presentingElement: this.routerOutlet.nativeEl,
      component: ModalBaseComponent,
      componentProps: {
        rootPage: ModalContentPage,
      },
    });

    await modal.present();
  }
}

At this point you should be able to open the modal and see the dummy name of the content page. If you get strange errors, restart the serve command. Sometimes it doesn’t immediately pick up the generated files.

Working with the ion-nav Component

Now that we arrived in the modal it’s time for the navigation to new pages. We can perform this both directly from the view, or from code by injecting the ion-nav.

The ion-nav component comes with a bunch of methods, and we can make use of them to navigate to another page (push), we can go back to our root page from anywhere, or you can use any of the other methods of the component to manipulate the view stack!

We’ll keep it simple for now and only use two of them. Besides that, we will track the level of our page because we are actually reusing the same content page when we push another page on the stack.

That’s the reason why we define the nextPage to be our current component – we need a reference to this when we use the ion-nav-link in the next step.

For now, open the pages/modal-content/modal-content.page.ts and change it to:

import { Component, OnInit } from '@angular/core';
import { ModalController, IonNav, Platform } from '@ionic/angular';

@Component({
  selector: 'app-modal-content',
  templateUrl: './modal-content.page.html',
  styleUrls: ['./modal-content.page.scss'],
})
export class ModalContentPage implements OnInit {
  level = 0;
  nextPage = ModalContentPage;

  constructor(private modalController: ModalController, private nav: IonNav) {}

  ngOnInit() {}

  goForward() {
    this.nav.push(this.nextPage, { level: this.level + 1 });
  }

  goRoot() {
    this.nav.popToRoot();
  }

  close() {
    this.modalController.dismiss();
  }
}

Now we throw the other component that’s used with this custom navigation into the mix, the ion-nav-link component.

As said before, you can do the custom navigation from code (like we did in the snippet before) or directly from the view. And to do so, you just need to define a few things on the component:

  • component: The component you want to push or set as root
  • componentProps: Just like the props when opening a modal – we simply increase the level variable for the next page
  • routerDirection: One of back, forward or root

By specifying and combining these things on the ion-nav-link, you can basically achieve any kind of navigation change right from here! It’s also a common navigation pattern I share more about inside my book Practical Ionic.

Besides that, we can also still use the standard ion-back-button in this navigation, but in our example below we will only show it on higher levels and otherwise display the close button for a modal. But that’s just an idea, you could combine this to your likes.

Inside the content of the page we keep the different buttons which either call our navigation from code or use the component with all properties specified to change the routing.

Put the code below into your pages/modal-content/modal-content.page.html now:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-button (click)="close()" *ngIf="level == 0">
        <ion-icon slot="icon-only" name="close"></ion-icon>
      </ion-button>
      <ion-back-button *ngIf="level > 0"></ion-back-button>
    </ion-buttons>
    <ion-title>My Modal Level: {{ level }}</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <!-- Not really necessary because of ion-back-button! -->
  <ion-nav-link router-direction="back" *ngIf="level > 0">
    <ion-button expand="full" color="medium">
      <ion-icon name="arrow-back" slot="end"></ion-icon>
      ion-nav go back
    </ion-button>
  </ion-nav-link>

  <!-- Open another modal -->
  <ion-nav-link
    router-direction="forward"
    [component]="nextPage"
    [componentProps]="{level: level+1}"
  >
    <ion-button expand="full" color="success">
      <ion-icon name="arrow-forward" slot="end"></ion-icon>
      Go to level {{ level + 1 }}
    </ion-button>
  </ion-nav-link>

  <!-- Open another modal from code -->
  <ion-button expand="full" color="secondary" (click)="goForward()">
    <ion-icon name="arrow-forward" slot="end"></ion-icon>
    Next Level from Code
  </ion-button>

  <!-- Go back to the first open modal in the view stack -->
  <ion-nav-link
    router-direction="root"
    [component]="nextPage"
    [componentProps]="{level: 0}"
    *ngIf="level > 0"
  >
    <ion-button expand="full" color="tertiary">
      <ion-icon name="arrow-back" slot="end"></ion-icon>
      Go to root
    </ion-button>
  </ion-nav-link>

  <!-- Go back to first modal from code -->
  <ion-button expand="full" color="dark" (click)="goRoot()" *ngIf="level > 0">
    <ion-icon name="arrow-back" slot="end"></ion-icon>
    Go root from Code
  </ion-button>
</ion-content>

Now we got a full navigation concept right inside a modal. We can go forward (basically infinite times), we can pop a page back (in different ways) and we can also navigate back to the root page of the modal.

But I already hear you crying because..

Android Hardware / Software Back Button

Yes, the hardware or software back button on Android won’t really respect our cool navigation concept. What a shame.

But it’s your lucky day, since you have full power over this button and how it behaves inside your Ionic app!

The only thing you need to do is define your own handler for the back button, and perform the correct operation in it. That means, we can check whether we can still go back a page and pop to the previous page, or otherwise close the modal as we are already on the root page.

Simply change the current constructor of the pages/modal-content/modal-content.page.ts to this now:

constructor(private modalController: ModalController, private nav: IonNav, private platform: Platform) {
  this.platform.backButton.subscribeWithPriority(101, async () => {
    let canGoBack = await this.nav.canGoBack();
    if (canGoBack) {
      this.nav.pop();
    } else {
      await this.modalController.dismiss();
    }
    return;
  });
}

This overwrites the default behaviour, because the priority of the modal is 100, which means our action is more important!

Capacitor Device Testing

If you finally want to try this on a device, you need to build the app once, add the platforms your want and then run the app:

ionic build

npx cap add ios
npx cap add android

ionic cap run ios --livereload --host=0.0.0.0

While testing I really prefer the livereload of Capacitor, as this will open Android Studio or Xcode, and you only need to deploy to your device once and then enjoy the same livereload that you are already used from your browser!

Conclusion

When you are inside an overlay (modal, popover..) there’s no routing since these elements live outside the pages and the URL routing of the Angular router.

Therefore, the ion-nav and ion-nav-link are a perfect substitution to building your own navigation, like inside a nested menu with multiple levels or other scenarios where you just quickly want to navigate deeper into a page.

If you want to learn more about Ionic with a library of 40+ courses and supportive community, you can also join the Ionic Academy and get access to a ton of learning material.

And don’t forget to subscribe to my YouTube channel for fresh Ionic tutorials coming every week!


Simon Grimm