Create a Full Mobile App with Ionic4

remko de knikker
Mar 21 · 11 min read

This article shows how to create a mobile application using Ionic 4, Angular 7 and TypeScript with all the basic functionalities a mobile application should have, like authentication, content feed and headers with sub-headers.

Follow the next steps to create the mobile app:

You can review the source code in the Github repository https://github.com/remkohdev/szirineapp.

My application is called Szirine, it is a mobile version of a website I created in the past called szirine.com.

Create an Empty Application with a Tabs Template

Start by creating an empty Ionic application using the tabs template. Run ‘ionic start’ to create the empty application, and run the application with ‘ionic serve’,

$ ionic start szirineapp tabs --cordova
? Install the free Ionic Appflow SDK and connect your app? No
$ cd ./szirineapp
$ ionic serve

This creates and runs the default tabs application. In Chrome Browser, open Menu > More Tools… > Developer Tools > Application, and click the ‘toggle device toolbar’ button to switch to a mobile application layout,

Rename Tabs

Instead of Tab1, Tab2, and Tab3, I would like to create tabs for Feed, Chat and Profile. I could delete the default tabs and create new ones, of course. Eat your heart out if you want to. Instead, I choose to rename the default tabs.

Do a search for and change all references to ‘Tab1’, ‘tab1’, and ‘Tab One’ to ‘feed’ and ‘Feed’. Edit the files ‘tab1.module.ts’, ‘tab1.page.spec.ts’, ‘tab1.page.ts’, ‘tab1.page.html’, ‘tabs.page.html’, and ‘tabs.router.module.ts’.

Similarly, change all references to ‘Tab2’, ‘tab2’ and ‘Tab Two’ to ‘chat’ and ‘Chat’. Change all references to ‘Tab3’, ‘tab3’ and ‘Tab Three’ to ‘profile’ and ‘Profile’.

Rename the folders and files in ‘src/app’.

In the file ‘src/app/tabs/tabs.page.html’ change the icons names for ‘flash’, ‘apps’ and ‘send’ to respectively ‘paper’, ‘chatboxes’ and ‘person’.

<ion-tabs main>
<ion-header no-border><ion-toolbar></ion-toolbar></ion-header>

<ion-tab-bar slot="bottom">
<ion-tab-button tab="feed">
<ion-icon name="paper"></ion-icon>
<ion-label>Feed</ion-label>
</ion-tab-button>
<ion-tab-button tab="chat">
<ion-icon name="chatboxes"></ion-icon>
<ion-label>Chat</ion-label>
</ion-tab-button>
<ion-tab-button tab="profile">
<ion-icon name="person"></ion-icon>
<ion-label>Profile</ion-label>
</ion-tab-button>
</ion-tab-bar></ion-tabs>

When you save the changes, ionic will automatically re-compile and refresh the application in your browser,

Add Header and Menu

Next, I want to add a header with menu items for Settings, Profile, and Logout, which shows across the application. I also want to add a sub header for each tab.

However, when the ‘Settings’ page opens, I don’t want any application header to show. The ‘Profile’ page is a tab page with a sub-header, and the ‘Logout’ is only a function call with a redirect to the default ‘Feed’ page, while the ‘Settings’ page is a new page outside of the tabs stack.

I add my header at the tabs component level, because if I add it to the app component level, it will show for all pages, but like I mentioned, I don’t want the header to show for some pages like the ‘Settings’ page.

To add a header with a header menu at the tabs component level, edit the file ‘src/app/tabs/tabs.page.html’. Add the ‘ion-menu’, the ‘ion-header’ and inside the ‘ion-tabs’, add a filler header or placeholder for the tabs sub-header with ‘ion-header’.

<!-- begin header menu definition -->
<ion-menu side="start" type="overlay" menuId="mainMenu" class="main-menu">
<ion-header>
<ion-toolbar>
<ion-title>Main Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item href="settings" routerDirection="root">Settings</ion-item>
<ion-item href="tabs/profile" routerDirection="root">Profile</ion-item>
<ion-item (click)="logout()">Logout</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<!-- end header menu definition -->
<!-- begin header -->
<ion-header no-border>
<ion-toolbar mode="ios">
<ion-title>
Szirine
</ion-title>
<ion-buttons slot="start">
<ion-menu-button autoHide="false"></ion-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<!-- end header -->
<ion-tabs main><!-- filler header -->
<ion-header no-border><ion-toolbar></ion-toolbar></ion-header>
<ion-tab-bar slot="bottom"> ...

The filler header will position the sub header directly under the header. If you remove this filler header or dummy header, then the sub header will be positioned in the same position as and under the header layer and in effect become invisible.

You could also decide to add the header menu at the app component level, in which case all the components, both standalone pages and tabs, would display the header. To do so, edit the above in the ‘src/app/app.component.html’ instead of the ‘tabs.page.html’.

Create a new ‘Settings’ page to connect the menu item ‘Settings’ with ‘href=”settings”’ property,

$ ionic g page settings

Add a logout() method to the ‘TabsPage’ class in file ‘src/app/tabs/tabs.page.ts’.

export class TabsPage {
constructor() {
console.log('TODO.TabsPage.constructor');
}
logout() {
console.log('TODO.TabsPage.logout');
}
}

Start the application again,

$ ionic serve

Add Sub Headers

I still need sub headers for each tab. In each of the tab pages, edit the sub header with styling. For example, edit the ‘src/app/chat/chat.page.html’,

<ion-header no-border>
<ion-toolbar mode="ios">
<ion-title>Chat</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>

The ‘no-border’ will remove the bottom shadow image under the headers. The ‘mode=”ios”’ will center the header title in Android.

Do the same for the ‘tab’.page.html for ‘feed’ and ‘profile’.

Add Authentication Service, Guard and LocalStorage

I want to protect the ‘Chat’, ‘Profile’, ‘Settings’ pages to only be accessible by authenticated users. The ‘Feed’ page should always be displayed. If an unauthenticated user tries to access a protected page, they should be redirected to the ‘Login’ page.

First, create a login page and a register page, an authentication service and an auth guard to protect pages,

$ ionic g page auth/login
$ ionic g page auth/register
$ ionic g service services/authentication
$ ionic g guard services/auth

Edit the file ‘src/app/auth/login/login.page.html’,

<ion-header no-border>
<ion-toolbar mode="ios">
<ion-title>Login</ion-title>
<ion-buttons slot="start">
<ion-button (click)="goHome()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-grid>
<ion-row justify-content-center>
<ion-col align-self-center>
<ion-item>
<ion-label stacked>Username</ion-label>
<ion-input [(ngModel)]="username"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>Password</ion-label>
<ion-input type="password" [(ngModel)]="password"></ion-input>
</ion-item>
<ion-button (click)="login()" expand="block">
Login
</ion-button>
<ion-button expand="block" color="secondary" routerLink="/register" routerDirection="forward">Register</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

Edit the file ‘src/app/auth/login/login.page.ts’,

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthenticationService } from './../../services/authentication.service';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
private username: string;
private password: string;
constructor(
private activatedRoute: ActivatedRoute,
private authService: AuthenticationService,
public router: Router
) { }

ngOnInit() {
}
login() {
this.authService.login(this.username, this.password);
this.username = null;
this.password = null;
this.goHome();
}
goHome() {
this.router.navigateByUrl( 'tabs/feed' );
}
}

You can choose to edit the register.page.html, but for now, I left the register page empty. You should be able to follow the login example and implement it on your own. Because the authentication flow is missing a proper backend at the moment, I left the register to be done at a later time when a proper backend service is added.

Edit the file ‘src/app/services/auth.guard.ts’,

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthenticationService } from './authentication.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
public auth: AuthenticationService, public router: Router
){}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return new Promise( (resolve, reject) => {
this.auth.isAuthenticated()
.then( isAuthenticated => {
if (!isAuthenticated) {
this.router.navigate(['login']);
}
resolve(true);
})
.catch( error => {
return (false);
});
});
}
}

Edit the file ‘src/app/services/authentication.service.ts’,

import { Platform } from '@ionic/angular';
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';
import { BehaviorSubject } from 'rxjs';
const TOKEN_KEY = 'auth-token';@Injectable({
providedIn: 'root'
})
/**
* See: https://ionicframework.com/docs/building/storage
*/
export class AuthenticationService {
authenticationState = new BehaviorSubject(false);
constructor(
private storage: Storage, private platform: Platform
) {
this.platform.ready().then(() => {
this.checkToken();
});
}
checkToken() {
return new Promise( (resolve, reject) => {
this.storage.get(TOKEN_KEY)
.then(res => {
if (res) {
this.authenticationState.next(true);
}
resolve(res);
})
.catch( error => {
reject(error);
});
});
}
login(username, password) {
const accessToken = btoa(username + ':' + password);
const token = 'Bearer ' + accessToken;
return this.storage.set(TOKEN_KEY, token).then(() => {
this.authenticationState.next(true);
});
}
logout() {
return this.storage.remove(TOKEN_KEY).then(() => {
this.authenticationState.next(false);
});
}
isAuthenticated() {
return new Promise( (resolve, reject) => {
this.checkToken()
.then( res => {
resolve(this.authenticationState.value);
})
.catch( error => {
reject(error);
});
});
}
}

When running in the web or as a Progressive Web App, Storage will attempt to use IndexedDB, WebSQL, and localstorage, in that order. For accessToken, I used Basic Authentication with a base64 encrypted username+’:’+password string. What is missing here, is a real implementation of checking the login against an authentication backend, and storing the accessToken with an expiration time. But since the backend for this is missing from this tutorial, I left it as is for now.

Replace the logout() and the constructor() functions and add the goHome() function in ‘src/app/tabs/tabs.page.ts’,

import { Component } from '@angular/core';
import { MenuController } from '@ionic/angular';
import { Router } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';
@Component({
selector: 'app-tabs',
templateUrl: 'tabs.page.html',
styleUrls: ['tabs.page.scss']
})
export class TabsPage { constructor(
private authService: AuthenticationService,
public menuCtrl: MenuController,
public router: Router
) { }
logout() {
this.authService.logout();
this.menuCtrl.close('mainMenu');
this.goHome();
}
goHome() {
this.router.navigateByUrl( 'tabs/feed' );
}
}

Add and install the NPM module ‘@ionic/storage’,

$ npm install --save @ionic/storage

Initialize the Storage by importing the ‘IonicStorageModule’ in ‘src/app/app.module.ts’,

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { IonicStorageModule } from '@ionic/storage';
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
IonicStorageModule.forRoot()
],
providers: [
StatusBar,
SplashScreen,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {}

Last, add the protection of the ‘Chat’, ‘Profile’ and ‘Settings’ pages. Edit the file ‘app-routing.module.ts’ and add the ‘canActivate: [AuthGuard]’ property to the necessary routes,

import { AuthGuard } from './services/auth.guard';const routes: Routes = [
{ path: '', loadChildren: './tabs/tabs.module#TabsPageModule' },
{ path: 'settings', loadChildren: './settings/settings.module#SettingsPageModule', canActivate: [AuthGuard] },
{ path: 'login', loadChildren: './auth/login/login.module#LoginPageModule' },
{ path: 'register', loadChildren: './auth/register/register.module#RegisterPageModule' }
];

and edit the file ‘src/app/tabs/tabs.router.module.ts’,

import { AuthGuard } from '../services/auth.guard';...{
path: 'chat',
children: [
{
path: '',
loadChildren: '../chat/chat.module#ChatPageModule',
canActivate: [AuthGuard]
}
]
},
{
path: 'profile',
children: [
{
path: '',
loadChildren: '../profile/profile.module#ProfilePageModule',
canActivate: [AuthGuard]
}
]
},
...

Start the application again,

$ ionic serve

In Chrome Browser, open Menu > More Tools… > Developer Tools > Application,

After logging in, the token is stored in the local storage, and you can access the chat and profile tabs,

Add Content and List of Cards to Feed

Add temporary test data to the file ‘feed.page.ts’,

import { Component, OnInit } from '@angular/core';const data = [
{
id: '1',
title: 'A History of the Great Wild West of America',
subtitle: 'The story of Chicago and its hinterland',
published: {
month: 'March',
day: '6',
year: '2017'
},
thumbnail: 'http://www.szirine.com/wpmain/uploads/2017/03/Ferris-wheel-1893-768x568.jpg',
text: 'The story of Chicago and its hinterland is one of the wildest histories of socio-economic growth you will encounter. In our time perhaps only comparable with the rapid growth of the Chinese economy and population.'
},
{
id: '2',
title: 'Anne Dunn',
subtitle: 'Anne Dunn (born 1929) is an English artist of the second School of London.',
published: {
month: 'Feb',
day: '7',
year: '2016'
},
thumbnail: 'http://www.szirine.com/wpmain/uploads/2016/02/Screen-Shot-2016-02-07-at-15.45.39.png',
text: 'Anne Dunn’s paintings seem to explore the middle zones: Between being and not being, between beauty and decay. Fruit and Swallow (1956) depicts bright sliced fruit beside a dead blue swallow. Vines After Rain (1959) explores a half seen landscape, abstracted, colourful and suggestive of movement.'
},
{
id: '3',
title: 'Can neuroscience help us understand Art?',
subtitle: 'A debate between Gabrielle Starr and Alva Noe at NYU\'s Case Italiana.',
published: {
month: 'Dec',
day: '13',
year: '2015'
},
thumbnail: 'http://www.szirine.com/wpmain/uploads/2015/12/fnhum-06-00066-g006.jpg',
text: 'One of my favorite garage punk songs by English artists is the Headcoats’ ‘Art or Arse’ with Billy Childish (from Tracey Emin). It raises the question of all times, ‘What is Art?’. To answer this, one could look at the neural activity during an aesthetic experience. At NYU’s Casa Italiana, Gabrielle Starr and Alva Noe debate the role of neuroscience.'
}
];
@Component({
selector: 'app-feed',
templateUrl: 'feed.page.html',
styleUrls: ['feed.page.scss']
})
export class FeedPage implements OnInit {
articles: any = [];
constructor() {} ngOnInit() {
this.articles = data;
}
openArticle(index: number) {
console.log('TODO');
}
}

Edit the file ‘feed.page.html’ and add a list of items for each article in articles,

<ion-header no-border class="bar bar-subheader">
<ion-toolbar mode="ios">
<ion-title>
Feed
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list lines="none">
<ion-item *ngFor="let a of articles; let i = index" (click)="openArticle(i)">
<ion-card>
<ion-card-header>
<ion-grid>
<ion-row>
<ion-col size="3" class="date-label">
<ion-row class="date-label-month">{{ a.published.month }}</ion-row>
<ion-row class="date-label-day">{{ a.published.day }}</ion-row>
<ion-row class="date-label-year">{{ a.published.year }}</ion-row>
</ion-col>
<ion-col size="9">
<ion-row>
<ion-card-title>{{ a.title }}</ion-card-title>
</ion-row>
<ion-row>
<ion-card-subtitle>{{ a.subtitle }}</ion-card-subtitle>
</ion-row>
</ion-col>
</ion-row>
</ion-grid>
</ion-card-header>
<ion-img [src]="a.thumbnail"></ion-img>
<ion-card-content>
<p>{{ a.text }} <ion-icon name="arrow-forward"></ion-icon></p>
</ion-card-content>
</ion-card>
</ion-item>
</ion-list>
</ion-content>

You now have all the features and implementations, to complete the application. In a following part I will complete the application’s chat, profile and settings pages, and add backend APIs for authentication, feed, add a filter to the feed for categories, etcetera.

NYC⚡️DEV

NYC Developer Community

remko de knikker

Written by

Developer Advocate @IBMDeveloper @IBMCloud — stateless tech humanist, serpent in the shepherds mouth — Dutch NY-er

NYC⚡️DEV

NYC Developer Community