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!


1 comment:

Subscribe to RSS Feed Follow me on Twitter!