Implementing a Master Detail Pattern in Ionic 4 with Angular Routing

Implementing a Master Detail Pattern in Ionic 4 with Angular Routing

Follow Josh Morony on

Earlier this week, I published an article that discussed how and why you should use Angular routing with Ionic 4.x. To summarise, the main points from that article were that:

  • Angular routing primarily relies on defining a bunch of routes that link to specific components/pages, and then using the current URL to determine which route should be activated
  • Push/pop navigation will still be available through Ionic’s components, but it is not recommended for Angular
  • There are big benefits to switching to using Angular routing with Ionic/Angular

If you haven’t already read the previous article I would recommend that you do if you do not already understand how to use Angular routing and lazy loading. This article will be easier to understand with that background knowledge.

Once you have routing set up in your application, there isn’t really much to worry about. You just set up the routes, then link between them as you please. However, using Angular routing does change the ways in which you can pass data between pages. Implementing a Master/Detail pattern (i.e. where you display a list of items on one page and the details for that item on another page) is extremely common in Ionic applications, and I think this is a concept a lot of developers will quickly butt heads with after switching to Angular routing.

I will be covering how to deal with this scenario in Ionic applications that use Angular routing.

Passing Data to Another Page with Push/Pop

In Ionic 3.x, it is common to use the NavController and NavParams to pass data around in an Ionic application. Typically, if you wanted to implement a master/detail pattern you might have a list set up like this:

<ion-item (click)="viewDetail(todo)" *ngFor="let todo of todos">
	{{ todo.title }}
</ion-item>

Each todo item has an event binding that will pass the specific todo item onto a function, which might look like this:

viewDetail(todo){
    this.navCtrl.push('DetailPage', {todo: todo});
}

This pushes the new page on to the navigation stack and sends the todo along with it. That object can then be retrieved using NavParams on the DetailPage.

Passing Data to Another Page with Angular Routing

There are a few methods for navigating between routes and passing data with Angular routing. Primarily, we would often just be using a routerLink:

<ion-button [routerLink]="'/detail/' + todo.id">

If we want to supply data to the page we are linking to, we can just add it to the URL (assuming that a route is set up to accept that data). You can also navigate to another page programmatically using the navigateBack, navigateForward, or navigateRoot methods of the new NavController that Ionic provides.

this.navCtrl.navigateForward(`/detail/${todo.id}`);

We are using string substitution here to add the data to the URL (since it is neat), but you could form the URL string any way you want. You could also use the navigate method that the Angular router provides if you like, which will allow you to supply additional parameters. This allows you to create more complex routes, like this example from the Angular documentation:

this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });

In most cases, you would likely just be using routerLink and where it is necessary to get functions involved you would mostly only need to use navigateBack, navigateForward, or navigateRoot (and it is important to use these methods through the NavController as they will apply the screen transition animations correctly in Ionic). In all of these cases, we are relying on sending the data through the URL. So, if we want to pass some object from one page to another, this isn’t a suitable method to do that. You could send all of the data for your object through the URL by turning your object into a JSON string, but it’s not an entirely practical solution in a lot of cases.

Implementing Master/Detail with Angular Routing

Usually, the best way to tackle this situation is to simply pass an id through the URL, and then use that id to grab the rest of the data through a provider/service. We would have our routes set up like this:

const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', loadChildren: './pages/home/home.module#HomeModule' },
  { path: 'detail/:id', loadChildren: './pages/detail/detail.module#DetailModule' }
];

This will allow our Detail page to accept an id parameter. All we need to do is supply that id in the URL:

http://localhost:8100/detail/12

If all we are doing is passing an id, that means we need to be able to grab the entire object from somewhere using that id. To do that, we would create a service to hold/retrieve that data:

todo.service.ts

import { Injectable } from '@angular/core';

interface Todo {
  id: string,
  title: string,
  description: string
}

@Injectable({
  providedIn: 'root'
})
export class TodoService {

  public todos: Todo[] = [];

  constructor() { 

    // Set some test todos
    this.todos = [
      { id: 'todo-one', title: 'Todo One', description: 'Do Stuff' },
      { id: 'todo-two', title: 'Todo Two', description: 'Do Stuff' },
      { id: 'todo-three', title: 'Todo Three', description: 'Do Stuff' },
      { id: 'todo-four', title: 'Todo Four', description: 'Do Stuff' },
      { id: 'todo-five', title: 'Todo Five', description: 'Do Stuff' },
      { id: 'todo-six', title: 'Todo Six', description: 'Do Stuff' },
      { id: 'todo-seven', title: 'Todo Seven', description: 'Do Stuff' }
    ];

  }

  getTodo(id): Todo {
    return this.todos.find(todo => todo.id === id);
  }

}

This is just a very simple service with some dummy data, but it will allow us to access the data, and retrieve a specific “todo” by using the getTodo function. We would inject that service into our master page:

home.page.ts:

import { Component } from '@angular/core';
import { TodoService } from '../../services/todo.service';

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

  constructor(private todoService: TodoService){

  }

}

and then we would then display this data in our master template like this:

home.page.html

<ion-list lines="none">
    <ion-item [routerLink]="'/detail/' + todo.id" detail="true" *ngFor="let todo of todoService.todos">
        <ion-label>{{ todo.title }}</ion-label>
    </ion-item>
</ion-list>

We are looping over the data int the TodoService, and for each todo we set up an routerLink that passes just the id of the todo onto the route. This allows each item to be clicked to activate the detail route with the appropriate id.

Then on our detail page, we need to grab that id and then use it to retrieve the appropriate todo:

detail.page.ts

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { TodoService } from '../../services/todo.service';

@Component({
  selector: 'app-page-detail',
  templateUrl: 'detail.page.html',
  styleUrls: ['detail.page.scss'],
})
export class DetailPage {

  private todo;

  constructor(private route: ActivatedRoute, private todoService: TodoService){

  }

  ionViewWillEnter(){
    let todoId = this.route.snapshot.paramMap.get('id');
    this.todo = this.todoService.getTodo(todoId);
  }

}

We can inject the ActivatedRoute to get a snapshot of the id value that is provided as a URL parameter. We then use that id to grab the specific todo we are interested in from the TodoService, and then we can display it in our detail template:

detail.page.html

<ion-header>
  <ion-toolbar>
    <ion-title>
      {{ todo?.title }}
    </ion-title>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <p>{{ todo?.description }}</p>
</ion-content>

Summary

The method for creating a master/detail pattern in Ionic 4 with Angular routing is noticeably more difficult because we are now required to have this intermediary service. However, I don’t think this should be viewed as a negative, it is a good thing in the way that it:

  1. Encourages you to design your application in a more modular way – if you have some entity in your application like a “Todo” it should have a service to handle operations associated with it anyway
  2. It allows for easier navigation by URL. If you link directly to a detail page everything will just work as expected, since all the information required is there. In applications where you pass the object through NavParams, if you link directly to a detail page it will be missing required information (and so you need to handle that case).
Check out my latest videos: