Last year I’ve published a post Implementing the Master-Detail Pattern with Ionic 3, 12 months later and a new major release Ionic 4, we’ll see on this new post how to achieve it with Ionic 4 and which benefits to upgrade from Ionic 3.

Main difference between Ionic 3 and Ionic 4

  • Ionic 4 brings significant performance and build time improvements
    • Ionic 4 moves to using Web Components for each component. Web Components push more work to the browser and require less code, bringing key performance improvements to load and startup times
  • Ionic Angular apps follow Angular standards and conventions
    • fully embrace the Angular CLI and Router. Angular developers can now use the Angular CLI directly for Ionic apps and stay up-to-date with the awesome progress Angular continues to make.

To go in-depth with migration Ionic team provide a migration guide.

Application

We are going to create an employee directory App, implementing CRUD actions (Create, Read, Update, Delete).

Our main concern is to apply good/recommended practices:

  • split each feature into a lazy loaded module
  • core, shared module
  • routing module
  • generate data provider and mockup
  • create interface for data

We’ll use Ionic v4 for UI with Angular 6 and firestore as Cloud db.

Setup

Prerequisites

We need to have Node.js and Git installed in order to install both Ionic and Cordova.

$ npm install cordova ionic typescript -g
...

$ npm ls -g cordova ionic npm typescript --depth 0
/usr/local/lib
├── @angular/cli@6.2.4
├── ionic@4.2.1 
├── npm@6.4.1 
├── phonegap@8.0.0
└── typescript@2.9.2

Create a new Ionic v4 application

Create a New Ionic 4 and Angular 6 Application with

$ ionic start meu-starter.crud-angularfire.ionic-v4 blank --type=angular
$ cd meu-starter.crud-angularfire.ionic-v4

You can test the App running ionic serve cmd:

$ ionic serve
> ng run app:serve --host=0.0.0.0 --port=8100

To test iOS and Android views I recommend using @ionic/lab package

$ npm i --save-dev @ionic/lab

and run

$ ionic serve --lab

File Structure

There’s no perfect solution or unique rule to define file structure, it’s important to consider how and where your App will grow to adapt the file structure to your project. I highly recommend to read some posts (as How to define a highly scalable folder structure for your Angular project) to be aware of basic recommendations.

Lazy load an entire module that can contain multiple pages, and the components they are suppose to use.

./src
  /app
    /profiles
      profiles-routing.module.ts
      profiles.module.ts
      components/
        profile-headline
      pages/
        profiles-list
        profile-detail
      profile.interface.ts
	  services/
	  	  index.ts
	      profile.service.ts
	      profile-mock.service.ts
	      profile-mock.ts
    /core
      core.module.ts
      /services
        /api
          api.service.ts
    /shared
      shared.module.ts
      /components

At this stage you can observe some interesting points:

  • we use routing module for items, not for shared module, because we don’t expect to share any route
  • items service is “mocked”, it’s easier to test our App, consuming mock entries.

Core module

Your CoreModule contains code that will be used to instantiate your app and load some core functionality.

The clearest and most important use of the CoreModule is the place to put your global/HTTP services. The idea is to make sure only one instance of those services will be created across the entire app. The CoreModule, by convention, is only included in your entire app once in AppModule (only in the import property of the @NgModule() decorator inside your main app.module.ts, not in any other module’s import) and this will ensure services inside it will be only created once in the entire app.

Source: Angular (2+): Core vs Shared Modules

Shared module

You SharedModule similarly contains code that will be used across your app and Feature Modules. But the difference is that you will import this SharedModule into the specific Feature Modules as needed. You DO NOT import the SharedModule into your main AppModule or CoreModule.

Common templates components should also go in the SharedModule. An example would be a global button component, eg ButtonComponent. These template components should be “dumb components” in that they should not expect or interact with any specific form of data.

Module

It’s considered best practice to add routing module for each feature module. Then we’ll add 3 modules on our App (before running each cli I recommend to add --dry-run to simulate cmd):

$ ng generate module items --routing
CREATE src/app/items/items-routing.module.ts (251 bytes)
CREATE src/app/items/items.module.spec.ts (291 bytes)
CREATE src/app/items/items.module.ts (287 bytes)

$ ng g module shared --spec=false
$ ng g module core --spec=false

ItemsRoutingModule will handle any items-related routing. This keeps the app’s structure organized as the app grows and allows you to reuse this module while easily keeping its routing intact.

ItemsModule is needed for setting up lazy loading for your feature module.

We generate core and shared modules without routing and spec (--spec=false). Depending on Unit tests you pretend to implement it’s up to you to add or remove specs. Concerning routes, it’s useless for non-feature modules.

Notice: we switch between ng and ionic, but both commands commonly have same behavior. When possible we prefer to use original, then ng instead of the “alias” ionic.

Tip: to list available schematics type $ npx ng g --help

Routing

We “lazy load” ItemsModule:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: './home/home.module#HomePageModule' },
  { path: 'items', loadChildren: './items/items.module#ItemsModule' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Tips: preload a lazyloaded module adding a data object to the routes config data: { preload: true }, for ex.:

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: './home/home.module#HomePageModule', data: { preload: true } },
  { path: 'items', loadChildren: './items/items.module#ItemsModule' },
];

Pages

We create both pages required to implement master-detail pattern, we call them items-list and item-detail and save them on items feature folder.

$ ng g page items/pages/items-list --module items
CREATE src/app/items/pages/items-list/items-list.module.ts (564 bytes)
CREATE src/app/items/pages/items-list/items-list.page.scss (0 bytes)
CREATE src/app/items/pages/items-list/items-list.page.html (137 bytes)
CREATE src/app/items/pages/items-list/items-list.page.spec.ts (713 bytes)
CREATE src/app/items/pages/items-list/items-list.page.ts (271 bytes)
UPDATE src/app/items/items.module.ts (371 bytes)
$ ng g page items/pages/item-detail --module items
CREATE src/app/items/pages/item-detail/item-detail.module.ts (564 bytes)
CREATE src/app/items/pages/item-detail/item-detail.page.scss (0 bytes)
CREATE src/app/items/pages/item-detail/item-detail.page.html (138 bytes)
CREATE src/app/items/pages/item-detail/item-detail.page.spec.ts (720 bytes)
CREATE src/app/items/pages/item-detail/item-detail.page.ts (275 bytes)
UPDATE src/app/items/items.module.ts (371 bytes)

And update ItemsRoutingModule, to include them

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ItemsListPage } from './pages/items-list/items-list.page';
import { ItemDetailPage } from './pages/item-detail/item-detail.page';

const routes: Routes = [
  { path: '', component: ItemsListPage'},
  { path: ':id', component: ItemDetailPage'},
];

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

At this stage you can add links on pages to test navigation or access directly to lazy-loaded pages running ionic serve and typing url on address bar of your browser.

  • ’/’ for home
  • ‘/items’ for items-list
  • ‘/items/123’ for item-detail

Interface

When dealing with different data sources, it is useful to model those data sources with a common interface. Interfaces are not compiled into the JavaScript output of TypeScript, but are useful for type-checking. It exists some discussions about using interface vs class, I let you read them and make your opinion.

$ ng generate interface items/models/items
CREATE src/app/items/models/items.ts (37 bytes)

// because I like to rename it:
$ mv src/app/items/models/items.ts src/app/items/models/items.interface.ts

For this tutorial we’ll use newsapi.org service to fetch news from a REST API, the interface of Items is described below:

export interface Items {
  author: null,
  title: string,
  description: string,
  url: string,
  urlToImage: string,
  publishedAt: string, // 2018-10-09T16:18:45Z
  content: string
}

Mock items service

The objective of mock is to help on development and debug

$ ng g service items/services/items-mock --spec=false
CREATE src/app/items/services/items-mock.service.ts (138 bytes)
$ ng g class items/services/items-mock --spec=false
CREATE src/app/items/services/items-mock.ts (27 bytes)

Core: API Service

To consum the News API we’ll create an API Service and consider it as a Core Service. Because we only use it to fetch news (items) we could save it on items module, but a common use case is to share an API service to be used by various modules fetching different data models.

The News API Authentication is handled with a simple API key:

  • via the apiKey querystring parameter.
  • via the X-Api-Key HTTP header.

On this section we’ll use apiKey querystring parameter and later the X-Api-Key HTTP header to introduce http interceptors.

$ ng g service core/services/api --spec=false
CREATE src/app/core/services/api.service.ts (132 bytes)
$ ng g service items/services/items --spec=false
CREATE src/app/items/services/items.service.ts (134 bytes)

Core: http interceptor

Interceptors are sitting in between your application and the backend. By using interceptors you can transform a request coming from the application before it is actually submitted to the backend. The same is possible for responses. We’ll move the authentication from URL queryString to http header, and to achieve it we’ll use HttpInterceptor.

Write an interceptor

$ ng g class core/http-interceptors/auth-interceptor
CREATE src/app/core/http-interceptors/auth-interceptor.ts (33 bytes)
import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';

import { environment } from '@env/environment';

const API_KEY = environment.apiKey;

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  
  constructor() {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {

    /*
    * The verbose way:
    // Clone the request and replace the original headers with
    // cloned headers, updated with the authorization.
    const authReq = req.clone({
      headers: req.headers.set('Authorization', authToken)
    });
    */
    // Clone the request and set the new header in one step.
    const authReq = req.clone({ setHeaders: { 'X-Api-Key': API_KEY } });

    // send cloned request with header to the next handler.
    return next.handle(authReq);
  }
}

Provide the interceptor

import { AuthInterceptor } from './http-interceptors/auth-interceptor';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ApiService } from '@core/services/api.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';


@NgModule({
  imports: [
    CommonModule,
    HttpClientModule
  ],
  declarations: [],
  providers: [
    ApiService,
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ]
})
export class CoreModule { }

If you don’t append your API key correctly, or your API key is invalid, you will receive a 401 - Unauthorized HTTP error.

Repository & Demo

All source code can be found on GitHub: meumobi/meu-starter.master-detail.ionic-v4

Furthermore


Victor Dias

Sharing mobile Experiences

Follow me