Ionic Navigation

For most apps, having some sort of route is often required. In this section we will cover how routing works in this app built with Ionic and Angular.

Importance of Navigation

Navigation is one of the most important parts of an app. Solid navigation patterns help us achieve great user experience while a great router implementation will ease the development process and at the same time will make our apps discoverable and linkable.

As the new Ionic position itself as the best tool to build Progressive Web Apps, and the Discoverable and Linkable principles are fundamental to PWAs, it is clear why the new Ionic Navigation relies on the Angular Router.

We have created a detailed post about Navigation in Ionic and Angular covering many use cases. If you want to learn more about navigation, I strongly recommend you check it out.

Angular Router

The Angular Router is a solid, URL based navigation library that eases the development process dramatically and at the same time enables you to build complex navigation structures.

In addition, the Angular Router is also capable of Lazy Loading modules, handle data through route with Route Resolvers, and handling Route Guards to fine tune access to certain parts of your app.

The Angular Router is one of the most important libraries in an Angular application. Without it, apps would be single view/single context apps or would not be able to maintain their navigation state on browser reloads.

With Angular Router, we can create rich apps that are linkable and have rich animations (when paired with Ionic of course).

This Ionic 5 starter app template features many different examples of navigation within an Ionic 5 app such as: Tabs, Side Menu, Lazy Loading and Angular Resolvers.

If you are new to Angular Router I strongly recommend you to read this guide.

We can use the routerLink directive to navigate to between routes. For example, in the following code, when we press the button we will navigate to the Sign Up page.

<ion-button [routerLink]="['/auth/signup']">
    Sign Up!
</ion-button>

RouterLink works on a similar idea as typical href, but instead of building out the URL as a string, it can be built as an array, which can provide more complicated paths.

Lazy Loading

Normally when a user opens a page, the entire page’s contents are downloaded and rendered in a single go. While this allows the browser or app to cache the content, there’s no guarantee that the user will actually view all of the downloaded content.

So, that's where Lazy Loading plays an important role, instead of bulk loading all the content at once, it can be loaded when the user accesses a part of the page that requires it. With lazy loading, pages are created with placeholder content which is only replaced with actual content when the user needs it.

Lazy loading sounds like a complicated process, but actually is very straight forward. Conceptually, we’re taking one segment of code, a chunk, and loading it on demand as the app requests it. This is a very framework agnostic take on things, and the finer details here come in the form of NgModules for Ionic apps. NgModules are the way we can organize our app’s pages, and separate them out into different chunks.

You can read more about Lazy Loading routes with Ionic and Angular, or you can see the following code example:

src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: '/walkthrough', pathMatch: 'full' },
  { path: 'walkthrough', loadChildren: './walkthrough/walkthrough.module#WalkthroughPageModule' },
  { path: 'getting-started', loadChildren: './getting-started/getting-started.module#GettingStartedPageModule' },
  { path: 'auth/login', loadChildren: './login/login.module#LoginPageModule' },
  { path: 'auth/signup', loadChildren: './signup/signup.module#SignupPageModule' },
  { path: 'auth/forgot-password', loadChildren: './forgot-password/forgot-password.module#ForgotPasswordPageModule' },
  { path: 'app', loadChildren: './tabs/tabs.module#TabsPageModule' },
  { path: 'contact-card', loadChildren: './contact-card/contact-card.module#ContactCardPageModule' },
  { path: 'forms-and-validations', loadChildren: './forms/forms.module#FormsPageModule' },
  { path: 'filters', loadChildren: './filters/filters.module#FiltersPageModule' },
  { path: 'page-not-found', loadChildren: './page-not-found/page-not-found.module#PageNotFoundModule' },
  { path: '**', redirectTo: 'page-not-found' }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

This code is from our AppRoutingModule where all the app routes are defined (except the tabs routes).

Resolvers

If you were using a real world API, there might be some delay before the data to display is returned from the server. You don't want to display a blank component while waiting for the data.

It's preferable to pre-fetch data from the server so it's ready the moment the route is activated. This also allows you to handle errors before routing to the component. There's no point in navigating to a route if we don't have any data to display.

You want to delay rendering the routed component until all necessary data have been fetched and for this you need a resolver.

In this Ionic app we use resolvers for each route that needs to load data. Let's see an example of the NotificationsResolverused to prefetch the data for the NotificationsPage.

src/app/notifications/notifications.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';

import { NotificationsService } from './notifications.service';

@Injectable()
export class NotificationsResolver implements Resolve<any> {

  constructor(private notificationsService: NotificationsService) { }

  resolve() {
    return this.notificationsService.getData();
  }
}

When using resolvers, we have to add them to the route definition like this:

src/app/notifications/notifications.module.ts
RouterModule.forChild([
    {
      path: '',
      component: NotificationsPage,
      resolve: {
        data: NotificationsResolver
      }
    }
  ])

Tabs Navigation

To customize the tabs navigation options go tosrc/app/tabs/tabs.page.html

The Angular Router provides Ionic with the mechanism to know what components should be loaded, but the heavy lifting is actually done by the Ion tabs component.

In the following code we have our TabsPageRoutingModule which contains all the routes under the tabs.

If you go back to our AppRoutingModule , your will find that we have a path 'app' which loads the TabsPageModule.

app-routing.module.ts
{
  path: 'app',
  loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
},

On this Ionic app, we named the path that has the tabs “app”, but the name of the paths are open to be changed. They can be named whatever fits your app.

In that route object, we can define a child route as well. In this example, one of the top level child route is "categories" and can load additional child routes.

Change the following file to add your own pages to the tabs navigation.

src/app/tabs/tabs.router.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { TabsPage } from './tabs.page';

const routes: Routes = [
  {
    path: '',
    component: TabsPage,
    children: [
      {
        path: 'categories',
        children: [
          {
            path: '',
            loadChildren: () => import('../categories/categories.module').then(m => m.CategoriesPageModule)
          },
          {
            path: 'fashion',
            loadChildren: () => import('../fashion/listing/fashion-listing.module').then(m => m.FashionListingPageModule)
          },
          {
            path: 'fashion/:productId',
            loadChildren: () => import('../fashion/details/fashion-details.module').then(m => m.FashionDetailsPageModule)
          },
          {
            path: 'food',
            loadChildren: () => import('../food/listing/food-listing.module').then(m => m.FoodListingPageModule)
          },
          {
            path: 'food/:productId',
            loadChildren: () => import('../food/details/food-details.module').then(m => m.FoodDetailsPageModule)
          },
          {
            path: 'travel',
            loadChildren: () => import('../travel/listing/travel-listing.module').then(m => m.TravelListingPageModule)
          },
          {
            path: 'travel/:productId',
            loadChildren: () => import('../travel/details/travel-details.module').then(m => m.TravelDetailsPageModule)
          },
          {
            path: 'deals',
            loadChildren: () => import('../deals/listing/deals-listing.module').then(m => m.DealsListingPageModule)
          },
          {
            path: 'deals/:productId',
            loadChildren: () => import('../deals/details/deals-details.module').then(m => m.DealsDetailsPageModule)
          },
          {
            path: 'real-state',
            loadChildren: () => import('../real-state/listing/real-state-listing.module').then(m => m.RealStateListingPageModule)
          },
          {
            path: 'real-state/:productId',
            loadChildren: () => import('../real-state/details/real-state-details.module').then(m => m.RealStateDetailsPageModule)
          }
        ]
      },
      {
        path: 'user',
        children: [
          {
            path: '',
            loadChildren: () => import('../user/profile/user-profile.module').then(m => m.UserProfilePageModule)
          },
          {
            path: 'friends',
            loadChildren: () => import('../user/friends/user-friends.module').then(m => m.UserFriendsPageModule)
          }
        ]
      },
      {
        path: 'notifications',
        children: [
          {
            path: '',
            loadChildren: () => import('../notifications/notifications.module').then(m => m.NotificationsPageModule)
          }
        ]
      },
    ]
  },
  // /app/ redirect
  {
    path: '',
    redirectTo: 'app/categories',
    pathMatch: 'full'
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes), HttpClientModule],
  exports: [RouterModule],
  providers: [ ]
})
export class TabsPageRoutingModule {}

Side Menu Navigation

To customize the side menu go tosrc/app/app.component.html and src/app/app.component.ts

We use the ion-menu component from Ionic UI Components. You can either define the menu items in the markup or in the Component.

Let's see an example. The following code defines the Forms section of the ion-menu with two items.

With the [routerLink] you define the route where you want to navigate to when clicking the ion-item.

Remember that those routes are defined in src/app/app-routing.module.ts

The icons can be set either using Ionicons, like we do in the Form Filters option, or using custom svg icons. This template includes lots of custom and beautiful svg icons that you can use in your ionic apps.

app.component.html
<ion-list>
  <ion-list-header>
    <ion-label>Forms</ion-label>
  </ion-list-header>
  <ion-menu-toggle autoHide="false">
    <ion-item [routerLink]="['/forms-and-validations']">
      <ion-icon slot="start" src="./assets/custom-icons/side-menu/forms.svg"></ion-icon>
      <ion-label>
        Forms & Validations
      </ion-label>
    </ion-item>
    <ion-item [routerLink]="['/forms-filters']">
      <ion-icon slot="start" name="options-outline"></ion-icon>
      <ion-label>
        Filters
      </ion-label>
    </ion-item>
  </ion-menu-toggle>
</ion-list>

Also, other menu item options are defined as a list in the AppComponent and iterated in the html. Let's see an example.

app.component.ts
accountPages = [
  {
     title: 'Log In',
     url: '/auth/login',
     ionicIcon: 'log-in-outline'
  },
  {
     title: 'Sign Up',
     url: '/auth/signup',
     ionicIcon: 'person-add-outline'
  },
  ... // more pages
]
app.component.html
<ion-list>
  <ion-list-header>
    <ion-label>Account</ion-label>
  </ion-list-header>
  <ion-menu-toggle autoHide="false" *ngFor="let p of accountPages; let i = index">
    <ion-item [routerLink]="p.url">
      <ion-icon slot="start" [name]="p.ionicIcon? p.ionicIcon: ''" [src]="p.customIcon? p.customIcon: ''"></ion-icon>
      <ion-label>
        {{p.title}}
      </ion-label>
    </ion-item>
  </ion-menu-toggle>
</ion-list>

Initial route of the application

Currently, the app starts with the walkthrough, and then follows with the authentication screens. If you would like to start your app at the Categories page, simply go to src/app/app-routing.module.ts and change this line:

{ path: '', redirectTo: '/walkthrough', pathMatch: 'full' },

for:

{ path: '', redirectTo: '/app/categories', pathMatch: 'full' },

Last updated