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!

Thursday, September 6, 2018

MS SQL Server: How to copy a standby DB (logs shipped) to another instance

Why we need to copy a standby database? The main reasons are the location & time. Normally the online DB will be in a server on datacenter/clouding, while the standby DB (logs shipped) will be in a local server. So if you have a big size DB, you may take time & traffic to transfer the backup of online DB to a local server for doing your needs. However, if you can copy the standby DB, it will be faster and save your traffic. After copying, your standby DB can still receive logs shipped from the online DB.

Below are steps to do this on the standby server.

1. Take offline the standby DB
Run the following command:

USE MASTER
GO
ALTER DATABASE [DBNAME] SET OFFLINE

2. Disable restoring job
On MS SQL Server Management Studio, go to SQL Server Agent >> Jobs >> right click on the restoring job >> select Disable, see the following for example:


3. Copy data file (.mdf) and log file (.ldf) of the standby DB to another place
To know where, you can right click on DB >> select Properties >>  select tab Files >> see Path column.

4. Take online the standby DB
Now, we take it back for continuing receiving logs shipped. Run the following command:

USE MASTER
GO
ALTER DATABASE [DBNAME] SET ONLINE
5. Enable restoring job again

After this step, you standby DB will run normally again.

Now we can create new DB and copy files in step 3 above to. Below are steps on the server which will run this copied DB.

6. Create new DB (same name with standby DB)
Run the following command (make sure new DB same names DBNAME_Data and DBNAME_Log with standby DB):

CREATE DATABASE [DBNAME]
ON
(NAME = 'DBNAME_Data', FILENAME = 'C:\SQL-Data-Folder\DBNAME.mdf')
LOG ON
(NAME = 'DBNAME_Log', FILENAME = 'C:\SQL-Data-Folder\DBNAME.ldf')

7. Take this new DB offline

ALTER DATABASE [DBNAME] SET OFFLINE

8. Copy files in step 3 overwrite files of new DB created

9. Grant permission for MSSQLSERVER working on copied files
On file explorer, right click on the file name >> select Properties menu >> select tab Security >> click Advanced button:


Change the owner and add full permissions on these files copied for user running SQL service (normally it is NT Service\MSSQLSERVER). See red boxes on above picture for example.

10. Take this new DB online

ALTER DATABASE [DBNAME] SET ONLINE

OK, now you have a copied DB of standby DB running. Have fun.

Tuesday, August 14, 2018

Using Chocolatey for managing NodeJS

What is Chocolatey? Let read here: https://chocolatey.org/about
Install & Upgrade Chocolatey => read here: https://chocolatey.org/install


1. Install NodeJS
Open CMD and the following command:

choco install nodejs -y

It will install latest NodeJS version and npm together.
For installing  long term support (LTS) version of NodeJS, run the command:

choco install nodejs-lts -y

2. Upgrade NodeJS
Run the command:

choco upgrade nodejs -y

It will upgrade to latest NodeJS + npm. In case you want to upgrade npm only, let run PowerShell as Administrator and execute below commands:

Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force
npm install -g npm-windows-upgrade
npm-windows-upgrade
Or

npm-windows-upgrade -p -v latest

3. Uninstall NodeJS
Run the command:

choco uninstall nodejs -y

I love chocolatey ❤

Subscribe to RSS Feed Follow me on Twitter!