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 4 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 4 Navigation relies on the Angular Router.

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 4 starter app template features many different examples of navigation within an Ionic 4 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 4 and Angular 7, 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).

Tabs Navigation

With Tabs, the Angular Router provides Ionic the mechanism to know what components should be loaded, but the heavy lifting is actually done by the 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.

In this Ionic app we call the path that will have the tabs “app”, but the name of the paths are open to be changed. They can be called whatever fits your app.

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

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: '../categories/categories.module#CategoriesPageModule'
},
{
path: 'fashion',
loadChildren: '../fashion/listing/fashion-listing.module#FashionListingPageModule'
},
{
path: 'fashion/:productId',
loadChildren: '../fashion/details/fashion-details.module#FashionDetailsPageModule'
},
{
path: 'food',
loadChildren: '../food/listing/food-listing.module#FoodListingPageModule'
},
{
path: 'food/:productId',
loadChildren: '../food/details/food-details.module#FoodDetailsPageModule'
},
{
path: 'travel',
loadChildren: '../travel/listing/travel-listing.module#TravelListingPageModule'
},
{
path: 'travel/:productId',
loadChildren: '../travel/details/travel-details.module#TravelDetailsPageModule'
},
{
path: 'deals',
loadChildren: '../deals/listing/deals-listing.module#DealsListingPageModule'
},
{
path: 'deals/:productId',
loadChildren: '../deals/details/deals-details.module#DealsDetailsPageModule'
},
{
path: 'real-state',
loadChildren: '../real-state/listing/real-state-listing.module#RealStateListingPageModule'
},
{
path: 'real-state/:productId',
loadChildren: '../real-state/details/real-state-details.module#RealStateDetailsPageModule'
}
]
},
{
path: 'user',
children: [
{
path: '',
loadChildren: '../user/user-profile/user-profile.module#UserProfilePageModule'
},
{
path: 'friends',
loadChildren: '../user/user-friends/user-friends.module#UserFriendsPageModule'
}
]
},
{
path: 'notifications',
children: [
{
path: '',
loadChildren: '../notifications/notifications.module#NotificationsPageModule'
}
]
},
]
},
// /app/ redirect
{
path: '',
redirectTo: 'app/categories',
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forChild(routes), HttpClientModule],
exports: [RouterModule],
providers: [ ]
})
export class TabsPageRoutingModule {}

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 new Promise((resolve, reject) => {
this.notificationsService.getData()
.then(
data => {
return resolve(data);
},
err => {
return reject();
}
);
});
}
}

Here we wait until this.notificationsService.getData() completes in order to navigate to the notifications route.

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
}
}
])