Ionic 4

Implementing a Master Detail Pattern in Ionic 4 with Angular Routing



·

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 for those who want to use it
  • There are big benefits to switching to using Angular routing with Ionic/Angular 4

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. I won’t be creating a step-by-step tutorial here since Ionic/Angular 4 is not officially available yet and some of the setup/bootstrap style stuff may change before it is. Instead, I will just be covering the basic concept at a high level.

Passing Data to Another Page with Push/Pop

Currently (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 the routerLink directive:

<ion-item tappable 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 navigate or navigateByUrl methods of the Angular router.

With navigateByUrl you just supply the URL you want to go to, so if you want to pass data along with that you could just add the data to the URL:

this.router.navigateByUrl(`/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. When using the navigate method you can supply the route you want to go to along with 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 navigateByUrl. 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()
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 no-lines>
    <ion-item tappable 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 a 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).

Of course, if this style of navigation is really not your cup of tea, you can still stick with the old push/pop style navigation.

What to watch next...