Sunday, March 17, 2019

Learning Ionic 4 - Part 3

In this Part 3, we will focus on adding a component into page, working with ionic components, using SCSS, and rolling numbers to play games.

According to the mockup, we have an array of numbers which appears on pages: Games (Home), Play Games and History. Below is my Home page after finishing:

So creating a component presenting array of numbers for reusing in pages is a good idea. Let generate this component:
ionic g component components/NumbersPanel
It will generate files:
src/app/components/numbers-panel/numbers-panel.component.html
src/app/components/numbers-panel/numbers-panel.component.spec.ts
src/app/components/numbers-panel/numbers-panel.component.ts
src/app/components/numbers-panel/numbers-panel.component.scss


Modify numbers-panel.component.ts as below:
import { Component, OnInit, Input } from '@angular/core';
@Component({
  selector: 'app-numbers-panel',
  templateUrl: './numbers-panel.component.html',
  styleUrls: ['./numbers-panel.component.scss']
})
export class NumbersPanelComponent implements OnInit {
  @Input() numbers: any = [];
  @Input() minval: number;
  @Input() maxval: number;
  constructor() {
  }
  ngOnInit() {
  }
}
Modify numbers-panel.component.html as below:

<div class="box-number-outline">
  <div class="box-number" *ngFor="let n of numbers">{{n}}</div>
</div>
Modify numbers-panel.component.scss as below:

.box-number-outline {
    text-align: center;
}
.box-number {
    width: 35px;
    height: 25px;
    background-color: purple;
    display: inline-block;
    border: solid 1px white;
    color: white;
    padding-top: 2px;
}
To allow this component used in pages, let create a file src\app\components\components.module.ts and add below code (this file can be created with the command ionic g module components/components, however I got error 'Tree type is not supported' - ng cli error so I created it manually):

import { NgModule } from '@angular/core';
import { NumbersPanelComponent } from './numbers-panel/numbers-panel.component';
import { IonicModule } from '@ionic/angular';
import { CommonModule } from '@angular/common';
@NgModule({
    declarations: [NumbersPanelComponent],
    imports: [CommonModule , IonicModule],
    exports: [NumbersPanelComponent]
})
export class ComponentsModule {}
OK, we already had our component, let use it in Home page by declaring some things in src\app\home\home.module.ts:

...
import { ComponentsModule } from '../components/components.module';
...
@NgModule({
  imports: [
    ...
    ComponentsModule,
    …
Below is new code of src\app\home\home.page.ts:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../services/auth.service';
import { MyTablesService } from '../services/my-tables.service';
import { NavController, AlertController } from '@ionic/angular';
@Component({
  selector: 'app-home',
  templateUrl: './home.page.html',
  styleUrls: ['./home.page.scss'],
})
export class HomePage implements OnInit {
  games: any = [];
  history: any = [];
  constructor(
    private auth: AuthService,
    private tableService: MyTablesService,
    private navCtrl: NavController,
    private alertCtrl: AlertController
  ) { }
  ngOnInit() {
    this.auth.loggedin().then(isLoggedin => {
      if (!isLoggedin) {
        this.navCtrl.goRoot('/login');
      }
    });
  }
  ionViewWillEnter() {
    this.loadGames();
  }
  loadGames() {
    this.tableService.getItems('games').then((d) => {
      this.games = d;
      this.tableService.getItems('history').then((h) => {
        this.history = h;
        for(let game of this.games) {
          let gh = this.tableService.getItemByField(this.history, 'game_id', game.id);
          if(gh.length > 0) {
            gh.sort(this.tableService.sortDescByField('play_time'));
            game.last_play_time = new Date(gh[0].play_time).toLocaleString('vi-VN');
            game.last_result = JSON.parse(gh[0].result);
          }
        }
      });
    });
  }
  newGame() {
    this.navCtrl.goForward('/new-game');
  }
  playGame(game_id) {
    this.navCtrl.goForward(`/play-game/${game_id}`);
  }
  async deleteGame(game_id) {
    let alert = await this.alertCtrl.create({
      message: 'Do you want to delete this game?',
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel',
          handler: () => {
          }
        },
        {
          text: 'OK',
          handler: () => {
            this.tableService.setItem('games', game_id, 'deleted', true);
          }
        }
      ]
    });
    await alert.present();
  }
}
And new code of src\app\home\home.page.html:

<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Games</ion-title>
  </ion-toolbar>
</ion-header>
<ion-content padding>
  <ion-grid *ngFor="let game of games; let i = index;">
    <div [ngClass]="(i % 2 == 0) ? 'grid_odd' : 'grid_even'" *ngIf="!game.deleted">
        <ion-row>
          <ion-col size="11">
            <b>{{game.name}}</b>
          </ion-col>
          <ion-col size="1">
            <div class="play_icon">
              <ion-icon name="arrow-dropright-circle" style="zoom:1.2;" (click)=playGame(game.id)></ion-icon>
            </div>
          </ion-col>
        </ion-row>
        <ion-row>
          <ion-col size="11">
            <div class="last_play_time" *ngIf="game.last_play_time">
              Last time: {{game.last_play_time}}
            </div>
            <div class="last_play_time" *ngIf="!game.last_play_time">
              Haven't been played yet. Let play.
            </div>
          </ion-col>
          <ion-col size="1">
            <div class="delete_icon">
              <ion-icon name="close-circle" style="zoom:1.2;" (click)=deleteGame(game.id)></ion-icon>
            </div>
          </ion-col>
        </ion-row>
        <ion-row>
          <ion-col size="3">
            <div class="last_result" *ngIf="game.last_play_time">
                Last result:
            </div>
          </ion-col>
          <ion-col size="9">
            <app-numbers-panel [numbers]="game.last_result"></app-numbers-panel>
          </ion-col>
        </ion-row>
    </div>
  </ion-grid>
   
  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button color="danger" (click)="newGame()">
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>
Pay your attention on the code <app-numbers-panel [numbers]="game.last_result"></app-numbers-panel> : ==> it will show the NumbersPanel component and put the last result of game to the component for displaying.

On above html file, I also make alternate color for game rows, watch the code: [ngClass]="(i % 2 == 0) ? 'grid_odd' : 'grid_even'".

At this time, your Home page can list games but won't have any info about last playing result of games. So let generate PlayGame page and play, run the command:

ionic g page PlayGame
Modify src\app\play-game\play-game.page.ts as below:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { History,  MyTablesService } from '../services/my-tables.service';
import { delay } from 'q';
@Component({
  selector: 'app-play-game',
  templateUrl: './play-game.page.html',
  styleUrls: ['./play-game.page.scss'],
})
export class PlayGamePage implements OnInit {
  game: any;
  numbers: any = [];
  history: History;
  curTime: any;
  played: boolean = false;
  constructor(
    private activeRoute: ActivatedRoute,
    private tableService: MyTablesService) {
      this.curTime = new Date().toLocaleString('vi-VN');
      this.history = new History();
      let game_id = this.activeRoute.snapshot.paramMap.get('id');
      this.tableService.getItem('games', game_id).then((d) => {
        this.game = d;
        for(let i=0; i < this.game.numbers; i++) {
          this.numbers.push(-1);
        }
      });
  }
  ngOnInit() {
  }
  async startGame() {
    for(let i = 1; i < 3; i++) {
      for(let j = this.game.min_number; j <=  this.game.max_number; j++) {
        for(let k=0; k < this.game.numbers; k++) {
          this.numbers[k] = j;
        }
        await delay(200);
      }
    }
   
    for(let i=0; i < this.game.numbers; i++) {
      this.numbers[i] = Math.floor(Math.random() * this.game.max_number) + this.game.min_number;
    }
    this.saveHistory();
    this.played = true;
  }
  saveHistory() {
    this.history.play_time = new Date();
    this.history.game_id = this.game.id;
    this.history.result = JSON.stringify(this.numbers);
    this.tableService.addItem('history', this.history);
  }
}
Modify src\app\play-game\play-game.page.html as below:

<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Play Game</ion-title>
  </ion-toolbar>
</ion-header>
<ion-content padding>
  <div *ngIf="game" style ="text-align: center; padding: 10px;">
    <b>{{game.name}}</b><br>
    <i>{{curTime}}</i><br>
  </div>
  <app-numbers-panel [numbers]="numbers"></app-numbers-panel>
  <div style ="text-align: center; padding: 10px;">
      <ion-button size="default" shape="round" color="secondary" (click)="startGame()" [disabled]="played">
        Start
      </ion-button>
  </div>
</ion-content>
In the function startGame(), I roll numbers from its min to max and delay 200ms when changing numbers. It is done 2 times and finished by randomizing numbers between its min & max. This is how I make the animation for rolling numbers -:)

Yeah! Now we can create games, play them and have a fun. See how I play a game:


After playing, we may need to see the history of playing games. It's time to code the History page.
Modify src\app\history\history.page.ts as below:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../services/auth.service';
import { MyTablesService } from '../services/my-tables.service';
import { NavController } from '@ionic/angular';
@Component({
  selector: 'app-history',
  templateUrl: './history.page.html',
  styleUrls: ['./history.page.scss'],
})
export class HistoryPage implements OnInit {
  games: any = [];
  history: any = [];
  items: any = [];
  keywords: string = '';
  showSearchBar: boolean = true;
  constructor(
    private auth: AuthService,
    private tableService: MyTablesService,
    private navCtrl: NavController) { }
    ngOnInit() {
      this.auth.loggedin().then(isLoggedin => {
        if (!isLoggedin) {
          this.navCtrl.goRoot('/login');
        }
      });
    }
 
    ionViewWillEnter() {
      this.loadGames();
    }
 
    loadGames() {
      this.tableService.getItems('games').then((d) => {
        this.games = d;
        this.items = d;
        this.tableService.getItems('history').then((h) => {
          this.history = h;
          for(let game of this.games) {
            let gh = this.tableService.getItemByField(this.history, 'game_id', game.id);
            game.pcount = gh.length;
          }
        });
      });
    }
    searchGames() {
      this.items = this.games.filter((item) => {
        return item.name.toLowerCase().indexOf(this.keywords.toLowerCase()) > -1;
      });
    }
    toggleSarchBar() {
      this.showSearchBar = !this.showSearchBar;
    }
    showGameHistory(game_id) {
      this.navCtrl.goForward(`/game-history/${game_id}`);
    }
}
Modify src\app\history\history.page.html as below:

<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>History</ion-title>
    <ion-buttons slot="end">
      <div class="button">
        <ion-icon name="search" style="zoom:1.2;" (click)="toggleSarchBar()"></ion-icon>
      </div>
    </ion-buttons>
  </ion-toolbar>
</ion-header>
<ion-content padding>
  <div *ngIf="showSearchBar" [hidden]="!showSearchBar">
      <ion-searchbar [(ngModel)]="keywords" (ionChange)="searchGames()" placeholder="Filter games" debounce="1000"></ion-searchbar>
  </div>
  <ion-grid *ngFor="let game of items; let i = index;">
    <div [ngClass]="(i % 2 == 0) ? 'grid_odd' : 'grid_even'" *ngIf="!game.deleted" (click)="showGameHistory(game.id)">
      <b>{{game.name}}</b><br/>
      <i>Created time: {{game.created_time}}</i><br/>
      Played: {{game.pcount}} times
    </div>
  </ion-grid>
</ion-content>
There is a tricky that I used to make the icon search can be clicked on the tool bar, that is wrapping the icon with <div class="button">. Without class="button", we cannot click on the icon. I don't know if it is normal behavior of Ionic 4 or a bug.

The search bar (ion-searchbar) also is set debounce="1000" to avoid filter immediately when keying. This debounce="1000" means if you stop keying in 1 second, the input value will be fired to ionChange event.

The last page is GameHistory page to show all playing history of a game. Let generate it:

ionic g page GameHistory
This page will use the NumbersPanel component, so let import ComponentsModule to its module file src\app\game-history\game-history.module.ts.

Basically, coding of this page is nearly same with other pages we done, except that it has new component for rating playing results. But I want to keep this for next article.

In next article, we may study how to make general CSS applied for all pages and code a rating component.

I hope that through Part 1 - Part 2 - Part 3, you can start to code your real application with Ionic 4. In case you want grab the source code, let check https://github.com/vnheros/GameLot on my GitHub.

See you. Any comment is welcome!


Saturday, March 9, 2019

Learning Ionic 4 - Part 2

After long time I'm busy with the projects in my company, now I have a time to write this Part 2.
In Part 1, we finished the login & sign-up page. In this part, we will focus on how to create pages with main menu (side menu), how to deal with local storage (use local storage as DB with tables).

The main menu has 2 submenu Games menu & History menu. Because we will Home page for Games menu, so we jus generate new page called History for History menu:
ionic g page History
Next, we add a side menu. Open file src/app/app.component.ts and add menuPages:
export class AppComponent {
  public menuPages = [
    {
      title: 'Games',
      url: '/home',
      icon: 'logo-usd'
    },
    {
      title: 'History',
      url:'/history',
      icon: 'list'
    }
  ];
  constructor(
...
Then open file src/app/app.component.html and change as below:
<ion-app>
  <ion-split-pane>
    <ion-menu>
      <ion-header>
        <ion-toolbar>
          <ion-title>Menu</ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-content padding>
        <ion-list>
          <ion-menu-toggle *ngFor="let p of menuPages">
            <ion-item [routerLink]="p.url" [routerDirection]="'root'">
              <ion-icon slot="start" [name]="p.icon"></ion-icon>
              <ion-label>{{p.title}}</ion-label>
            </ion-item>
          </ion-menu-toggle>
        </ion-list>
      </ion-content>
    </ion-menu>
    <ion-router-outlet main></ion-router-outlet>
  </ion-split-pane>
</ion-app>
This change will add ion-split-pane into ion-app, and it will hold ion-menu (for menu content) and ion-router-outlet (for routing).
To display menu button on any page, let add ion-menu-button into its html. We need to add it into Home page & History page, for example src/app/home/home.page.html of Home page:
<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Games</ion-title>
  </ion-toolbar>
</ion-header>
OK, now we had the menu.
In Login page, we use a remote DB for authenticating, however we will use local storage to store the content of Games & History. In the real app, what is needed to store in centralized remote DB and what is needed to save on local storage depending on your strategy for your app. Here I want to show you a way to deal with the local storage like we work with tables. For beginning, let generate a MyTablesService service:
ionic g service services/MyTables
Open src/app/services/my-tables.service.ts and add the codes like the following:
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';

export class Game {
  public id: number;
  public name: string;
  public numbers: number;
  public min_number: number;
  public max_number: number;
  public created_time: Date;
  public deleted: boolean;
  public deleted_time: Date;
}

export class History {
  public id: number;
  public game_id: number;
  public play_time: Date;
  public result: string;
  public rating: number;
}

@Injectable({
  providedIn: 'root'
})
export class MyTablesService {
  table = 'table_';
  seedkey = 'seed_table';
  items: any = [];
  seed = 0;

  constructor(private storage: Storage) { }
  private getKeyFormat(id) {
    return this.table + id;
  }

  private _addItem(item) {
    item.id = this.seed;
    this.storage.set(this.getKeyFormat(item.id), JSON.stringify(item));
    this.seed++;
    this.storage.set(this.seedkey, this.seed);
  }

  private setTable(table_name: string) {
    this.table = table_name + '_';
    this.seedkey = 'seed_' + table_name;
  }

  getItems(table_name: string) {
    this.setTable(table_name);
    this.items = [];
    var promise = new Promise((resolve, reject) => {
      this.storage.forEach((v, k, i) => {
        let a = this.table;
        if (k.indexOf(a) > -1) {
          this.items.push(JSON.parse(v));
        }
      }).then(() => {
        resolve(this.items);
      });
    });
    return promise;
  }

  addItem(table_name: string, item: any) {
    this.setTable(table_name);
    this.storage.get(this.seedkey).then((value) => {
      if (value) this.seed = value;
      else this.seed = 1;
      this._addItem(item);
  }, (error) => {
      this.seed = 1;
      this._addItem(item);
    });
  }

  getItem(table_name: string, id: any) {
    this.setTable(table_name);
    var promise = new Promise((resolve, reject) => {
      this.storage.get(this.getKeyFormat(id)).then((value) => {
        resolve (JSON.parse(value));
      }, (error) => {
        reject (false);
      });
    });
    return promise;
  }

  getItemByField(data: any, field: string, value: string) {
    return data.filter(i => i[field] == value);
  }

  sortAscByField(field: string) {
    return function(a,b){
      if( a[field] > b[field]){
          return 1;
      }else if( a[field] < b[field] ){
          return -1;
      }
      return 0;
    }
  }

  sortDescByField(field: string) {
    return function(a,b){
      if( a[field] > b[field]){
          return -1;
      }else if( a[field] < b[field] ){
          return 1;
      }
      return 0;
    }
  }
}


Basically the local storage will store data in form of key & value, the MyTablesService service above will help to work with local storage as tables. In which, Game class and History class present for the structure of games table & history tables. We will see on later code.

Next, we will generate "New Game" page for adding new game:
ionic g page "New Game"
Here are codes for src/app/new-game/new-game.page.ts:
import { Component, OnInit } from '@angular/core';
import { Game,  MyTablesService } from '../services/my-tables.service';
import { NavController } from '@ionic/angular';

@Component({
  selector: 'app-new-game',
  templateUrl: './new-game.page.html',
  styleUrls: ['./new-game.page.scss'],
})
export class NewGamePage implements OnInit {
  game: Game;

  constructor(private gameService: GamesService, private navCtrl: NavController) {
    this.game = new Game();
    this.game.numbers = 5; //default value
  }

  ngOnInit() {
  }

  save() {
    this.game.created_time = new Date();
    this.gameService.addItem('games', this.game);
    this.navCtrl.goRoot('/home');
  }
}
And codes for src/app/new-game/new-game.page.html:
<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>New Game</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <ion-list>
    <ion-item>
      <ion-label>Name:</ion-label>
      <ion-input [(ngModel)]="game.name" type="text"></ion-input>
    </ion-item>
    <ion-item>
      <ion-label>Quantity of numbers (1-10):</ion-label>
      <ion-select [(ngModel)]="game.numbers" value="5" okText="Ok" cancelText="Cancel">
          <ion-select-option value="1">1</ion-select-option>
          <ion-select-option value="2">2</ion-select-option>
          <ion-select-option value="3">3</ion-select-option>
          <ion-select-option value="4">4</ion-select-option>
          <ion-select-option value="5">5</ion-select-option>
          <ion-select-option value="6">6</ion-select-option>
          <ion-select-option value="7">7</ion-select-option>
          <ion-select-option value="8">8</ion-select-option>
          <ion-select-option value="9">9</ion-select-option>
          <ion-select-option value="10">10</ion-select-option>
      </ion-select>
    </ion-item>
    <ion-item>
      <ion-label>Random # between:</ion-label>
      <ion-input [(ngModel)]="game.min_number" type="number"></ion-input>
      <ion-label>and</ion-label>
      <ion-input [(ngModel)]="game.max_number" type="number"></ion-input>
    </ion-item>
  </ion-list>
  <ion-button expand="full" shape="round" color="secondary" (click)="save()">Save</ion-button>
</ion-content>
To invoke this New Game page, let add a FAB (floating action button) into the html of Home page:
...
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button color="danger" (click)="newGame()">
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
</ion-fab>
...
Add function newGame() into src/app/home/home.page.ts:
newGame() {
    this.navCtrl.goForward('/new-game');
}
Below are new screenshots of our app until now after login:


Through this part, we have known how to add a menu, work with local storage and create a new game. In next Part 3, we will code remain things & finish the app. See you!

Thursday, October 11, 2018

CentOS + MS Bot Framework: Solving error "ChatConnector: receive - invalid token"

When I deploy my chat bot written with MS Bot Framework on CentOS, I got the error "ERROR: ChatConnector: receive - invalid token. Check bot's app ID & Password", although I provide exactly my bot's app ID & Password.


After googling, I know the problem is server time not corrected (in my case, it is late ~ 20 minutes), so the security token is invalid.

You can solve this easily by syncing your server time with a standard NTP (Network Time Protocol) server. Below are steps.

1. Install ntp and ntpdate
# yum install ntp ntpdate
2. Enable ntpd service
# systemctl start ntpd
# systemctl enable ntpd
Check its status:
# systemctl status ntpd
3. Add some CentOS NTP servers
# ntpdate -u -s 0.centos.pool.ntp.org 1.centos.pool.ntp.org 2.centos.pool.ntp.org
4. Restart ntpd service
# systemctl restart ntpd
5. Check again
# timedatectl
# date

Addition, let set hardware clock to the current server time:
# hwclock  -w
Now you can start your bot without this error. Cheers!

Tuesday, October 2, 2018

Learning Ionic 4 - Part 1

At the point I'm writing this article, Ionic 4 is going to official release, it is an innovation of Ionic Framework. Ionic 4 is the beginning version of the Framework to completely support modern Web APIs such as Custom Elements, CSS Variables and Shadow DOM.

Different with past versions, Ionic 4 can work as a standalone Web Component library. Although it still supports Angular as its root part, now it can be used with other JavaScript frameworks like React or Vue. The core components can work standalone with just a script tag in a web page. It means you can use Ionic as a standalone library in a single page even in a context like WordPress.

With many changes in the core, however the migration from Ionic 3 to Ionic 4 is very easy (almost change nothing in your code except the structure of source code) than from Ionic 1 to Ionic 2. You can read here for migrating from Ionic 3 to Ionic 4.

In this article, I will play with Ionic 4 by coding a game (lottery program) that you can use for mini games in marketing campaigns (e.g. on Facebook fan page). I call it as "GameLot". Through coding this game, I hope you can learn how to create a real application with Ionic 4.

Design the game

Before developing any app, we should have a clear idea what we want and design it at least in wireframes. Below are my ideas about the game and its wireframes.

Basically my GameLot will have:
  • A login page
  • A signup page
  • A side menu navigate to pages:
    • Games page: it is default page, it lists games created and have functions to new game, play game, delete game (and its history?).
    • History page: it list games and their history (results of every time playing games), and may have a function for rating a result.
And here are wireframes which I painted by MS Visio 💪.
Login page:
Signup page:
Side menu:
Games page:
New game page:

Play game page:
History page:
History of a game:

We have total 8 pages for this game. Next steps we will go to details how to code it.

Code it

For the login of this game, I build a JWT authentication server with Node.js, Express and MySQL (click on the link to see how to build it).

Now in this article, I will focus on building the game on Ionic 4.

If you don't have Ionic 4, let install it:
npm install -g ionic
Start new blank app:
ionic start GameLot blank --type=angular
You can jump to the folder GameLot generated to see what inside. Almost of the code will be in the src/app folder. You can read here for more details.

From Ionic 2, you can generate new app features by using the command ionic generate, see my example in the article "Simple starter kit for building realistic app with Ionic 2". In Ionic 4, it has a little change, you can use the command ionic g --help for more details.

OK, let generate a service to work with authentication server for login:
ionic g service services/Auth
It will generate 2 files auth.service.spec.ts and auth.service.ts in the foder src/app/services. Next, let generate a page for login:
ionic g page Login
It will generate the following files:
src/app/login/login.module.ts
src/app/login/login.page.html
src/app/login/login.page.spec.ts
src/app/login/login.page.ts
src/app/login/login.page.scss
src/app/app-routing.module.ts
Now we have files generated for coding a login page. We need change the default route to login page. When you generate any new page, it will add a new route to this file src/app/app-routing.module.ts, for example after above command, this file will have a content like the following:

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: 'Login', loadChildren: './login/login.module#LoginPageModule' },
];

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

Here, Ionic 4 use Angular 6 Router and lazy loading for page and each page has its own routing module. Let change const routes to:

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

Before coding the login page, we need to code the authentication service first. Create a folder named helpers (src/app/helpers) and copy this file from my old article (Simple starter kit for building realistic app with Ionic 2) into.
Because we will use Ionic storage to store authentication token, so we need to install ionic storage (read here for more info on Ionic storage):
npm install --save @ionic/storage
The authentication service will use HTTP to get/post, so we need to add HttpModule into the file src/app/app.module.ts, below is the code of this file after adding HttpModule:

import { HttpModule } from '@angular/http';
import { IonicStorageModule } from '@ionic/storage';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule
    IonicModule.forRoot(), 
    HttpModule,
    IonicStorageModule.forRoot(),
    AppRoutingModule
  ],
 

})
export class AppModule {}

Then modify the file src/app/services/auth.service.ts generated above as the following:

import {JwtHelper} from  '../helpers/jwt-helper';
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { Storage } from '@ionic/storage';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  jwtHelper: JwtHelper = new JwtHelper();
  LOGIN_URL: string = "http://<SERVER_AUTH:PORT>/user/login";
  SIGNUP_URL: string = "http://<SERVER_AUTH:PORT>/user/create";
  CHECK_URL = "http://<SERVER_AUTH:PORT>/user/check/";
  headers: Headers = new Headers({ "Content-Type": "application/json" });
  token: any;
  user: string;
  
  constructor(private http: Http, private storage: Storage) {
  }

  loggedin() {
    return this.storage.get('id_token').then((value) => {
      this.token = value;
      return this.token && !this.jwtHelper.isTokenExpired(this.token, null);
    }, (error) => {
      return false;
    });
  }

  check(user) {
    return new Promise((resolve, reject) => {
      this.http.get(this.CHECK_URL + user, { headers: this.headers })
        .subscribe(
          data => {
            resolve(data);
          },
          err => {
            reject(err);
          }
        );
    });
  }

  login(credentials) {
    return new Promise((resolve, reject) => {
      this.http.post(this.LOGIN_URL, JSON.stringify(credentials), { headers: this.headers })
        .subscribe(
          res => {
            const data = res.json();
            this.authSuccess(data.id_token);
            resolve(data);
          },
          err => {
            reject(err);
          }
        );  
    });
  }

  signup(credentials) {
    return new Promise((resolve, reject) => {
      this.http.post(this.SIGNUP_URL, JSON.stringify(credentials), { headers: this.headers })
        .subscribe(
          res => {
            const data = res.json();
            this.authSuccess(data.id_token);
            resolve(data);
          },
          err => {
            reject(err);
          }
        );
    });
  }

  authSuccess(token) {
    this.token = token;
    this.setAuth();
    this.storage.set('id_token', token);
  }

  setAuth() {
    this.user = this.jwtHelper.decodeToken(this.token).username;
    this.headers.append('Authorization', 'Bearer ' + this.token);
  }

  logout() {
    this.storage.set('id_token', '');
    this.user = null;
  }
}

The first version of login page has 2 files as below.
src/app/login/login.page.ts:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../services/auth.service';
import { LoadingController, NavController } from '@ionic/angular';

@Component({
  selector: 'app-login',
  templateUrl: './login.page.html',
  styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
  username: string;
  password: string;
  loading: any;

  constructor(
    private auth: AuthService,
    private navCtrl: NavController,
    private loadingCtrl: LoadingController) { }

  ngOnInit() {
    this.auth.loggedin().then(isLoggedin => {
      if (isLoggedin) {
        this.auth.setAuth();
        this.navCtrl.goRoot('/home');
      }
    });
  }

  async login() {
    await this.showLoader();
    let credentials = {
      username: this.username,
      password: this.password
    };
    this.auth.login(credentials).then((result) => {
      this.loading.dismiss();
      console.log("OK: " + result);
      this.navCtrl.goRoot('/home');
    }, (err) => {
      this.loading.dismiss();
      console.log("ERROR: " + err);
    });
  }

  launchSignup() {
  }

  async showLoader() {
    this.loading = await this.loadingCtrl.create({
      content: 'Authenticating...'
    });
    return await this.loading.present();
  }

}

src/app/login/login.page.html:

<ion-header>
  <ion-toolbar>
    <ion-title>GameLot Login</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <ion-row class="login-form">
      <ion-col>
          <ion-list inset>

            <ion-item>
              <ion-label><ion-icon name="person"></ion-icon></ion-label>
              <ion-input [(ngModel)]="username" placeholder="username" type="text"></ion-input>
            </ion-item>
            <ion-item>
              <ion-label><ion-icon name="lock"></ion-icon></ion-label>
              <ion-input [(ngModel)]="password" placeholder="password" type="password"></ion-input>
           </ion-item>
        </ion-list>
        <ion-button expand="full" shape="round" color="secondary" class="login-button" (click)="login()">Login</ion-button>
      </ion-col>
   </ion-row>

  <ion-row>
      <ion-col>
          <ion-button expand="clear" (click)="launchSignup()">Sign up</ion-button>
      </ion-col>
  </ion-row>
</ion-content>

src/app/login/login.page.scss:

ion-scroll {
    display: none;
}
ion-col {
    text-align: center;
    align-items: center;
}
.login-button {
    width: 50%;
    margin-left: 25%;
}

Run the command ionic serve, you will have the following login page:


You can test login page and see log info in console. Run below command to create Signup page:
ionic g page Signup
We will create a simple signup form with validations (it is nearly similar with old Ionic version, you can read here for form validation on Ionic 2). For using FormGroup, we must add ReactiveFormsModule into NgModule of sign up page. Open file src/app/signup/signup.module.ts, add ReactiveFormsModule as below:


import { FormsModule, ReactiveFormsModule } from '@angular/forms';


@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    IonicModule,
    RouterModule.forChild(routes)
  ],
  declarations: [SignupPage]
})
export class SignupPageModule {}

Then create a file /src/app/validators/password.ts for validating password input. The password must be between 4 and 10 characters and must have at least 1 number. Below is its code:

import { FormControl } from "@angular/forms";

export class PasswordValidator {
    static checkPassword(control: FormControl) {
        if (control.value.match(/^(?=.*[0-9])[a-zA-Z0-9!@#$%^&*]{4,10}$/)) {
            return null;
        } else {
            return { 'invalidPassword': true };
        }
    }
}

Then modify files src/app/signup/signup.page.ts and src/app/signup/signup.page.html as the following:

import { Component, OnInit } from '@angular/core';
import { PasswordValidator } from '../validators/password';
import { AuthService } from '../services/auth.service';
import { Validators, FormBuilder, FormGroup, FormControl } from '@angular/forms';
import { LoadingController, NavController } from '@ionic/angular';

@Component({
  selector: 'app-signup',
  templateUrl: './signup.page.html',
  styleUrls: ['./signup.page.scss'],
})
export class SignupPage implements OnInit {
  signupCreds: FormGroup;
  usernameIsValid: boolean = false;
  loading: any;

  constructor(
    private auth: AuthService,
    private formBuilder: FormBuilder,
    private navCtrl: NavController,
    private loadingCtrl: LoadingController) { }

  ngOnInit() {
   this.signupCreds = this.formBuilder.group({
    username: new FormControl('', Validators.compose([Validators.required,
              Validators.minLength(4), Validators.maxLength(20), Validators.pattern('[a-zA-Z]*')])),
    password: new FormControl('', Validators.compose([Validators.required, PasswordValidator.checkPassword])),
    email: new FormControl('', Validators.compose([Validators.required,
              Validators.pattern('^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$')]))
   });
  }

  checkUsername(username) {
    console.log("Check user name:" + username);
    if (username.length < 4) {
      this.usernameIsValid = false;
      console.log("NG");
      return;
    }
    this.auth.check(username.toLowerCase()).then(
      (success) => {
        this.usernameIsValid = true;
        console.log("OK");
      },
      (err) => {
        this.usernameIsValid = false;
        console.log("Existed");
      }
    );
  }

  async signup(credentials){
    await this.showLoader();
    this.auth.signup(credentials).then((result) => {
      this.loading.dismiss();
      console.log(result);
      this.navCtrl.goRoot('/home');
    }, (err) => {
      this.loading.dismiss();
    });
  }

  async showLoader(){
    this.loading = await this.loadingCtrl.create({
      content: 'Creating new account...'
    });
    return await this.loading.present();
  }

}

<ion-header>
  <ion-toolbar>
    <ion-title>Signup</ion-title>
    <ion-buttons slot="start">
      <ion-back-button></ion-back-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content padding>

    <form [formGroup]="signupCreds" (submit)="signup(signupCreds.value)">
      <ion-item>
        <ion-label><ion-icon name="person"></ion-icon></ion-label>
        <ion-input type="text" (ionBlur)="checkUsername($event.target.value)" placeholder="User Name" formControlName="username" required></ion-input>
      </ion-item>
      <p *ngIf="signupCreds.get('username').hasError('required') && signupCreds.get('username').touched" class="error">Username is required</p>
      <p *ngIf="signupCreds.get('username').hasError('minlength') && signupCreds.get('username').touched" class="error">Username must have at least 4 characters</p>
      <p *ngIf="signupCreds.get('username').hasError('maxlength') && signupCreds.get('username').touched" class="error">Username must have maximum 20 characters</p>
      <p *ngIf="signupCreds.get('username').hasError('pattern') && signupCreds.get('username').touched" class="error">Username must contain only letters</p>
      <p *ngIf="signupCreds.get('username').touched && !usernameIsValid" class="error">User is invalid or taken</p>
      <p *ngIf="!signupCreds.get('username').hasError('maxlength') && !signupCreds.get('username').hasError('pattern') && signupCreds.get('username').touched && usernameIsValid" class="success"> User is OK</p>
   
      <ion-item>
        <ion-label><ion-icon name="lock"></ion-icon></ion-label>
        <ion-input type="password" placeholder="Password" formControlName="password"></ion-input>
      </ion-item>
      <p *ngIf="!signupCreds.get('password').valid && signupCreds.get('password').touched" class="error">Password is not met standards</p>
      <p *ngIf="signupCreds.get('password').valid && signupCreds.get('password').touched" class="success">Password is OK</p>

      <ion-item>
        <ion-label><ion-icon name="mail"></ion-icon></ion-label>
        <ion-input type="email" placeholder="Your email" formControlName="email" required></ion-input>
      </ion-item>
      <p *ngIf="signupCreds.get('email').hasError('required') && signupCreds.get('email').touched" class="error">Email is required</p>
      <p *ngIf="signupCreds.get('email').hasError('pattern') && signupCreds.get('email').touched" class="error">Email is wrong format</p>
   
      <ion-button expand="block" type="submit" color="secondary" [disabled]="!signupCreds.valid">Sign up</ion-button>
  </form>

</ion-content>

And src/app/signup/signup.page.scss:

.error {
    color: red;
}
.success {
    color: green;
}

Now you can sign up and login. That's all for Part 1. In Part 2, we will code some things new.
See you!

Subscribe to RSS Feed Follow me on Twitter!