เริ่มต้นเขียน Angular2 กันดีกว่า

เริ่มต้นเขียน Angular2 กันดีกว่า Cover Image

เมื่อเร็วๆนี้ Angular ได้ปล่อยตัว Angular 4 ออกมา ซึ่งจริงๆแล้ว มันก็ไม่ได้แตกต่างกับ Angular 2 เท่าไหร่นัก เป็นเพียงแค่ Improve Feature เล็กๆน้อยๆ ไม่ได้ Major change แบบตอน Angular 1 มา Angular 2 นั่นเอง

วันนี้ก็เลยมานำเสนอบทความการเขียน Angular 2 ฉบับเริ่มต้นกันครับ ซึ่งตัว Angular 2 นั้นเขียนด้วย TypeScript และตัวอย่างของวันนี้จะเป็นเว็บแบบ Single Page Application ที่เอาไว้แสดงข้อมูล Heroes ของเกม Dota 2 ครับ

หน้าตาของ App เมื่อเขียนเสร็จก็จะได้ประมาณรูปด้านล่างครับ

Example Screen

จะเรียก Angular เวอร์ชั่น 2 ขึ้นไปว่า Angular ส่วน Angular 1.x จะเรียกว่า AngularJS

Enviroments

ในตัวอย่างนี้จะใช้ Angular-cli ในการสร้างโปรเจ็ค รวมถึงการสร้าง Component และเขียนด้วย TypeScript

ฉะนั้นสิ่งที่ควรมีก็คือ

หากยังไม่ได้ติดตั้ง Node.js สามารถอ่านได้จากบทความนี้ Node.js คืออะไร ? + เริ่มต้นใช้งาน Node.js

Step 1 : Installation

$ npm install typescript -g
$ npm install angular-cli -g

บน Mac OS X หรือ Linux อาจจะต้องติดตั้ง Watchman เพิ่มเติม (ตัว Watchman เอาไว้สำหรับ Monitoring file ของ AngularCLI)

$ brew install watchman

หากไม่มี Homebrew หรือยังไม่รู้จัก อ่านบทความนี้ครับ Homebrew คืออะไร? + สอนวิธีใช้งาน

Step 2 : Create Project

เริ่มต้นสร้างโปรเจ็ค Angular ด้วยการใช้ angular-cli จะใช้คำสั่ง ng new <ชื่อโปรเจ็ค> ครับ

$ ng new awesome-app

installing ng2
  create .editorconfig
  create README.md
  create src/app/app.component.css
  create src/app/app.component.html
  create src/app/app.component.spec.ts
  create src/app/app.component.ts
  create src/app/app.module.ts
  create src/assets/.gitkeep
  create src/environments/environment.prod.ts
  create src/environments/environment.ts
  create src/favicon.ico
  create src/index.html
  create src/main.ts
  create src/polyfills.ts
  create src/styles.css
  create src/test.ts
  create src/tsconfig.json
  create angular-cli.json
  create e2e/app.e2e-spec.ts
  create e2e/app.po.ts
  create e2e/tsconfig.json
  create .gitignore
  create karma.conf.js
  create package.json
  create protractor.conf.js
  create tslint.json
Successfully initialized git.
Installing packages for tooling via npm.

Project 'awesome-app' successfully created.

ไฟล์และ dependencies ต่างๆจะถูกติดตั้งด้วย angular-cli

เมื่อสร้างโปรเจ็คด้วย angular-cli เสร็จแล้ว ก็ลองรัน server ดูด้วยคำสั่ง ng serve

$ cd awesome-app
$ ng serve

เข้าเว็บผ่าน http://localhost:4200/ จะเห็นหน้าเว็บ Angular

Angular HelloWorld

Step 3 : Project Structure

เมื่อลอง browse file ในโฟลเดอร์ src จะเห็นว่ามีไฟล์และโฟลเดอร์ต่างๆดังด้านล่างนี้

├── app/
│   ├── app.component.css
│   ├── app.component.html
│   ├── app.component.spec.ts
│   ├── app.component.ts
│   └── app.module.ts
├── assets/
├── environments/
│   ├── environment.prod.ts
│   └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css
├── test.ts
└── tsconfig.json

ลองดูที่ไฟล์ src/app/app.component.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>AwesomeApp</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

จะสังเกตเห็น tag <app-root> ใน Element <app-root>Loading...</app-root> ซึ่งหากใน AngularJS (Angular 1) จะเรียกว่า Diretives ส่วนใน Angular (Angular 2)เราจะเรียกมันใหม่ว่า Component

แล้วทีนี้ Component ใน Angular มันสร้างมาได้ยังไง? ลองเปิดไฟล์ src/app/app.component.ts ขึ้นมา

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app works!';
}

จากโค๊ดด้านบน มีการ import Component จากโมดูล @angular/core และประกาศ Component โดยมี property คือ

  • selector : ชื่อ component ที่เราต้องการ เช่น app-root เวลาใช้งานก็คือ <app-root></app-root>
  • templateUrl: Path ไฟล์ html ที่เราต้องการให้ Component นี้มัน render
  • styleUrls: สามารถกำหนด stylesheet เฉพาะ component ได้
  • template: หรือหากไม่ระบุ url ก็สามารถใส่ HTML tag ลงไปในนี้ได้เลย

ซึ่งหากสังเกตแล้ว จริงๆ 1 component ของ Angular จะมีด้วยกัน 4 ไฟล์ คือ

  • app.component.css : เอาไว้สำหรับจัดการ stylesheet ของ component นั้น
  • app.component.html : ไฟล์ html template ของ Component
  • app.component.spec.ts : ไฟล์สำหรับ test
  • app.component.ts : ไฟล์สำหรับ implement logic component

Step 4 : Generate component

เนื่องจากว่าตอนเราสร้างโปรเจ็คด้วย Angular-CLI app.component จะถูกสร้างมาให้อัตโนมัติอยู่แล้ว ทีนี้เราลองมาสร้าง Component เพิ่มด้วยคำสั่ง CLI กันครับ

คำสั่งก็ง่ายๆเลยคือ ng generate component <ชื่อ component> เช่น

ng generate component about

installing component
  create src/app/about/about.component.css
  create src/app/about/about.component.html
  create src/app/about/about.component.spec.ts
  create src/app/about/about.component.ts
  update src/app/app.module.ts

หรือจะใช้คำสั่งแบบสั้นๆ ก็ได้ คือ ng g c about

จะเห็นว่าไฟล์ถูกสร้างมา 4 ไฟล์ รูปแบบคล้ายๆการสร้าง 1 Component และมีอัพเดทไฟล์ src/app/app.module.ts

ซึ่งหากเราเปิดไฟล์ app.module.ts ดูจะพบว่า

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { AboutComponent } from './about/about.component';

@NgModule({
  declarations: [
    AppComponent,
    AboutComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

สิ่งสำคัญมีอยู่ 3 ส่วนครับ คือ

  1. declarations : เอาไว้กำหนด Component ของเราให้ใช้ได้ภายใน app ถ้าไม่ใส่ในส่วนนี้ Component ที่เราสร้างมาตัว angular จะไม่รู้จัก
  2. imports : สำหรับ import โมดูลต่างๆเพื่อเอาไว้ใช้ในโปรเจ็ค เช่น HttpModule สำหรับ GET/POST Http เป็นต้น
  3. providers : ตัวนี้จะเป็นสำหรับส่วนที่เอาไว้ประกาศ เวลาเรามี Service

Step 5: Create Routing

ต่อมาเป็นการสร้าง Routing เพื่อจะกำหนดว่า เวลาเราเข้า path นี้ จะให้มันไปไหน เช่น เข้า /about จะให้แสดงหน้า About เข้าหน้า /heroes ก็แสดงรายชื่อ Heroes เป็นต้น

ตัวอย่างการกำหนด Routing โดย App นี้จะแบ่งออกเป็น 3 หน้าคือ

  • Home : เป็นหน้าแรก
  • Heroes : แสดงรายชื่อ Heroes
  • About : หน้ารายละเอียด

ซึ่งทัง้สามหน้า จะเป็นแค่ Static website มีเพียงแค่ Heroes ที่จะต้องดึง Service API นั่นเอง

สร้างไฟล์ชื่อ app-routing.module.ts ขึ้นมา

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AboutComponent } from './about/about.component';

const routes: Routes = [
  {
    path: 'about',
    component: AboutComponent
  }
];

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

export class AppRoutingModule {}

โดย Angular จะมาพร้อมโมดูล RouterModule และ Routes ให้เราไว้สำหรับ Config Routing อยู่แล้ว เราก็กำหนดได้เลยว่า path = about ให้มันแสดง component ชื่อ AboutComponent

ตัว path ไม่ต้องใส่ slash (/)

กลับไปที่ไฟล์ app.module.ts ทำการ import RoutingModule ที่เราสร้างนี้ไปด้วย

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { AboutComponent } from './about/about.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    AppComponent,
    AboutComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

และที่ไฟล์ app.component.html ให้เราเพิ่ม tag <router-outlet></router-outlet> เพื่อให้ Angular Router ทำงานนั่นเอง เมื่อเวลา Routing มันแมตกับ path ที่เรากำหนด ตัว Component ก็จะ replace ข้อมูลแทนแท็กนี้นั่นเอง

<div class="container">
  <router-outlet></router-outlet>
</div>

ต่อมา Generate หน้า Home และ Hero List สำหรับเป็น Component และ map routing ไว้ก่อน (ยังไม่มีคอนเท้น)

ng g c home
ng g c hero-list

และทำการเพิ่ม Routing ในไฟล์ app-routing.module.ts

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

import { AboutComponent } from './about/about.component';
import { HomeComponent } from './home/home.component';
import { HeroListComponent } from './hero-list/hero-list.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'about',
    component: AboutComponent
  },
  {
    path: 'heroes',
    component: HeroListComponent
  },
  {
    path: '**',
    redirectTo: '/',
    pathMatch: 'full'
  }
];

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

export class AppRoutingModule {}

โดย path: '' คือจะเป็นหน้า Home และ path: '**' คือไม่เจอ path ใดๆเลย จะให้มัน redirect ไปที่ Home นั่นเอง

ตอนนี้ App เราก็มี 3 หน้าแล้ว คือ

Step 6: Services

ต่อมาส่วน Service ที่เราใช้ request เพื่อจะได้ข้อมูล Heroes แบบทั้งหมด และอีก request สำหรับ Hero id นั้นๆ

ไฟล์ชื่อ hero.service.ts

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class HeroService {

  constructor(private http: Http) { }

  findAll() {
    return this.http.get('http://localhost:5000/api/heroes')
      .map(res => res.json());
  }

  findOne(id) {
    return this.http.get(`http://localhost:5000/api/heroes/${id}`)
      .map(res => res.json());
  }

}

จะเห็นว่าโค๊ดด้านบน มีการ import { Http } from '@angular/http'; ซึ่งเป็น module สำหรับ Http ในส่วนของ

`constructor(private http:Http) {}`

จะมีค่าเท่ากับเราประกาศ

let http;

constructor(http: Http) {
  this.http = http;
}

คือเราทำการประกาศตัวแปร http ใน constructor ได้เลย

ต่อมาไฟล์ hero-list/hero-list.component.ts

import { Component, OnInit } from '@angular/core';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-hero-list',
  templateUrl: './hero-list.component.html',
  styleUrls: ['./hero-list.component.scss']
})
export class HeroListComponent implements OnInit {

  heroes: any = [];

  constructor(private heroService: HeroService) { }

  ngOnInit() {
    this.heroService.findAll().subscribe(response => {
      this.heroes = response.data;
    });
  }

}

ด้านบน เรา Inject HeroService ที่สร้างไว้ มาไว้ในนี้ผ่านตัวแปร heroService จากนั้นก็สามารถ call method findAll ได้เลย

แต่จะเห็นว่ามีส่วน .subscribe() ตรงนี้ทำหน้าที่คล้ายๆกับ Promise().then() คือเมื่อได้ response มันก็จะ ทำงานในส่วนของ subscribe() ซึ่งในที่นี้คือ นำค่า response ไปใส่ไว้ตัวแปรชื่อ heroes

เช่นเดียวกันกับ hero/hero.component.ts จะ Implement คล้ายๆกัน เพียงแต่ ส่วนที่เป็น /heroes/:id จะต้องใช้ ActivatedRoute ในการหา params ดังโค๊ดด้านล่าง

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-hero',
  templateUrl: './hero.component.html',
  styleUrls: ['./hero.component.scss']
})
export class HeroComponent implements OnInit {

  hero: any;

  constructor(private route: ActivatedRoute, private heroService: HeroService) { }

  ngOnInit() {

    this.route.params.subscribe(params => {
      const id = params['id'];
      this.heroService.findOne(id).subscribe(response => {
        this.hero = response.data;
      });

    });

  }

  like() {
    console.log('like');
  }

}

สุดท้าย เพิ่ม HeroService ไปในส่วน provider ของ app.module.ts ด้วย

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { AboutComponent } from './about/about.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroComponent } from './hero/hero.component';
import { HomeComponent } from './home/home.component';
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroService } from './hero.service';

@NgModule({
  declarations: [
    AppComponent,
    AboutComponent,
    HeroComponent,
    HomeComponent,
    HeroListComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  providers: [HeroService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Step 7: Add HTML Markup

ต่อมาผมทำการเพิ่ม HTML Markup ของแต่ละหน้าเริ่มจากไฟล์ index.html ทำการเพิ่ม CSS ของ Bulma

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>AwesomeApp</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.2/css/bulma.css">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

ต่อมาไฟล์ app.component.html ก็จะเพิ่มส่วน Navbar เพื่อจะได้ Link ไปหน้าต่างๆได้

<div class="container">
  <nav class="nav">
    <div class="nav-left">
      <a class="nav-item" routerLinkActive="active" routerLink="/">Home</a>
    </div>

    <div id="nav-menu" class="nav-right nav-menu">
      <a class="nav-item" routerLinkActive="active" routerLink="/heroes">
        Heroes
      </a>
      <a class="nav-item" routerLinkActive="active" routerLink="/about">
        About
      </a>
    </div>
  </nav>
  <router-outlet></router-outlet>
</div>

ซึ่ง HTML Tag ด้านบน เราจะเห็น routerLink และ routerLinkActive ซึ่งเป็นส่วนของ Angular Router โดย routerLink เอาไว้สำหรับบอกว่า Link นี้ จะไปไหน โดยมันจะไป map กับที่เรา config ไว้ใน app-routing.module.ts นั่นเอง

ไฟล์ app.component.css (บทความใช้ .scss)

body {
  background-color: #ccc;
}

nav {
  margin-bottom: 20px;
}

ต่อมา ส่วนหน้า About ก็ใส่ Content เพิ่มเข้าไป

<div class="tile is-ancestor">
  <div class="tile is-half is-vertical is-parent">
    <div class="tile is-child box">
      <p class="title">About</p>

      <div class="content">
        <p>This is demo application built with Angular 2<p>
        <ul>
          <li><a href="https://www.eventbrite.co.uk/e/building-spa-with-nodejs-and-angular2-tickets-32402850799">Building SPA with Hapi.js and Angular 2</a></li>
          <li><a href="https://github.com/Phonbopit/node-x-angular">Source code on Github</a></li>
        </ul>
      </div>

    </div>
  </div>
  <div class="tile is-half is-parent">
    <div class="tile is-child box">
      <p class="title">Credit</p>

      <div class="content">
        <ul>
          <li><a href="http://bulma.io/">Bulma</a> for CSS Framework</li>
          <li><a href="https://www.opendota.com/">OpenDota API</a> for mock data</li>
        </ul>
      </div>

    </div>
  </div>
</div>

หน้า hero-list/hero-list.component.html ก็เช่นเดียวกัน

<div class="columns is-multiline ">
  <div class="column is-one-quarter" *ngFor="let hero of heroes">
    <div class="card">
      <div class="card-image">
        <figure class="image is-4by3">
          <img src="https://api.opendota.com{{hero.img}}" alt="Icon">
        </figure>
      </div>
      <div class="card-content">
        <div class="media">
          <div class="media-content">
            <p class="title is-4"><a routerLink="/heroes/{{hero.id}}" class="is-success">{{hero.localized_name}}</a></p>
            <p class="subtitle is-6">{{hero.name}}</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

และสุดท้ายหน้า Hero Detail hero/hero.component.ts

<div class="card">
  <div class="card-image">
    <figure class="image is-4by3">
      <img src="https://api.opendota.com{{hero?.img}}" alt="Icon">
    </figure>
  </div>
  <div class="card-content">
    <div class="media">
      <div class="media-content">
        <p class="title is-4">{{hero?.localized_name}}</p>
        <p class="subtitle is-6">{{hero?.name}}</p>
      </div>
    </div>
  </div>
</div>

<hr>

<div class="columns">
  <div class="column">
    <a class="button is-outlined" routerLink="/heroes">Back</a>
  </div>
    <div class="column">
    <a class="button is-outlined" (click)="like()">Like</a>
  </div>
</div>

Step 8: Backend

สำหรับส่วน Backend จะไม่อธิบายถึงละกันครับ เพราะว่าอยู่นอก scope ของบทความ ซึ่งสามารถดูได้จาก Link นี้ https://github.com/Phonbopit/node-x-angular/tree/master/server

ซึ่งเมื่อเราทำการรัน Server หรือ มี response จาก backend ตัวหน้าเว็บที่เราทำไว้ก็จะสามารถใช้งานได้นั่นเอง

เป็นอันเรียบร้อย :)

Conclusion

บทความนี้ก็เป็นคล้ายๆ Walkthrough สำหรับทำ Single Page Appliction ด้วย Angular 2 หรือ 4 แบบง่ายๆนะครับ อาจจะไม่ได้เจาะลึกลงไปในแต่ละรายละเอียด ซึ่งอยากให้มองเห็นภาพรวมแล้วก็ไปต่อยอดศึกษาเพิ่มเติมเอาเองนะครับ เช่น Angular Router ว่ามันทำอะไรได้บ้าง หรือ RxJS หรือ Observable มันใช้ยังไง ทำอะไรอีกได้บ้าง เป็นต้น หวังว่าบทความนี้จะมีประโยชน์กับผู้ที่กำลังสนใจ Angular นะครับ

สุดท้าย Source Code สามารถดูได้จาก Link ด้านล่างเลยครับ

Source Code

Chai Chai Phonbopit : Software Engineer @Nextzy • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Android, JavaScript (Node.js) และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football

บทความล่าสุด